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

  1. Production Dockerfile Patterns
  2. Multi-Stage Builds
  3. Security Hardening
  4. Docker Compose — Dev & Staging
  5. Registry & Image Management
  6. Networking
  7. Storage & Volumes
  8. BuildKit & Performance
  9. Production Gotchas
  10. Essential Commands

1. Production Dockerfile Patterns {#dockerfile}

Go Application (Smallest Possible Image)

dockerfile
1# syntax=docker/dockerfile:1.6
2FROM golang:1.22-alpine AS builder
3WORKDIR /app
4
5# Copy dependency files first (better layer caching)
6COPY go.mod go.sum ./
7RUN --mount=type=cache,target=/go/pkg/mod \
8 go mod download
9
10COPY . .
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/api
17
18# Final stage — distroless (no shell, no package manager)
19FROM gcr.io/distroless/static-debian12:nonroot
20COPY --from=builder /api /api
21EXPOSE 8080
22USER nonroot:nonroot
23ENTRYPOINT ["/api"]

Node.js Application

dockerfile
1# syntax=docker/dockerfile:1.6
2FROM node:20-alpine AS deps
3WORKDIR /app
4COPY package*.json ./
5RUN --mount=type=cache,target=/root/.npm \
6 npm ci --only=production
7
8FROM node:20-alpine AS builder
9WORKDIR /app
10COPY package*.json ./
11RUN --mount=type=cache,target=/root/.npm \
12 npm ci
13COPY . .
14RUN npm run build
15
16# Production stage
17FROM node:20-alpine AS runner
18WORKDIR /app
19ENV NODE_ENV=production
20
21# Create non-root user
22RUN addgroup --system --gid 1001 nodejs && \
23 adduser --system --uid 1001 nextjs
24
25COPY --from=deps /app/node_modules ./node_modules
26COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
27COPY --from=builder /app/public ./public
28COPY --from=builder /app/package.json ./
29
30USER nextjs
31EXPOSE 3000
32ENV PORT=3000
33CMD ["node_modules/.bin/next", "start"]

Python Application

dockerfile
1# syntax=docker/dockerfile:1.6
2FROM python:3.12-slim AS builder
3WORKDIR /app
4
5# Install build deps
6RUN apt-get update && apt-get install -y --no-install-recommends \
7 gcc libpq-dev \
8 && rm -rf /var/lib/apt/lists/*
9
10COPY requirements.txt .
11RUN --mount=type=cache,target=/root/.cache/pip \
12 pip install --user --no-warn-script-location -r requirements.txt
13
14FROM python:3.12-slim AS runner
15WORKDIR /app
16
17# Runtime deps only
18RUN 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 app
23
24COPY --from=builder /root/.local /home/app/.local
25COPY --chown=app:app . .
26
27USER app
28ENV PATH=/home/app/.local/bin:$PATH
29CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]

Java / JVM Application

dockerfile
1FROM eclipse-temurin:21-jdk-alpine AS builder
2WORKDIR /app
3COPY . .
4RUN ./gradlew bootJar --no-daemon
5
6# Use JRE not JDK in production
7FROM eclipse-temurin:21-jre-alpine AS runner
8WORKDIR /app
9RUN addgroup -S appgroup && adduser -S appuser -G appgroup
10
11COPY --from=builder /app/build/libs/*.jar app.jar
12USER appuser
13
14# 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"
21
22EXPOSE 8080
23ENTRYPOINT ["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.

dockerfile
1# Builder stage has: compiler, build tools, test dependencies
2# Runner stage has: only the compiled artifact
3
4# Example: Node.js with test stage
5FROM node:20-alpine AS base
6WORKDIR /app
7COPY package*.json ./
8
9FROM base AS test
10RUN npm ci
11COPY . .
12RUN npm test # Tests run at build time — fail fast
13
14FROM base AS deps
15RUN npm ci --only=production
16
17FROM node:20-alpine AS runner
18WORKDIR /app
19COPY --from=deps /app/node_modules ./node_modules
20COPY --from=test /app/dist ./dist # Only copy tested build output
21USER node
22CMD ["node", "dist/index.js"]

Build specific stage

bash
1# Build only up to 'test' stage (useful in CI)
2docker build --target test -t myapp:test .
3
4# Build final production image
5docker 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

bash
1# Scan image before pushing
2trivy image --exit-code 1 --severity HIGH,CRITICAL myimage:latest
3
4# Scan with SBOM generation
5trivy image --format cyclonedx --output sbom.json myimage:latest
6
7# Scan Dockerfile for misconfigurations
8trivy config ./Dockerfile
9
10# In CI pipeline (GitHub Actions)
11- name: Scan image
12 uses: aquasecurity/trivy-action@master
13 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

bash
1# Install cosign
2brew install cosign # or download binary
3
4# Generate key pair (store private key in Vault/Secrets Manager)
5cosign generate-key-pair
6
7# Sign after push
8cosign sign --key cosign.key myregistry/myimage:tag
9
10# Verify before deploy
11cosign verify --key cosign.pub myregistry/myimage:tag
12
13# 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}

yaml
1# docker-compose.yml — Local development
2services:
3 api:
4 build:
5 context: .
6 dockerfile: Dockerfile
7 target: builder # Use builder stage for dev (has hot reload)
8 cache_from:
9 - type=local,src=/tmp/.buildx-cache
10 ports:
11 - "8080:8080"
12 environment:
13 - NODE_ENV=development
14 - DATABASE_URL=postgres://postgres:postgres@db:5432/devdb
15 - REDIS_URL=redis://redis:6379
16 volumes:
17 - .:/app # Hot reload
18 - /app/node_modules # Don't override container's node_modules
19 depends_on:
20 db:
21 condition: service_healthy
22 redis:
23 condition: service_healthy
24 restart: unless-stopped
25
26 db:
27 image: postgres:16-alpine
28 environment:
29 POSTGRES_USER: postgres
30 POSTGRES_PASSWORD: postgres
31 POSTGRES_DB: devdb
32 volumes:
33 - postgres_data:/var/lib/postgresql/data
34 - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
35 ports:
36 - "5432:5432"
37 healthcheck:
38 test: ["CMD-SHELL", "pg_isready -U postgres"]
39 interval: 5s
40 timeout: 5s
41 retries: 5
42
43 redis:
44 image: redis:7-alpine
45 command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
46 volumes:
47 - redis_data:/data
48 ports:
49 - "6379:6379"
50 healthcheck:
51 test: ["CMD", "redis-cli", "ping"]
52 interval: 5s
53 timeout: 3s
54 retries: 5
55
56 # Local observability
57 prometheus:
58 image: prom/prometheus:latest
59 volumes:
60 - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
61 ports:
62 - "9090:9090"
63
64 grafana:
65 image: grafana/grafana:latest
66 environment:
67 - GF_SECURITY_ADMIN_PASSWORD=admin
68 ports:
69 - "3001:3000"
70 volumes:
71 - grafana_data:/var/lib/grafana
72 - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards
73
74volumes:
75 postgres_data:
76 redis_data:
77 grafana_data:
bash
1# Useful Compose commands
2docker compose up -d # Start all services
3docker compose up -d api # Start specific service
4docker compose logs -f api # Follow logs
5docker compose exec api /bin/sh # Shell into running container
6docker compose restart api # Restart single service
7docker compose down -v # Stop + remove volumes (clean slate)
8docker compose ps # Service status
9docker compose top # Process tree

5. Registry & Image Management {#registry}

Tagging Strategy

bash
1# NEVER use :latest in production
2# Use semantic versioning + git SHA
3
4# Tag with version + git SHA
5IMAGE_TAG="${VERSION}-$(git rev-parse --short HEAD)"
6docker build -t myregistry/myapp:${IMAGE_TAG} .
7docker build -t myregistry/myapp:${VERSION} .
8
9# Push both tags
10docker push myregistry/myapp:${IMAGE_TAG}
11docker push myregistry/myapp:${VERSION}
12
13# In CI, also tag with branch name (not for production deploy)
14docker build -t myregistry/myapp:branch-${BRANCH_NAME//\//-} .

Private Registry Authentication

bash
1# AWS ECR
2aws ecr get-login-password --region us-east-1 \
3 | docker login --username AWS --password-stdin \
4 123456789.dkr.ecr.us-east-1.amazonaws.com
5
6# GCP Artifact Registry
7gcloud auth configure-docker us-central1-docker.pkg.dev
8
9# GitHub Container Registry
10echo $GITHUB_TOKEN | docker login ghcr.io -u $GITHUB_USER --password-stdin

Image Pruning (run on nodes)

bash
1# Remove dangling images
2docker image prune -f
3
4# Remove all unused images (be careful in dev)
5docker image prune -a -f --filter "until=24h"
6
7# Remove stopped containers
8docker container prune -f
9
10# Remove unused volumes
11docker volume prune -f
12
13# Nuclear option — clean everything (dev only)
14docker system prune -a -f --volumes

6. Networking {#networking}

bash
1# List networks
2docker network ls
3
4# Create custom bridge network
5docker network create --driver bridge \
6 --subnet=172.20.0.0/16 \
7 --gateway=172.20.0.1 \
8 myapp-network
9
10# Connect running container to network
11docker network connect myapp-network mycontainer
12
13# Inspect network (see IPs of connected containers)
14docker network inspect myapp-network
15
16# DNS resolution: containers on same network can use service names
17# api container can reach db at http://db:5432

7. Storage & Volumes {#storage}

bash
1# Named volume (managed by Docker, survives container deletion)
2docker volume create myapp-data
3docker run -v myapp-data:/app/data myimage
4
5# Bind mount (host path → container — use for dev, not production)
6docker run -v $(pwd)/data:/app/data myimage
7
8# tmpfs mount (memory only — for sensitive data that shouldn't hit disk)
9docker run --tmpfs /tmp:rw,noexec,nosuid,size=100m myimage
10
11# Backup a volume
12docker 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}

bash
1# Enable BuildKit (default in Docker 23+, but always set explicitly)
2export DOCKER_BUILDKIT=1
3
4# Or in daemon.json
5# { "features": { "buildkit": true } }
6
7# Build with cache mount (speeds up dependency installation massively)
8# npm: caches ~/.npm
9RUN --mount=type=cache,target=/root/.npm npm ci
10
11# pip: caches pip cache dir
12RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
13
14# Go: caches module and build cache
15RUN --mount=type=cache,target=/go/pkg/mod \
16 --mount=type=cache,target=/root/.cache/go-build \
17 go build ./...
18
19# Build with secrets (never baked into image layers)
20RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
21
22docker build --secret id=npmrc,src=$HOME/.npmrc .
23
24# Parallel builds (BuildKit does this automatically for multi-stage)
25# But you can also build multiple images simultaneously
26docker 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}

bash
1# ── Build ─────────────────────────────────────
2docker build -t myimage:tag .
3docker build --no-cache -t myimage:tag . # Force fresh build
4docker build --target builder -t myimage:dev . # Specific stage
5
6# ── Run ───────────────────────────────────────
7docker run -d --name myapp -p 8080:8080 myimage
8docker run --rm -it myimage /bin/sh # Ephemeral interactive
9docker run --memory=512m --cpus=0.5 myimage # Resource limits
10
11# ── Inspect & Debug ───────────────────────────
12docker ps -a # All containers
13docker logs myapp -f --tail=100
14docker exec -it myapp /bin/sh
15docker inspect myapp | jq '.[0].HostConfig' # Config details
16docker stats --no-stream # Resource usage snapshot
17docker diff myapp # Changed files vs image
18
19# ── Images ────────────────────────────────────
20docker images -a
21docker image history myimage --no-trunc # Layer sizes
22docker image inspect myimage | jq '.[0].Config' # Image config
23docker rmi myimage:tag
24
25# ── System ────────────────────────────────────
26docker system df # Disk usage
27docker system events # Real-time events
28docker info # Docker daemon info
29
30# ── Multi-arch builds ─────────────────────────
31docker buildx create --use
32docker buildx build \
33 --platform linux/amd64,linux/arm64 \
34 -t myregistry/myimage:tag \
35 --push .