D
Dev SOPKnowledge Base
Search
← All topics

Docker Multi-Stage Builds: Production Patterns and Security Hardening

Docker multi-stage build patterns for Next.js and Node.js, security hardening, Docker Compose production setup, image size optimization, and health checks.

dockercontainersdevopsdeploymentsecurity
Agent trigger phrases: Docker multi-stage build · Docker production · Dockerfile Next.js · Docker security · Docker Compose production · container hardening

Overview

Multi-stage Docker builds for production Node.js/Next.js applications. Covers image size optimization, security hardening, and Docker Compose patterns.

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 (smallest possible image)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

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

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
ENV HOSTNAME="0.0.0.0"

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

Requires output: "standalone" in next.config.ts. The runner image contains only the production output — no source files, no dev dependencies.

Security Hardening Rules

# Always use specific image versions — never :latest
FROM node:20.18.1-alpine AS base

# Run as non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Read-only filesystem where possible
# Set in docker run: --read-only --tmpfs /tmp

# No shell in production image
RUN rm -f /bin/sh  # Only if app doesn't need shell

# Minimal apt/apk — only what the app needs
RUN apk add --no-cache curl  # curl for healthcheck only

.dockerignore (Essential)

node_modules
.next
.git
*.log
*.md
.env*
.DS_Store
coverage
.nyc_output
dist

A missing .dockerignore sends gigabytes of node_modules into build context. Always create it first.

Docker Compose — Production Pattern

services:
  app:
    image: my-app:${APP_VERSION:-latest}
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
    env_file:
      - .env.production
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          memory: 256M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      app:
        condition: service_healthy

Development Override

# docker-compose.override.yml (auto-loaded in dev)
services:
  app:
    build:
      context: .
      target: builder
    volumes:
      - .:/app
      - /app/node_modules      # Prevent host node_modules overriding container
    environment:
      - NODE_ENV=development
    command: npm run dev
    ports:
      - "3000:3000"
      - "9229:9229"            # Node debugger

Health Check Patterns

// app/api/health/route.ts
export async function GET() {
  try {
    // Lightweight dependency check
    await db.execute("SELECT 1");
    return Response.json({ status: "ok", timestamp: Date.now() });
  } catch (err) {
    return Response.json({ status: "error", error: String(err) }, { status: 503 });
  }
}
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:3000/api/health || exit 1

Cross-Platform Builds (M1/M2 Mac to Linux)

# Build for multiple architectures
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myapp:latest \
  --push .

# Set up buildx once
docker buildx create --use --name multi-arch
docker buildx inspect --bootstrap

Image Size Targets

| Stack | Target Size | |-------|-------------| | Next.js standalone | < 200MB | | Node.js API | < 150MB | | Python FastAPI | < 300MB | | Static site (nginx) | < 50MB |

Use docker image inspect <image> and docker history <image> to diagnose bloated layers.