Tools: Latest: Dockerizing a Node.js App in 2026: The Practical Guide

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

Code Block

Copy

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"]