D
Dev SOPKnowledge Base
Search
← All topics

Docker Mastery: Multi-Stage Builds, Security, and Compose Orchestration

Docker production reference — multi-stage builds for Node.js/Next.js, non-root user hardening, secrets management, Docker Compose production/dev patterns, image optimization, health checks, .dockerignore, and cross-platform builds.

dockercontainersdevopssecuritycomposebuildkit
Agent trigger phrases: Docker · Dockerfile · docker-compose · Docker multi-stage · container security · Docker production · Docker image size · Docker health check

Overview

Docker production patterns for Node.js and Next.js applications. The gold standard is multi-stage builds with a non-root user, minimal image, and no secrets in layers.

Multi-Stage Build (Gold Standard)

# Stage 1: Dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# Stage 3: Runner (minimal production image)
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy only what's needed
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]

Security Hardening

# Non-root user (always)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Read-only filesystem (where possible)
# In docker-compose:
# read_only: true
# tmpfs:
#   - /tmp
#   - /var/run

# No new privileges
# docker run --security-opt no-new-privileges:true

# Specific capabilities only
# docker run --cap-drop ALL --cap-add NET_BIND_SERVICE

Secrets Management

Never put secrets in ENV or ARG build instructions — they appear in image history:

# BAD — secret in image history forever
ENV API_KEY=sk-abc123

# GOOD — runtime env var (not in image)
# Pass at runtime: docker run -e API_KEY=... my-image

# GOOD — Docker secrets (Swarm/Compose v3.1+)
# docker-compose.yml with secrets
services:
  app:
    secrets:
      - db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Docker Compose: Production

# docker-compose.prod.yml
version: "3.8"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: runner
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 512M
        reservations:
          memory: 256M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Docker Compose: Development Override

# docker-compose.override.yml (auto-merged with docker-compose.yml)
services:
  app:
    build:
      target: builder
    volumes:
      - .:/app
      - /app/node_modules
    command: npm run dev
    environment:
      NODE_ENV: development
# Dev: uses docker-compose.yml + docker-compose.override.yml
docker compose up

# Prod: uses only docker-compose.prod.yml
docker compose -f docker-compose.prod.yml up -d

.dockerignore (Essential)

node_modules
.next
.git
.env*
*.log
coverage
.DS_Store
README.md
docker-compose*.yml
Dockerfile*
.github
tests

Missing .dockerignore is the #1 cause of bloated images (node_modules copying into context).

Image Size Optimization

# Use alpine base
FROM node:20-alpine

# Combine RUN commands (fewer layers)
RUN apk add --no-cache curl && \
    rm -rf /var/cache/apk/*

# Only install production deps in final stage
RUN npm ci --omit=dev && npm cache clean --force

# Use .dockerignore
# Remove dev files
RUN find . -name "*.test.*" -delete

BuildKit (Enable for All Builds)

# Enable BuildKit
DOCKER_BUILDKIT=1 docker build .

# Or set in Docker config
echo '{"features": {"buildkit": true}}' > /etc/docker/daemon.json

# Mount cache in builds (speeds up npm install across builds)
RUN --mount=type=cache,target=/root/.npm npm ci

Health Check Patterns

# HTTP health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/api/health || exit 1

# Process check (for services without HTTP)
HEALTHCHECK CMD pgrep -x node || exit 1

Common Commands

# Build
docker build -t my-app:latest .
docker build -t my-app:v1.2.3 --build-arg VERSION=1.2.3 .

# Run
docker run -d --name my-app -p 3000:3000 --env-file .env my-app:latest

# Inspect
docker logs my-app --follow
docker stats my-app
docker exec -it my-app sh

# Cleanup
docker system prune -af         # remove all unused images/containers/networks
docker image prune -f           # remove dangling images only

Cross-Platform Builds (M1/ARM)

# Build for linux/amd64 from Apple Silicon
docker buildx build --platform linux/amd64 -t my-app:latest --push .

# Multi-platform
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t my-app:latest \
  --push .