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 Image | Size | Shell | Use Case |
|---|---|---|---|
| ubuntu:22.04 | ~77MB | โ | Full debugging |
| debian:slim | ~74MB | โ | Slim debian |
| alpine:3.19 | ~7MB | sh | Small, musl libc |
| distroless/static | ~2MB | โ | Static binaries |
| scratch | 0MB | โ | Minimal, Go/Rust |