Docker Patterns: Production-Grade Containerization
Overview
Docker patterns encompass the art and science of building efficient, secure, and maintainable container images. This skill covers the entire lifecycle — from writing optimized Dockerfiles and orchestrating multi-service environments with docker-compose to hardening images against vulnerabilities and minimizing attack surface. Mastery of these patterns is essential for any DevOps practitioner aiming to ship reliable, fast, and secure software.
Core Principles
- Minimalism — Every layer, every package, every instruction adds weight and risk. Include only what the runtime needs, nothing more.
- Reproducibility — Builds must produce identical images given the same source. Pin base image tags, lock dependency versions, and avoid network-dependent build steps.
- Cache Efficiency — Order Dockerfile instructions from least to most frequently changing to maximize layer cache reuse. This transforms build times from minutes to seconds.
- Defense in Depth — Never run containers as root. Use read-only root filesystems. Drop all unnecessary Linux capabilities. Scan images before deployment.
- Single Responsibility — Each container should run exactly one process. Use docker-compose to compose multiple containers rather than cramming processes into one image.
- Immutable Infrastructure — Never modify a running container. Build a new image, test it, and replace the old one. This eliminates configuration drift.
Docker Maturity Model
🟢 Beginner
- Uses a single
FROMstatement in Dockerfiles - Runs containers as
rootby default - Installs build tools and runtime dependencies in the same layer
- No
.dockerignorefile - Pulls
:latestbase image tags - Uses
docker commitfor ad-hoc image creation - Builds take 5–15 minutes with no layer caching strategy
- Image sizes range from 500 MB to 2+ GB
Typical Beginner Dockerfile (anti-pattern):
dockerfileFROM node:latest RUN apt-get update && apt-get install -y build-essential COPY . /app WORKDIR /app RUN npm install RUN npm run build CMD ["npm", "start"]
🟡 Proficient
- Uses multi-stage builds to separate build and runtime environments
- Leverages official slim or alpine base images (e.g.,
node:20-slim) - Creates and uses
.dockerignorefiles - Pins specific base image digests (
node:20-slim@sha256:...) - Orders Dockerfile layers for optimal caching (dependencies before source)
- Runs containers with a non-root user
- Uses
docker scanortrivyfor vulnerability scanning - Image sizes: 100–300 MB
- Build times: 1–3 minutes
Proficient Dockerfile:
dockerfile# Stage 1: Build FROM node:20-slim AS builder WORKDIR /build COPY package.json package-lock.json ./ RUN npm ci --only=production COPY . . RUN npm run build # Stage 2: Runtime FROM node:20-slim AS runtime RUN groupadd -r appuser && useradd -r -g appuser appuser WORKDIR /app COPY --from=builder /build/dist ./dist COPY --from=builder /build/node_modules ./node_modules USER appuser EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s \ CMD node healthcheck.js CMD ["node", "dist/server.js"]
🔴 Expert
- Distroless or scratch-based runtime images for minimal attack surface
- BuildKit cache mounts and
--mount=type=cachefor zero-copy dependency installs - Custom base images with pre-hardened OS configurations
- SBOM (Software Bill of Materials) generation with
docker sbomorsyft - Signed images with Docker Content Trust (DCT) or cosign
- Runtime security profiles: seccomp, AppArmor, and SELinux policies
- Dockerfile linting with
hadolintintegrated into CI - Image size: 10–50 MB for compiled languages, 80–150 MB for interpreted
- Build times: 15–45 seconds
- Automatic base image vulnerability patching with Dependabot/Renovate
Expert Dockerfile:
dockerfile# syntax=docker/dockerfile:1.7 # Stage 1: Build with cache mounts FROM golang:1.22-alpine AS builder RUN apk add --no-cache ca-certificates WORKDIR /src COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod \ go mod download COPY . . RUN --mount=type=cache,target=/go/pkg/mod \ CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app . # Stage 2: Distroless runtime FROM gcr.io/distroless/static-debian12:nonroot COPY --from=builder /app /app COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ USER nonroot:nonroot EXPOSE 8080 ENTRYPOINT ["/app"]
Actionable Guidance
1. Multi-Stage Builds
Multi-stage builds use multiple FROM statements in a single Dockerfile. Each stage can use a different base image. Only the final stage is saved in the image — intermediate stages are discarded.
Why they matter:
- Build tools (compilers, dev dependencies) are isolated in build stages
- Runtime images contain only binaries and essentials
- Dramatically reduces image size and attack surface
Pattern — Build and Copy Artifacts:
dockerfile# Build stage FROM python:3.12-slim AS builder COPY requirements.txt . RUN pip install --user -r requirements.txt # Runtime stage FROM python:3.12-slim COPY --from=builder /root/.local /root/.local COPY app/ ./app ENV PATH=/root/.local/bin:$PATH CMD ["python", "app/main.py"]
Pattern — Conditional Stages with Build Args:
dockerfileARG BUILD_ENV=production FROM node:20-alpine AS base WORKDIR /app COPY package*.json ./ FROM base AS development RUN npm install --include=dev COPY . . FROM base AS production RUN npm ci --only=production COPY . . RUN npm run build FROM ${BUILD_ENV} CMD ["node", "dist/server.js"]
2. Dockerfile Best Practices
Layer Caching Strategy:
- Copy
package.json/requirements.txtbefore source code — dependency install layers only invalidate when dependencies change - Combine
RUN apt-get updatewithapt-get installin the same layer to avoid stale cache issues - Use
--no-cacheor--no-install-recommendsflags to reduce size
dockerfile# GOOD: Dependencies before source COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # BAD: Source before dependencies — invalidates cache on every code change COPY . . RUN pip install -r requirements.txt
.dockerignore File:
Always create a .dockerignore to exclude files from the build context:
node_modules .git .env *.md coverage .gitignore Dockerfile .dockerignore dist .cache npm-debug.log
Image Size Optimization:
- Prefer
-slimvariants over full images - Use
-alpinefor even smaller sizes when compatibility allows - Clean up package manager caches in the same RUN layer:
dockerfile
RUN apt-get update && \ apt-get install -y --no-install-recommends curl && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* - Remove temporary files within the same RUN instruction
Health Checks:
dockerfileHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1
3. Docker Compose Patterns
Service Composition:
yamlversion: "3.9" services: api: build: context: . target: production cache_from: - myapp/api:latest ports: - "8080:8080" environment: - DB_HOST=db - REDIS_HOST=redis depends_on: db: condition: service_healthy redis: condition: service_started volumes: - type: volume source: app_data target: /app/data db: image: postgres:16-alpine volumes: - pgdata:/var/lib/postgresql/data environment: POSTGRES_DB: myapp POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: - db_password healthcheck: test: ["CMD-SHELL", "pg_isready -U myapp"] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine volumes: - redis_data:/data command: redis-server --appendonly yes volumes: pgdata: redis_data: app_data: secrets: db_password: file: ./secrets/db_password.txt
Development vs Production Profiles:
yamlservices: app: build: . profiles: ["dev", "prod"] mailhog: image: mailhog/mailhog profiles: ["dev"] ports: ["8025:8025"] prometheus: image: prom/prometheus profiles: ["prod"]
Start dev: docker compose --profile dev up
Docker Compose Health Check Wait Pattern:
yamlservices: app: depends_on: db: condition: service_healthy
4. Security Best Practices
Never Run as Root:
dockerfileRUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser
Read-Only Root Filesystem:
yamlservices: app: read_only: true tmpfs: - /tmp
Drop Capabilities:
yamlservices: app: cap_drop: - ALL cap_add: - NET_BIND_SERVICE
Security Scanning with Trivy:
bash# Scan image trivy image --severity HIGH,CRITICAL myapp:latest # Scan Dockerfile for misconfigurations trivy config --severity HIGH,CRITICAL Dockerfile # CI integration trivy image --exit-code 1 --severity CRITICAL myapp:latest
Docker Bench Security:
bashdocker run --privileged --pid=host \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /etc:/etc:ro \ docker/docker-bench-security
Use Specific Image Digests:
dockerfileFROM node:20-slim@sha256:abc123def456...
Common Mistakes
-
Using
:latest— Unpinned tags cause unpredictable builds. Always pin to a specific version or digest. -
Copying entire context —
COPY . /appsends the entire directory includingnode_modules,.git, and secrets. Use.dockerignoreand specific COPY paths. -
Installing unnecessary packages — Every package is a potential vulnerability. Use
--no-install-recommendsand prefer distroless images. -
Multiple services in one container — Containers should run one process. Use docker-compose for multi-service architectures.
-
Storing secrets in images — Secrets in Dockerfile layers persist even if the layer is removed. Use Docker secrets, BuildKit secrets, or external secret stores.
-
Ignoring layer ordering — Putting code before dependencies destroys cache efficiency. Always structure Dockerfiles for optimal layer caching.
-
Skipping health checks — Without health checks, orchestration platforms can't determine actual container readiness.
-
Running as root — Root in a container is root on the host if the container escapes. Always use a non-root user.
-
No vulnerability scanning — Images accumulate CVEs over time. Scan in CI and set thresholds to fail builds on critical/high vulnerabilities.
-
Overly permissive compose volumes —
.:/appbind mounts expose the host filesystem. Use named volumes or specific host paths instead.


