Tools: Latest: Dockerizing a Node.js App in 2026: The Practical Guide
Dockerizing a Node.js App in 2026: The Practical Guide
Why Docker?
The Basics
Key Concepts Explained
Multi-Stage Builds
Layer Caching
.dockerignore
docker-compose.yml for Development
Development Dockerfile
Useful Commands
Production Tips
Quick Reference Card Containerize your app. Deploy anywhere. Never worry about "it works on my machine" again. Are you using Docker in development or just production? Follow @armorbreak for more DevOps content. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
Without Docker: Your machine → Node 22, Ubuntu, specific libraries Server → Node 18, Alpine, different glibc Colleague's Mac → Node 20, macOS, different everything → "Works on my machine!" 😤 With Docker: Everyone → Same OS, same Node version, same dependencies → Works everywhere! 🎉
Without Docker: Your machine → Node 22, Ubuntu, specific libraries Server → Node 18, Alpine, different glibc Colleague's Mac → Node 20, macOS, different everything → "Works on my machine!" 😤 With Docker: Everyone → Same OS, same Node version, same dependencies → Works everywhere! 🎉
Without Docker: Your machine → Node 22, Ubuntu, specific libraries Server → Node 18, Alpine, different glibc Colleague's Mac → Node 20, macOS, different everything → "Works on my machine!" 😤 With Docker: Everyone → Same OS, same Node version, same dependencies → Works everywhere! 🎉
# Stage 1: Build
FROM node:22-alpine AS builder WORKDIR /app # Copy dependency files first (layer caching!)
COPY package*.json ./
RUN npm ci # Copy source code
COPY . .
RUN npm run build # Stage 2: Production (smaller image)
FROM node:22-alpine AS runner WORKDIR /app RUN addgroup -g 1001 appuser && \ adduser -u 1001 -G appuser -s /bin/sh -D appuser # Copy built files from builder stage
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
COPY --from=builder --chown=appuser:appuser /app/package.json ./package.json USER appuser EXPOSE 3000 CMD ["node", "dist/server.js"]
# Stage 1: Build
FROM node:22-alpine AS builder WORKDIR /app # Copy dependency files first (layer caching!)
COPY package*.json ./
RUN npm ci # Copy source code
COPY . .
RUN npm run build # Stage 2: Production (smaller image)
FROM node:22-alpine AS runner WORKDIR /app RUN addgroup -g 1001 appuser && \ adduser -u 1001 -G appuser -s /bin/sh -D appuser # Copy built files from builder stage
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
COPY --from=builder --chown=appuser:appuser /app/package.json ./package.json USER appuser EXPOSE 3000 CMD ["node", "dist/server.js"]
# Stage 1: Build
FROM node:22-alpine AS builder WORKDIR /app # Copy dependency files first (layer caching!)
COPY package*.json ./
RUN npm ci # Copy source code
COPY . .
RUN npm run build # Stage 2: Production (smaller image)
FROM node:22-alpine AS runner WORKDIR /app RUN addgroup -g 1001 appuser && \ adduser -u 1001 -G appuser -s /bin/sh -D appuser # Copy built files from builder stage
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
COPY --from=builder --chown=appuser:appuser /app/package.json ./package.json USER appuser EXPOSE 3000 CMD ["node", "dist/server.js"]
# ❌ Bad: Single stage = big image with dev tools included
FROM node:22
WORKDIR /app
COPY . .
RUN npm install # Installs ALL deps including devDependencies
RUN npm run build
CMD ["node", "server.js"]
# Image size: ~1.5GB (includes TypeScript, test frameworks, etc.) # ✅ Good: Multi-stage = small production image
FROM node:22 AS build # Has dev tools
→ npm install + build
FROM node:22-alpine # Minimal runtime only
→ COPY --from=build # Only copy what you need
# Image size: ~150MB (only runtime + your code)
# ❌ Bad: Single stage = big image with dev tools included
FROM node:22
WORKDIR /app
COPY . .
RUN npm install # Installs ALL deps including devDependencies
RUN npm run build
CMD ["node", "server.js"]
# Image size: ~1.5GB (includes TypeScript, test frameworks, etc.) # ✅ Good: Multi-stage = small production image
FROM node:22 AS build # Has dev tools
→ npm install + build
FROM node:22-alpine # Minimal runtime only
→ COPY --from=build # Only copy what you need
# Image size: ~150MB (only runtime + your code)
# ❌ Bad: Single stage = big image with dev tools included
FROM node:22
WORKDIR /app
COPY . .
RUN npm install # Installs ALL deps including devDependencies
RUN npm run build
CMD ["node", "server.js"]
# Image size: ~1.5GB (includes TypeScript, test frameworks, etc.) # ✅ Good: Multi-stage = small production image
FROM node:22 AS build # Has dev tools
→ npm install + build
FROM node:22-alpine # Minimal runtime only
→ COPY --from=build # Only copy what you need
# Image size: ~150MB (only runtime + your code)
# Order matters! Docker caches each layer. # ✅ Good order — changes rarely → put first
COPY package*.json ./ # Layer 1: Changes when deps change
RUN npm ci # Layer 2: Cached if package.json didn't change
COPY . . # Layer 3: Changes on every code edit
RUN npm run build # Layer 4: Rebuilds only when source changes # ❌ Bad order — invalidates cache too often
COPY . . # Changes every time you save a file!
RUN npm install # Reinstalls deps every time (slow!)
RUN npm run build
# Order matters! Docker caches each layer. # ✅ Good order — changes rarely → put first
COPY package*.json ./ # Layer 1: Changes when deps change
RUN npm ci # Layer 2: Cached if package.json didn't change
COPY . . # Layer 3: Changes on every code edit
RUN npm run build # Layer 4: Rebuilds only when source changes # ❌ Bad order — invalidates cache too often
COPY . . # Changes every time you save a file!
RUN npm install # Reinstalls deps every time (slow!)
RUN npm run build
# Order matters! Docker caches each layer. # ✅ Good order — changes rarely → put first
COPY package*.json ./ # Layer 1: Changes when deps change
RUN npm ci # Layer 2: Cached if package.json didn't change
COPY . . # Layer 3: Changes on every code edit
RUN npm run build # Layer 4: Rebuilds only when source changes # ❌ Bad order — invalidates cache too often
COPY . . # Changes every time you save a file!
RUN npm install # Reinstalls deps every time (slow!)
RUN npm run build
# Like .gitignore for Docker — reduces context size
node_modules
npm-debug.log
dist
.git
.env
.env.local
coverage
.vscode
.idea
*.md
Dockerfile*
docker-compose*
# Like .gitignore for Docker — reduces context size
node_modules
npm-debug.log
dist
.git
.env
.env.local
coverage
.vscode
.idea
*.md
Dockerfile*
docker-compose*
# Like .gitignore for Docker — reduces context size
node_modules
npm-debug.log
dist
.git
.env
.env.local
coverage
.vscode
.idea
*.md
Dockerfile*
docker-compose*
version: '3.8' services: app: build: context: . dockerfile: Dockerfile.dev ports: - '3000:3000' volumes: # Hot reload: map local files into container - .:/app - /app/node_modules # Use container's node_modules environment: - NODE_ENV=development - DATABASE_URL=postgres://postgres:password@db:5432/myapp - REDIS_URL=redis://redis:6379 depends_on: db: condition: service_healthy redis: condition: service_healthy db: image: postgres:16-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: myapp ports: - '5432:5432' volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - '6379:6379' volumes: - redisdata:/data healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 5s volumes: pgdata: redisdata:
version: '3.8' services: app: build: context: . dockerfile: Dockerfile.dev ports: - '3000:3000' volumes: # Hot reload: map local files into container - .:/app - /app/node_modules # Use container's node_modules environment: - NODE_ENV=development - DATABASE_URL=postgres://postgres:password@db:5432/myapp - REDIS_URL=redis://redis:6379 depends_on: db: condition: service_healthy redis: condition: service_healthy db: image: postgres:16-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: myapp ports: - '5432:5432' volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - '6379:6379' volumes: - redisdata:/data healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 5s volumes: pgdata: redisdata:
version: '3.8' services: app: build: context: . dockerfile: Dockerfile.dev ports: - '3000:3000' volumes: # Hot reload: map local files into container - .:/app - /app/node_modules # Use container's node_modules environment: - NODE_ENV=development - DATABASE_URL=postgres://postgres:password@db:5432/myapp - REDIS_URL=redis://redis:6379 depends_on: db: condition: service_healthy redis: condition: service_healthy db: image: postgres:16-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: myapp ports: - '5432:5432' volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - '6379:6379' volumes: - redisdata:/data healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 5s volumes: pgdata: redisdata:
# Dockerfile.dev — optimized for development
FROM node:22-alpine WORKDIR /app # Install dev tools needed for hot reload
RUN npm install -g nodemon # Copy deps first (cached layer)
COPY package*.json ./
RUN npm ci # Don't copy source here — use volume mount instead EXPOSE 3000 CMD ["nodemon", "--legacy-watch", "server.js"]
# --legacy-watch: fixes file watching in mounted volumes
# Dockerfile.dev — optimized for development
FROM node:22-alpine WORKDIR /app # Install dev tools needed for hot reload
RUN npm install -g nodemon # Copy deps first (cached layer)
COPY package*.json ./
RUN npm ci # Don't copy source here — use volume mount instead EXPOSE 3000 CMD ["nodemon", "--legacy-watch", "server.js"]
# --legacy-watch: fixes file watching in mounted volumes
# Dockerfile.dev — optimized for development
FROM node:22-alpine WORKDIR /app # Install dev tools needed for hot reload
RUN npm install -g nodemon # Copy deps first (cached layer)
COPY package*.json ./
RUN npm ci # Don't copy source here — use volume mount instead EXPOSE 3000 CMD ["nodemon", "--legacy-watch", "server.js"]
# --legacy-watch: fixes file watching in mounted volumes
# Build and run
docker-compose up --build # Build + start all services
docker-compose up -d # Start in background
docker-compose down # Stop and remove containers
docker-compose logs -f app # Follow app logs
docker-compose exec app sh # Shell into container
docker-compose exec db psql -U postgres # Connect to Postgres # One-off commands
docker-compose exec app npm run migrate
docker-compose exec app npm run seed
docker-compose exec app npx jest # Cleanup (when things get weird)
docker system prune -a # Remove unused images/containers/volumes
docker compose down -v # Remove volumes too (resets DB!) # Debug container
docker run -it --rm myimage sh # Interactive shell
docker logs <container_id> # View logs
docker stats # Resource usage of running containers
# Build and run
docker-compose up --build # Build + start all services
docker-compose up -d # Start in background
docker-compose down # Stop and remove containers
docker-compose logs -f app # Follow app logs
docker-compose exec app sh # Shell into container
docker-compose exec db psql -U postgres # Connect to Postgres # One-off commands
docker-compose exec app npm run migrate
docker-compose exec app npm run seed
docker-compose exec app npx jest # Cleanup (when things get weird)
docker system prune -a # Remove unused images/containers/volumes
docker compose down -v # Remove volumes too (resets DB!) # Debug container
docker run -it --rm myimage sh # Interactive shell
docker logs <container_id> # View logs
docker stats # Resource usage of running containers
# Build and run
docker-compose up --build # Build + start all services
docker-compose up -d # Start in background
docker-compose down # Stop and remove containers
docker-compose logs -f app # Follow app logs
docker-compose exec app sh # Shell into container
docker-compose exec db psql -U postgres # Connect to Postgres # One-off commands
docker-compose exec app npm run migrate
docker-compose exec app npm run seed
docker-compose exec app npx jest # Cleanup (when things get weird)
docker system prune -a # Remove unused images/containers/volumes
docker compose down -v # Remove volumes too (resets DB!) # Debug container
docker run -it --rm myimage sh # Interactive shell
docker logs <container_id> # View logs
docker stats # Resource usage of running containers
# Production optimizations
FROM node:22-alpine # Security: Run as non-root user
RUN addgroup -g 1001 app && \ adduser -u 1001 -G app -s /bin/sh -D app # Health check
HEALTHCHECK --interval=30s --timeout=3s \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 # Set Node to production mode
ENV NODE_ENV=production # Optimize Node.js memory
ENV NODE_OPTIONS="--max-old-space-size=512" WORKDIR /app
COPY --from=builder --chown=app:app ./dist ./dist
COPY --from=builder --chown=app:app ./node_modules ./node_modules
COPY --from=builder --chown=app:app ./package.json ./package.json USER app EXPOSE 3000 # Graceful shutdown
STOPSIGNAL SIGTERM CMD ["node", "dist/server.js"]
# Production optimizations
FROM node:22-alpine # Security: Run as non-root user
RUN addgroup -g 1001 app && \ adduser -u 1001 -G app -s /bin/sh -D app # Health check
HEALTHCHECK --interval=30s --timeout=3s \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 # Set Node to production mode
ENV NODE_ENV=production # Optimize Node.js memory
ENV NODE_OPTIONS="--max-old-space-size=512" WORKDIR /app
COPY --from=builder --chown=app:app ./dist ./dist
COPY --from=builder --chown=app:app ./node_modules ./node_modules
COPY --from=builder --chown=app:app ./package.json ./package.json USER app EXPOSE 3000 # Graceful shutdown
STOPSIGNAL SIGTERM CMD ["node", "dist/server.js"]
# Production optimizations
FROM node:22-alpine # Security: Run as non-root user
RUN addgroup -g 1001 app && \ adduser -u 1001 -G app -s /bin/sh -D app # Health check
HEALTHCHECK --interval=30s --timeout=3s \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 # Set Node to production mode
ENV NODE_ENV=production # Optimize Node.js memory
ENV NODE_OPTIONS="--max-old-space-size=512" WORKDIR /app
COPY --from=builder --chown=app:app ./dist ./dist
COPY --from=builder --chown=app:app ./node_modules ./node_modules
COPY --from=builder --chown=app:app ./package.json ./package.json USER app EXPOSE 3000 # Graceful shutdown
STOPSIGNAL SIGTERM CMD ["node", "dist/server.js"]