{"slug":"docker-multi-stage-builds","title":"Docker Multi-Stage Builds: Production Patterns and Security Hardening","tags":["docker","containers","devops","deployment","security"],"agent_summary":"Docker multi-stage build patterns for Next.js and Node.js, security hardening, Docker Compose production setup, image size optimization, and health checks.","trigger_phrases":["Docker multi-stage build","Docker production","Dockerfile Next.js","Docker security","Docker Compose production","container hardening"],"runnable":false,"markdown":"\n## Overview\n\nMulti-stage Docker builds for production Node.js/Next.js applications. Covers image size optimization, security hardening, and Docker Compose patterns.\n\n## Multi-Stage Build (Gold Standard)\n\n```dockerfile\n# Stage 1: Dependencies\nFROM node:20-alpine AS deps\nRUN apk add --no-cache libc6-compat\nWORKDIR /app\nCOPY package.json package-lock.json ./\nRUN npm ci --omit=dev\n\n# Stage 2: Builder\nFROM node:20-alpine AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nENV NEXT_TELEMETRY_DISABLED=1\nRUN npm run build\n\n# Stage 3: Runner (smallest possible image)\nFROM node:20-alpine AS runner\nWORKDIR /app\nENV NODE_ENV=production\nENV NEXT_TELEMETRY_DISABLED=1\n\n# Non-root user — security requirement\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\nCOPY --from=builder /app/public ./public\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static\n\nUSER nextjs\nEXPOSE 3000\nENV PORT=3000\nENV HOSTNAME=\"0.0.0.0\"\n\nCMD [\"node\", \"server.js\"]\n```\n\nRequires `output: \"standalone\"` in `next.config.ts`. The runner image contains only the production output — no source files, no dev dependencies.\n\n## Security Hardening Rules\n\n```dockerfile\n# Always use specific image versions — never :latest\nFROM node:20.18.1-alpine AS base\n\n# Run as non-root\nRUN addgroup -S appgroup && adduser -S appuser -G appgroup\nUSER appuser\n\n# Read-only filesystem where possible\n# Set in docker run: --read-only --tmpfs /tmp\n\n# No shell in production image\nRUN rm -f /bin/sh  # Only if app doesn't need shell\n\n# Minimal apt/apk — only what the app needs\nRUN apk add --no-cache curl  # curl for healthcheck only\n```\n\n## .dockerignore (Essential)\n\n```\nnode_modules\n.next\n.git\n*.log\n*.md\n.env*\n.DS_Store\ncoverage\n.nyc_output\ndist\n```\n\nA missing `.dockerignore` sends gigabytes of node_modules into build context. Always create it first.\n\n## Docker Compose — Production Pattern\n\n```yaml\nservices:\n  app:\n    image: my-app:${APP_VERSION:-latest}\n    ports:\n      - \"3000:3000\"\n    environment:\n      - NODE_ENV=production\n      - DATABASE_URL=${DATABASE_URL}\n    env_file:\n      - .env.production\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3000/api/health\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 40s\n    deploy:\n      resources:\n        limits:\n          cpus: \"1.0\"\n          memory: 512M\n        reservations:\n          memory: 256M\n    logging:\n      driver: \"json-file\"\n      options:\n        max-size: \"10m\"\n        max-file: \"3\"\n\n  nginx:\n    image: nginx:1.27-alpine\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - ./nginx.conf:/etc/nginx/nginx.conf:ro\n      - ./certs:/etc/nginx/certs:ro\n    depends_on:\n      app:\n        condition: service_healthy\n```\n\n## Development Override\n\n```yaml\n# docker-compose.override.yml (auto-loaded in dev)\nservices:\n  app:\n    build:\n      context: .\n      target: builder\n    volumes:\n      - .:/app\n      - /app/node_modules      # Prevent host node_modules overriding container\n    environment:\n      - NODE_ENV=development\n    command: npm run dev\n    ports:\n      - \"3000:3000\"\n      - \"9229:9229\"            # Node debugger\n```\n\n## Health Check Patterns\n\n```typescript\n// app/api/health/route.ts\nexport async function GET() {\n  try {\n    // Lightweight dependency check\n    await db.execute(\"SELECT 1\");\n    return Response.json({ status: \"ok\", timestamp: Date.now() });\n  } catch (err) {\n    return Response.json({ status: \"error\", error: String(err) }, { status: 503 });\n  }\n}\n```\n\n```dockerfile\nHEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \\\n  CMD curl -f http://localhost:3000/api/health || exit 1\n```\n\n## Cross-Platform Builds (M1/M2 Mac to Linux)\n\n```bash\n# Build for multiple architectures\ndocker buildx build \\\n  --platform linux/amd64,linux/arm64 \\\n  -t myapp:latest \\\n  --push .\n\n# Set up buildx once\ndocker buildx create --use --name multi-arch\ndocker buildx inspect --bootstrap\n```\n\n## Image Size Targets\n\n| Stack | Target Size |\n|-------|-------------|\n| Next.js standalone | < 200MB |\n| Node.js API | < 150MB |\n| Python FastAPI | < 300MB |\n| Static site (nginx) | < 50MB |\n\nUse `docker image inspect <image>` and `docker history <image>` to diagnose bloated layers.\n","html":"<h2>Overview</h2>\n<p>Multi-stage Docker builds for production Node.js/Next.js applications. Covers image size optimization, security hardening, and Docker Compose patterns.</p>\n<h2>Multi-Stage Build (Gold Standard)</h2>\n<pre><code class=\"language-dockerfile\"># Stage 1: Dependencies\nFROM node:20-alpine AS deps\nRUN apk add --no-cache libc6-compat\nWORKDIR /app\nCOPY package.json package-lock.json ./\nRUN npm ci --omit=dev\n\n# Stage 2: Builder\nFROM node:20-alpine AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nENV NEXT_TELEMETRY_DISABLED=1\nRUN npm run build\n\n# Stage 3: Runner (smallest possible image)\nFROM node:20-alpine AS runner\nWORKDIR /app\nENV NODE_ENV=production\nENV NEXT_TELEMETRY_DISABLED=1\n\n# Non-root user — security requirement\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\nCOPY --from=builder /app/public ./public\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static\n\nUSER nextjs\nEXPOSE 3000\nENV PORT=3000\nENV HOSTNAME=\"0.0.0.0\"\n\nCMD [\"node\", \"server.js\"]\n</code></pre>\n<p>Requires <code>output: \"standalone\"</code> in <code>next.config.ts</code>. The runner image contains only the production output — no source files, no dev dependencies.</p>\n<h2>Security Hardening Rules</h2>\n<pre><code class=\"language-dockerfile\"># Always use specific image versions — never :latest\nFROM node:20.18.1-alpine AS base\n\n# Run as non-root\nRUN addgroup -S appgroup &#x26;&#x26; adduser -S appuser -G appgroup\nUSER appuser\n\n# Read-only filesystem where possible\n# Set in docker run: --read-only --tmpfs /tmp\n\n# No shell in production image\nRUN rm -f /bin/sh  # Only if app doesn't need shell\n\n# Minimal apt/apk — only what the app needs\nRUN apk add --no-cache curl  # curl for healthcheck only\n</code></pre>\n<h2>.dockerignore (Essential)</h2>\n<pre><code>node_modules\n.next\n.git\n*.log\n*.md\n.env*\n.DS_Store\ncoverage\n.nyc_output\ndist\n</code></pre>\n<p>A missing <code>.dockerignore</code> sends gigabytes of node_modules into build context. Always create it first.</p>\n<h2>Docker Compose — Production Pattern</h2>\n<pre><code class=\"language-yaml\">services:\n  app:\n    image: my-app:${APP_VERSION:-latest}\n    ports:\n      - \"3000:3000\"\n    environment:\n      - NODE_ENV=production\n      - DATABASE_URL=${DATABASE_URL}\n    env_file:\n      - .env.production\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3000/api/health\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 40s\n    deploy:\n      resources:\n        limits:\n          cpus: \"1.0\"\n          memory: 512M\n        reservations:\n          memory: 256M\n    logging:\n      driver: \"json-file\"\n      options:\n        max-size: \"10m\"\n        max-file: \"3\"\n\n  nginx:\n    image: nginx:1.27-alpine\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - ./nginx.conf:/etc/nginx/nginx.conf:ro\n      - ./certs:/etc/nginx/certs:ro\n    depends_on:\n      app:\n        condition: service_healthy\n</code></pre>\n<h2>Development Override</h2>\n<pre><code class=\"language-yaml\"># docker-compose.override.yml (auto-loaded in dev)\nservices:\n  app:\n    build:\n      context: .\n      target: builder\n    volumes:\n      - .:/app\n      - /app/node_modules      # Prevent host node_modules overriding container\n    environment:\n      - NODE_ENV=development\n    command: npm run dev\n    ports:\n      - \"3000:3000\"\n      - \"9229:9229\"            # Node debugger\n</code></pre>\n<h2>Health Check Patterns</h2>\n<pre><code class=\"language-typescript\">// app/api/health/route.ts\nexport async function GET() {\n  try {\n    // Lightweight dependency check\n    await db.execute(\"SELECT 1\");\n    return Response.json({ status: \"ok\", timestamp: Date.now() });\n  } catch (err) {\n    return Response.json({ status: \"error\", error: String(err) }, { status: 503 });\n  }\n}\n</code></pre>\n<pre><code class=\"language-dockerfile\">HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \\\n  CMD curl -f http://localhost:3000/api/health || exit 1\n</code></pre>\n<h2>Cross-Platform Builds (M1/M2 Mac to Linux)</h2>\n<pre><code class=\"language-bash\"># Build for multiple architectures\ndocker buildx build \\\n  --platform linux/amd64,linux/arm64 \\\n  -t myapp:latest \\\n  --push .\n\n# Set up buildx once\ndocker buildx create --use --name multi-arch\ndocker buildx inspect --bootstrap\n</code></pre>\n<h2>Image Size Targets</h2>\n<p>| Stack | Target Size |\n|-------|-------------|\n| Next.js standalone | &#x3C; 200MB |\n| Node.js API | &#x3C; 150MB |\n| Python FastAPI | &#x3C; 300MB |\n| Static site (nginx) | &#x3C; 50MB |</p>\n<p>Use <code>docker image inspect &#x3C;image></code> and <code>docker history &#x3C;image></code> to diagnose bloated layers.</p>\n"}