Docker — Production Guide
Complete Production Implementation Guide
Version: 26.x | Category: Containers | Level: All
Docker is the foundational container technology every DevOps engineer must master. This guide covers production Dockerfiles, security hardening, multi-stage builds, Compose for local dev, and everything in between.
Table of Contents
- Production Dockerfile Patterns
- Multi-Stage Builds
- Security Hardening
- Docker Compose — Dev & Staging
- Registry & Image Management
- Networking
- Storage & Volumes
- BuildKit & Performance
- Production Gotchas
- Essential Commands
1. Production Dockerfile Patterns {#dockerfile}
Go Application (Smallest Possible Image)
dockerfile1# syntax=docker/dockerfile:1.62FROM golang:1.22-alpine AS builder3WORKDIR /app45# Copy dependency files first (better layer caching)6COPY go.mod go.sum ./7RUN --mount=type=cache,target=/go/pkg/mod \8 go mod download910COPY . .11RUN --mount=type=cache,target=/go/pkg/mod \12 --mount=type=cache,target=/root/.cache/go-build \13 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \14 go build \15 -ldflags="-w -s -X main.version=$(git describe --tags)" \16 -o /api ./cmd/api1718# Final stage — distroless (no shell, no package manager)19FROM gcr.io/distroless/static-debian12:nonroot20COPY --from=builder /api /api21EXPOSE 808022USER nonroot:nonroot23ENTRYPOINT ["/api"]
Node.js Application
dockerfile1# syntax=docker/dockerfile:1.62FROM node:20-alpine AS deps3WORKDIR /app4COPY package*.json ./5RUN --mount=type=cache,target=/root/.npm \6 npm ci --only=production78FROM node:20-alpine AS builder9WORKDIR /app10COPY package*.json ./11RUN --mount=type=cache,target=/root/.npm \12 npm ci13COPY . .14RUN npm run build1516# Production stage17FROM node:20-alpine AS runner18WORKDIR /app19ENV NODE_ENV=production2021# Create non-root user22RUN addgroup --system --gid 1001 nodejs && \23 adduser --system --uid 1001 nextjs2425COPY --from=deps /app/node_modules ./node_modules26COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next27COPY --from=builder /app/public ./public28COPY --from=builder /app/package.json ./2930USER nextjs31EXPOSE 300032ENV PORT=300033CMD ["node_modules/.bin/next", "start"]
Python Application
dockerfile1# syntax=docker/dockerfile:1.62FROM python:3.12-slim AS builder3WORKDIR /app45# Install build deps6RUN apt-get update && apt-get install -y --no-install-recommends \7 gcc libpq-dev \8 && rm -rf /var/lib/apt/lists/*910COPY requirements.txt .11RUN --mount=type=cache,target=/root/.cache/pip \12 pip install --user --no-warn-script-location -r requirements.txt1314FROM python:3.12-slim AS runner15WORKDIR /app1617# Runtime deps only18RUN apt-get update && apt-get install -y --no-install-recommends \19 libpq5 \20 && rm -rf /var/lib/apt/lists/* \21 && addgroup --system app \22 && adduser --system --ingroup app app2324COPY --from=builder /root/.local /home/app/.local25COPY --chown=app:app . .2627USER app28ENV PATH=/home/app/.local/bin:$PATH29CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]
Java / JVM Application
dockerfile1FROM eclipse-temurin:21-jdk-alpine AS builder2WORKDIR /app3COPY . .4RUN ./gradlew bootJar --no-daemon56# Use JRE not JDK in production7FROM eclipse-temurin:21-jre-alpine AS runner8WORKDIR /app9RUN addgroup -S appgroup && adduser -S appuser -G appgroup1011COPY --from=builder /app/build/libs/*.jar app.jar12USER appuser1314# Container-aware JVM flags (critical!)15ENV JAVA_OPTS="\16 -XX:+UseContainerSupport \17 -XX:MaxRAMPercentage=75.0 \18 -XX:+ExitOnOutOfMemoryError \19 -XX:+UseG1GC \20 -Djava.security.egd=file:/dev/./urandom"2122EXPOSE 808023ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
2. Multi-Stage Builds {#multistage}
Multi-stage builds are mandatory for production. They reduce image size by 80-90% and eliminate build tools from your runtime image.
dockerfile1# Builder stage has: compiler, build tools, test dependencies2# Runner stage has: only the compiled artifact34# Example: Node.js with test stage5FROM node:20-alpine AS base6WORKDIR /app7COPY package*.json ./89FROM base AS test10RUN npm ci11COPY . .12RUN npm test # Tests run at build time — fail fast1314FROM base AS deps15RUN npm ci --only=production1617FROM node:20-alpine AS runner18WORKDIR /app19COPY --from=deps /app/node_modules ./node_modules20COPY --from=test /app/dist ./dist # Only copy tested build output21USER node22CMD ["node", "dist/index.js"]
Build specific stage
bash1# Build only up to 'test' stage (useful in CI)2docker build --target test -t myapp:test .34# Build final production image5docker build --target runner -t myapp:prod .
3. Security Hardening {#security}
.dockerignore — Always Required
# Version control
.git
.gitignore
# Dependencies (re-installed in image)
node_modules
vendor
# Build artifacts
dist
build
.next
out
*.pyc
__pycache__
*.class
target
# Secrets — NEVER in images
.env
.env.*
*.pem
*.key
*.crt
secrets/
credentials/
# Dev tools
.vscode
.idea
Makefile
docker-compose*.yml
# Docs
*.md
docs/
Scanning with Trivy
bash1# Scan image before pushing2trivy image --exit-code 1 --severity HIGH,CRITICAL myimage:latest34# Scan with SBOM generation5trivy image --format cyclonedx --output sbom.json myimage:latest67# Scan Dockerfile for misconfigurations8trivy config ./Dockerfile910# In CI pipeline (GitHub Actions)11- name: Scan image12 uses: aquasecurity/trivy-action@master13 with:14 image-ref: myimage:${{ github.sha }}15 format: 'sarif'16 output: 'trivy-results.sarif'17 exit-code: '1'18 severity: 'CRITICAL,HIGH'
Sign Images with Cosign
bash1# Install cosign2brew install cosign # or download binary34# Generate key pair (store private key in Vault/Secrets Manager)5cosign generate-key-pair67# Sign after push8cosign sign --key cosign.key myregistry/myimage:tag910# Verify before deploy11cosign verify --key cosign.pub myregistry/myimage:tag1213# Keyless signing (uses OIDC — best for CI/CD)14cosign sign myregistry/myimage:tag # In GitHub Actions with id-token: write
4. Docker Compose — Dev & Staging {#compose}
yaml1# docker-compose.yml — Local development2services:3 api:4 build:5 context: .6 dockerfile: Dockerfile7 target: builder # Use builder stage for dev (has hot reload)8 cache_from:9 - type=local,src=/tmp/.buildx-cache10 ports:11 - "8080:8080"12 environment:13 - NODE_ENV=development14 - DATABASE_URL=postgres://postgres:postgres@db:5432/devdb15 - REDIS_URL=redis://redis:637916 volumes:17 - .:/app # Hot reload18 - /app/node_modules # Don't override container's node_modules19 depends_on:20 db:21 condition: service_healthy22 redis:23 condition: service_healthy24 restart: unless-stopped2526 db:27 image: postgres:16-alpine28 environment:29 POSTGRES_USER: postgres30 POSTGRES_PASSWORD: postgres31 POSTGRES_DB: devdb32 volumes:33 - postgres_data:/var/lib/postgresql/data34 - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql35 ports:36 - "5432:5432"37 healthcheck:38 test: ["CMD-SHELL", "pg_isready -U postgres"]39 interval: 5s40 timeout: 5s41 retries: 54243 redis:44 image: redis:7-alpine45 command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru46 volumes:47 - redis_data:/data48 ports:49 - "6379:6379"50 healthcheck:51 test: ["CMD", "redis-cli", "ping"]52 interval: 5s53 timeout: 3s54 retries: 55556 # Local observability57 prometheus:58 image: prom/prometheus:latest59 volumes:60 - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml61 ports:62 - "9090:9090"6364 grafana:65 image: grafana/grafana:latest66 environment:67 - GF_SECURITY_ADMIN_PASSWORD=admin68 ports:69 - "3001:3000"70 volumes:71 - grafana_data:/var/lib/grafana72 - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards7374volumes:75 postgres_data:76 redis_data:77 grafana_data:
bash1# Useful Compose commands2docker compose up -d # Start all services3docker compose up -d api # Start specific service4docker compose logs -f api # Follow logs5docker compose exec api /bin/sh # Shell into running container6docker compose restart api # Restart single service7docker compose down -v # Stop + remove volumes (clean slate)8docker compose ps # Service status9docker compose top # Process tree
5. Registry & Image Management {#registry}
Tagging Strategy
bash1# NEVER use :latest in production2# Use semantic versioning + git SHA34# Tag with version + git SHA5IMAGE_TAG="${VERSION}-$(git rev-parse --short HEAD)"6docker build -t myregistry/myapp:${IMAGE_TAG} .7docker build -t myregistry/myapp:${VERSION} .89# Push both tags10docker push myregistry/myapp:${IMAGE_TAG}11docker push myregistry/myapp:${VERSION}1213# In CI, also tag with branch name (not for production deploy)14docker build -t myregistry/myapp:branch-${BRANCH_NAME//\//-} .
Private Registry Authentication
bash1# AWS ECR2aws ecr get-login-password --region us-east-1 \3 | docker login --username AWS --password-stdin \4 123456789.dkr.ecr.us-east-1.amazonaws.com56# GCP Artifact Registry7gcloud auth configure-docker us-central1-docker.pkg.dev89# GitHub Container Registry10echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_USER --password-stdin
Image Pruning (run on nodes)
bash1# Remove dangling images2docker image prune -f34# Remove all unused images (be careful in dev)5docker image prune -a -f --filter "until=24h"67# Remove stopped containers8docker container prune -f910# Remove unused volumes11docker volume prune -f1213# Nuclear option — clean everything (dev only)14docker system prune -a -f --volumes
6. Networking {#networking}
bash1# List networks2docker network ls34# Create custom bridge network5docker network create --driver bridge \6 --subnet=172.20.0.0/16 \7 --gateway=172.20.0.1 \8 myapp-network910# Connect running container to network11docker network connect myapp-network mycontainer1213# Inspect network (see IPs of connected containers)14docker network inspect myapp-network1516# DNS resolution: containers on same network can use service names17# api container can reach db at http://db:5432
7. Storage & Volumes {#storage}
bash1# Named volume (managed by Docker, survives container deletion)2docker volume create myapp-data3docker run -v myapp-data:/app/data myimage45# Bind mount (host path → container — use for dev, not production)6docker run -v $(pwd)/data:/app/data myimage78# tmpfs mount (memory only — for sensitive data that shouldn't hit disk)9docker run --tmpfs /tmp:rw,noexec,nosuid,size=100m myimage1011# Backup a volume12docker run --rm \13 -v myapp-data:/data \14 -v $(pwd):/backup \15 alpine tar czf /backup/backup-$(date +%Y%m%d).tar.gz /data
8. BuildKit & Performance {#buildkit}
bash1# Enable BuildKit (default in Docker 23+, but always set explicitly)2export DOCKER_BUILDKIT=134# Or in daemon.json5# { "features": { "buildkit": true } }67# Build with cache mount (speeds up dependency installation massively)8# npm: caches ~/.npm9RUN --mount=type=cache,target=/root/.npm npm ci1011# pip: caches pip cache dir12RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt1314# Go: caches module and build cache15RUN --mount=type=cache,target=/go/pkg/mod \16 --mount=type=cache,target=/root/.cache/go-build \17 go build ./...1819# Build with secrets (never baked into image layers)20RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci2122docker build --secret id=npmrc,src=$HOME/.npmrc .2324# Parallel builds (BuildKit does this automatically for multi-stage)25# But you can also build multiple images simultaneously26docker buildx bake -f docker-bake.hcl --push
9. Production Gotchas {#gotchas}
| Gotcha | Why It Matters | Fix |
|---|---|---|
| Using :latest tag | Non-deterministic — latest changes under you | Always pin to specific version or SHA |
| Running as root | Container escape = root on host | USER nonroot in Dockerfile |
| Secrets in ENV vars | Visible in docker inspect, process list | Use secrets files or external secrets manager |
| No .dockerignore | Sends entire project to build daemon, includes secrets | Always add .dockerignore |
| Not setting JAVA_OPTS container flags | JVM ignores cgroup memory limits → OOMKill | -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 |
| ADD instead of COPY | ADD auto-extracts tarballs, fetches URLs — unexpected | Use COPY unless you specifically need ADD's features |
| One giant RUN layer | No build cache benefit | Chain related commands, but split independent steps |
| Installing dev deps in production | Larger image, more attack surface | npm ci --only=production, multi-stage builds |
| Not pinning base image digest | FROM node:20-alpine can change | Pin: FROM node:20-alpine@sha256:abc123... |
| Ignoring health checks | Container shows Running but app is dead | Always add HEALTHCHECK instruction |
10. Essential Commands {#commands}
bash1# ── Build ─────────────────────────────────────2docker build -t myimage:tag .3docker build --no-cache -t myimage:tag . # Force fresh build4docker build --target builder -t myimage:dev . # Specific stage56# ── Run ───────────────────────────────────────7docker run -d --name myapp -p 8080:8080 myimage8docker run --rm -it myimage /bin/sh # Ephemeral interactive9docker run --memory=512m --cpus=0.5 myimage # Resource limits1011# ── Inspect & Debug ───────────────────────────12docker ps -a # All containers13docker logs myapp -f --tail=10014docker exec -it myapp /bin/sh15docker inspect myapp | jq '.[0].HostConfig' # Config details16docker stats --no-stream # Resource usage snapshot17docker diff myapp # Changed files vs image1819# ── Images ────────────────────────────────────20docker images -a21docker image history myimage --no-trunc # Layer sizes22docker image inspect myimage | jq '.[0].Config' # Image config23docker rmi myimage:tag2425# ── System ────────────────────────────────────26docker system df # Disk usage27docker system events # Real-time events28docker info # Docker daemon info2930# ── Multi-arch builds ─────────────────────────31docker buildx create --use32docker buildx build \33 --platform linux/amd64,linux/arm64 \34 -t myregistry/myimage:tag \35 --push .