Dockerizing a Node.js App in 2026: The Practical Guide
Containerize your app. Deploy anywhere. Never worry about "it works on my machine" again.
Why Docker?
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! 🎉
The Basics
# 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"]
Key Concepts Explained
Multi-Stage Builds
# ❌ 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)
Layer Caching
# 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
.dockerignore
# Like .gitignore for Docker — reduces context size
node_modules
npm-debug.log
dist
.git
.env
.env.local
coverage
.vscode
.idea
*.md
Dockerfile*
docker-compose*
docker-compose.yml for Development
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:
Development Dockerfile
# 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
Useful Commands
# 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 Tips
# 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"]
Quick Reference Card
| Command | Purpose |
|---|---|
docker build -t myapp . |
Build image |
docker run -p 3000:3000 myapp |
Run container |
docker-compose up |
Start all services |
docker-compose down |
Stop all services |
docker ps |
List running containers |
docker images |
List images |
docker logs <id> |
Container logs |
docker exec -it <id> sh |
Shell into container |
docker system prune |
Clean up unused resources |
Are you using Docker in development or just production?
Follow @armorbreak for more DevOps content.
Top comments (0)