DEV Community

Alex Chen
Alex Chen

Posted on

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

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! 🎉
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

.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*
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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)