# Dockerfile Best Practices Reference ## Layer Optimization ### The Golden Rule Every `RUN`, `COPY`, and `ADD` instruction creates a new layer. Fewer layers = smaller image. ### Combine Related Commands ```dockerfile # Bad — 3 layers RUN apt-get update RUN apt-get install -y curl git RUN rm -rf /var/lib/apt/lists/* # Good — 1 layer RUN apt-get update && \ apt-get install -y --no-install-recommends curl git && \ rm -rf /var/lib/apt/lists/* ``` ### Order Layers by Change Frequency ```dockerfile # Least-changing layers first COPY package.json package-lock.json ./ # Changes rarely RUN npm ci # Changes when deps change COPY . . # Changes every build RUN npm run build # Changes every build ``` ### Use .dockerignore ``` .git node_modules __pycache__ *.pyc .env .env.* dist build *.log .DS_Store .vscode .idea coverage .pytest_cache ``` --- ## Base Image Selection ### Size Comparison (approximate) | Base | Size | Use Case | |------|------|----------| | `scratch` | 0MB | Static binaries (Go, Rust) | | `distroless/static` | 2MB | Static binaries with CA certs | | `alpine` | 7MB | Minimal Linux, shell access | | `distroless/base` | 20MB | Dynamic binaries (C/C++) | | `debian-slim` | 80MB | When you need glibc + apt | | `ubuntu` | 78MB | Full Ubuntu ecosystem | | `python:3.12-slim` | 130MB | Python apps (production) | | `node:20-alpine` | 130MB | Node.js apps | | `golang:1.22` | 800MB | Go build stage only | | `python:3.12` | 900MB | Never use in production | | `node:20` | 1000MB | Never use in production | ### When to Use Alpine - Small image size matters - No dependency on glibc (musl works) - Willing to handle occasional musl-related issues - Not running Python with C extensions that need glibc ### When to Use Slim - Need glibc compatibility - Python with compiled C extensions (numpy, pandas) - Fewer musl compatibility issues - Still much smaller than full images ### When to Use Distroless - Maximum security (no shell, no package manager) - Compiled/static binaries - Don't need debugging access inside container - Production-only (not development) --- ## Multi-Stage Builds ### Why Multi-Stage - Build tools and source code stay out of production image - Final image contains only runtime artifacts - Dramatically reduces image size and attack surface ### Naming Stages ```dockerfile FROM golang:1.22 AS builder # Named stage FROM alpine:3.19 AS runtime # Named stage COPY --from=builder /app /app # Reference by name ``` ### Selective Copy ```dockerfile # Only copy the built artifact — nothing else COPY --from=builder /app/server /server COPY --from=builder /app/config.yaml /config.yaml # Don't COPY --from=builder /app/ /app/ (copies source code too) ``` --- ## Security Hardening ### Run as Non-Root ```dockerfile # Create user RUN groupadd -r appgroup && useradd -r -g appgroup -s /sbin/nologin appuser # Set ownership COPY --chown=appuser:appgroup . . # Switch user (after all root-requiring operations) USER appuser ``` ### Secret Management ```dockerfile # Bad — secret baked into layer ENV API_KEY=sk-12345 # Good — BuildKit secret mount (never in layer) RUN --mount=type=secret,id=api_key \ export API_KEY=$(cat /run/secrets/api_key) && \ ./configure --api-key=$API_KEY ``` Build with: ```bash docker build --secret id=api_key,src=./api_key.txt . ``` ### Read-Only Filesystem ```yaml # docker-compose.yml services: app: read_only: true tmpfs: - /tmp - /var/run ``` ### Drop Capabilities ```yaml services: app: cap_drop: - ALL cap_add: - NET_BIND_SERVICE # Only if binding to ports < 1024 ``` --- ## Build Performance ### BuildKit Cache Mounts ```dockerfile # Cache pip downloads across builds RUN --mount=type=cache,target=/root/.cache/pip \ pip install -r requirements.txt # Cache apt downloads RUN --mount=type=cache,target=/var/cache/apt \ apt-get update && apt-get install -y curl ``` ### Parallel Builds ```dockerfile # These stages build in parallel when using BuildKit FROM node:20-alpine AS frontend COPY frontend/ . RUN npm ci && npm run build FROM golang:1.22 AS backend COPY backend/ . RUN go build -o server FROM alpine:3.19 COPY --from=frontend /dist /static COPY --from=backend /server /server ``` ### Enable BuildKit ```bash export DOCKER_BUILDKIT=1 docker build . # Or in daemon.json { "features": { "buildkit": true } } ``` --- ## Health Checks ### HTTP Service ```dockerfile HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1 ``` ### Without curl (using wget) ```dockerfile HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:8000/health || exit 1 ``` ### TCP Check ```dockerfile HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD nc -z localhost 8000 || exit 1 ``` ### PostgreSQL ```dockerfile HEALTHCHECK --interval=10s --timeout=5s --retries=5 \ CMD pg_isready -U postgres || exit 1 ``` ### Redis ```dockerfile HEALTHCHECK --interval=10s --timeout=3s --retries=3 \ CMD redis-cli ping | grep PONG || exit 1 ```