{"slug":"docker-mastery-reference","title":"Docker Mastery: Multi-Stage Builds, Security, and Compose Orchestration","tags":["docker","containers","devops","security","compose","buildkit"],"agent_summary":"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.","trigger_phrases":["Docker","Dockerfile","docker-compose","Docker multi-stage","container security","Docker production","Docker image size","Docker health check"],"runnable":false,"markdown":"\n## Overview\n\nDocker 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.\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 (minimal production image)\nFROM node:20-alpine AS runner\nWORKDIR /app\n\nENV NODE_ENV=production\nENV NEXT_TELEMETRY_DISABLED=1\n\n# Non-root user\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\n# Copy only what's needed\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\n\nCMD [\"node\", \"server.js\"]\n```\n\n## Security Hardening\n\n```dockerfile\n# Non-root user (always)\nRUN addgroup -S appgroup && adduser -S appuser -G appgroup\nUSER appuser\n\n# Read-only filesystem (where possible)\n# In docker-compose:\n# read_only: true\n# tmpfs:\n#   - /tmp\n#   - /var/run\n\n# No new privileges\n# docker run --security-opt no-new-privileges:true\n\n# Specific capabilities only\n# docker run --cap-drop ALL --cap-add NET_BIND_SERVICE\n```\n\n## Secrets Management\n\nNever put secrets in ENV or ARG build instructions — they appear in image history:\n\n```bash\n# BAD — secret in image history forever\nENV API_KEY=sk-abc123\n\n# GOOD — runtime env var (not in image)\n# Pass at runtime: docker run -e API_KEY=... my-image\n\n# GOOD — Docker secrets (Swarm/Compose v3.1+)\n```\n\n```yaml\n# docker-compose.yml with secrets\nservices:\n  app:\n    secrets:\n      - db_password\n    environment:\n      DB_PASSWORD_FILE: /run/secrets/db_password\n\nsecrets:\n  db_password:\n    file: ./secrets/db_password.txt\n```\n\n## Docker Compose: Production\n\n```yaml\n# docker-compose.prod.yml\nversion: \"3.8\"\n\nservices:\n  app:\n    build:\n      context: .\n      dockerfile: Dockerfile\n      target: runner\n    restart: unless-stopped\n    ports:\n      - \"3000:3000\"\n    environment:\n      NODE_ENV: production\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\"\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  db:\n    image: postgres:16-alpine\n    restart: unless-stopped\n    environment:\n      POSTGRES_DB: myapp\n      POSTGRES_USER: ${DB_USER}\n      POSTGRES_PASSWORD: ${DB_PASSWORD}\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U ${DB_USER}\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\nvolumes:\n  postgres_data:\n```\n\n## Docker Compose: Development Override\n\n```yaml\n# docker-compose.override.yml (auto-merged with docker-compose.yml)\nservices:\n  app:\n    build:\n      target: builder\n    volumes:\n      - .:/app\n      - /app/node_modules\n    command: npm run dev\n    environment:\n      NODE_ENV: development\n```\n\n```bash\n# Dev: uses docker-compose.yml + docker-compose.override.yml\ndocker compose up\n\n# Prod: uses only docker-compose.prod.yml\ndocker compose -f docker-compose.prod.yml up -d\n```\n\n## .dockerignore (Essential)\n\n```\nnode_modules\n.next\n.git\n.env*\n*.log\ncoverage\n.DS_Store\nREADME.md\ndocker-compose*.yml\nDockerfile*\n.github\ntests\n```\n\nMissing `.dockerignore` is the #1 cause of bloated images (node_modules copying into context).\n\n## Image Size Optimization\n\n```dockerfile\n# Use alpine base\nFROM node:20-alpine\n\n# Combine RUN commands (fewer layers)\nRUN apk add --no-cache curl && \\\n    rm -rf /var/cache/apk/*\n\n# Only install production deps in final stage\nRUN npm ci --omit=dev && npm cache clean --force\n\n# Use .dockerignore\n# Remove dev files\nRUN find . -name \"*.test.*\" -delete\n```\n\n## BuildKit (Enable for All Builds)\n\n```bash\n# Enable BuildKit\nDOCKER_BUILDKIT=1 docker build .\n\n# Or set in Docker config\necho '{\"features\": {\"buildkit\": true}}' > /etc/docker/daemon.json\n\n# Mount cache in builds (speeds up npm install across builds)\nRUN --mount=type=cache,target=/root/.npm npm ci\n```\n\n## Health Check Patterns\n\n```dockerfile\n# HTTP health check\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\\n  CMD curl -f http://localhost:3000/api/health || exit 1\n\n# Process check (for services without HTTP)\nHEALTHCHECK CMD pgrep -x node || exit 1\n```\n\n## Common Commands\n\n```bash\n# Build\ndocker build -t my-app:latest .\ndocker build -t my-app:v1.2.3 --build-arg VERSION=1.2.3 .\n\n# Run\ndocker run -d --name my-app -p 3000:3000 --env-file .env my-app:latest\n\n# Inspect\ndocker logs my-app --follow\ndocker stats my-app\ndocker exec -it my-app sh\n\n# Cleanup\ndocker system prune -af         # remove all unused images/containers/networks\ndocker image prune -f           # remove dangling images only\n```\n\n## Cross-Platform Builds (M1/ARM)\n\n```bash\n# Build for linux/amd64 from Apple Silicon\ndocker buildx build --platform linux/amd64 -t my-app:latest --push .\n\n# Multi-platform\ndocker buildx build \\\n  --platform linux/amd64,linux/arm64 \\\n  -t my-app:latest \\\n  --push .\n```\n","html":"<h2>Overview</h2>\n<p>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.</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 (minimal production image)\nFROM node:20-alpine AS runner\nWORKDIR /app\n\nENV NODE_ENV=production\nENV NEXT_TELEMETRY_DISABLED=1\n\n# Non-root user\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\n# Copy only what's needed\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\n\nCMD [\"node\", \"server.js\"]\n</code></pre>\n<h2>Security Hardening</h2>\n<pre><code class=\"language-dockerfile\"># Non-root user (always)\nRUN addgroup -S appgroup &#x26;&#x26; adduser -S appuser -G appgroup\nUSER appuser\n\n# Read-only filesystem (where possible)\n# In docker-compose:\n# read_only: true\n# tmpfs:\n#   - /tmp\n#   - /var/run\n\n# No new privileges\n# docker run --security-opt no-new-privileges:true\n\n# Specific capabilities only\n# docker run --cap-drop ALL --cap-add NET_BIND_SERVICE\n</code></pre>\n<h2>Secrets Management</h2>\n<p>Never put secrets in ENV or ARG build instructions — they appear in image history:</p>\n<pre><code class=\"language-bash\"># BAD — secret in image history forever\nENV API_KEY=sk-abc123\n\n# GOOD — runtime env var (not in image)\n# Pass at runtime: docker run -e API_KEY=... my-image\n\n# GOOD — Docker secrets (Swarm/Compose v3.1+)\n</code></pre>\n<pre><code class=\"language-yaml\"># docker-compose.yml with secrets\nservices:\n  app:\n    secrets:\n      - db_password\n    environment:\n      DB_PASSWORD_FILE: /run/secrets/db_password\n\nsecrets:\n  db_password:\n    file: ./secrets/db_password.txt\n</code></pre>\n<h2>Docker Compose: Production</h2>\n<pre><code class=\"language-yaml\"># docker-compose.prod.yml\nversion: \"3.8\"\n\nservices:\n  app:\n    build:\n      context: .\n      dockerfile: Dockerfile\n      target: runner\n    restart: unless-stopped\n    ports:\n      - \"3000:3000\"\n    environment:\n      NODE_ENV: production\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\"\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  db:\n    image: postgres:16-alpine\n    restart: unless-stopped\n    environment:\n      POSTGRES_DB: myapp\n      POSTGRES_USER: ${DB_USER}\n      POSTGRES_PASSWORD: ${DB_PASSWORD}\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U ${DB_USER}\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\nvolumes:\n  postgres_data:\n</code></pre>\n<h2>Docker Compose: Development Override</h2>\n<pre><code class=\"language-yaml\"># docker-compose.override.yml (auto-merged with docker-compose.yml)\nservices:\n  app:\n    build:\n      target: builder\n    volumes:\n      - .:/app\n      - /app/node_modules\n    command: npm run dev\n    environment:\n      NODE_ENV: development\n</code></pre>\n<pre><code class=\"language-bash\"># Dev: uses docker-compose.yml + docker-compose.override.yml\ndocker compose up\n\n# Prod: uses only docker-compose.prod.yml\ndocker compose -f docker-compose.prod.yml up -d\n</code></pre>\n<h2>.dockerignore (Essential)</h2>\n<pre><code>node_modules\n.next\n.git\n.env*\n*.log\ncoverage\n.DS_Store\nREADME.md\ndocker-compose*.yml\nDockerfile*\n.github\ntests\n</code></pre>\n<p>Missing <code>.dockerignore</code> is the #1 cause of bloated images (node_modules copying into context).</p>\n<h2>Image Size Optimization</h2>\n<pre><code class=\"language-dockerfile\"># Use alpine base\nFROM node:20-alpine\n\n# Combine RUN commands (fewer layers)\nRUN apk add --no-cache curl &#x26;&#x26; \\\n    rm -rf /var/cache/apk/*\n\n# Only install production deps in final stage\nRUN npm ci --omit=dev &#x26;&#x26; npm cache clean --force\n\n# Use .dockerignore\n# Remove dev files\nRUN find . -name \"*.test.*\" -delete\n</code></pre>\n<h2>BuildKit (Enable for All Builds)</h2>\n<pre><code class=\"language-bash\"># Enable BuildKit\nDOCKER_BUILDKIT=1 docker build .\n\n# Or set in Docker config\necho '{\"features\": {\"buildkit\": true}}' > /etc/docker/daemon.json\n\n# Mount cache in builds (speeds up npm install across builds)\nRUN --mount=type=cache,target=/root/.npm npm ci\n</code></pre>\n<h2>Health Check Patterns</h2>\n<pre><code class=\"language-dockerfile\"># HTTP health check\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\\n  CMD curl -f http://localhost:3000/api/health || exit 1\n\n# Process check (for services without HTTP)\nHEALTHCHECK CMD pgrep -x node || exit 1\n</code></pre>\n<h2>Common Commands</h2>\n<pre><code class=\"language-bash\"># Build\ndocker build -t my-app:latest .\ndocker build -t my-app:v1.2.3 --build-arg VERSION=1.2.3 .\n\n# Run\ndocker run -d --name my-app -p 3000:3000 --env-file .env my-app:latest\n\n# Inspect\ndocker logs my-app --follow\ndocker stats my-app\ndocker exec -it my-app sh\n\n# Cleanup\ndocker system prune -af         # remove all unused images/containers/networks\ndocker image prune -f           # remove dangling images only\n</code></pre>\n<h2>Cross-Platform Builds (M1/ARM)</h2>\n<pre><code class=\"language-bash\"># Build for linux/amd64 from Apple Silicon\ndocker buildx build --platform linux/amd64 -t my-app:latest --push .\n\n# Multi-platform\ndocker buildx build \\\n  --platform linux/amd64,linux/arm64 \\\n  -t my-app:latest \\\n  --push .\n</code></pre>\n"}