Docker Best Practices

Multi-Stage Build (Go example)

# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download           # Cache dependencies as separate layer
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .

# Stage 2: Minimal runtime image
FROM gcr.io/distroless/static-debian12
# Or: FROM alpine:3.19 (adds 5MB but has shell)
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

Layer Cache Optimization

# BAD โ€” invalidates cache on any source change
COPY . .
RUN npm install

# GOOD โ€” package.json changes rarely
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .

# Rule: put things that change least first

Security Hardening

# Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Read-only filesystem
docker run --read-only --tmpfs /tmp myapp

# Limit capabilities
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp

# Scan for vulnerabilities
docker scout cves myimage:latest
trivy image myimage:latest

docker-compose Best Practices

services:
  app:
    build:
      context: .
      target: production          # Use build target
    image: myapp:${VERSION:-latest}
    restart: unless-stopped
    environment:
      - NODE_ENV=production
    env_file: .env               # Never hardcode secrets
    ports:
      - "127.0.0.1:8080:8080"   # Bind to loopback only
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 512m
          cpus: '0.5'

Image Size Comparison

Base ImageSizeShellUse Case
ubuntu:22.04~77MBโœ“Full debugging
debian:slim~74MBโœ“Slim debian
alpine:3.19~7MBshSmall, musl libc
distroless/static~2MBโœ—Static binaries
scratch0MBโœ—Minimal, Go/Rust