08 - Docker Best Practices
Image Size Optimization
Use Minimal Base Images
dockerfile# BAD: Full Ubuntu (~77 MB compressed) FROM ubuntu:22.04 # BETTER: Slim variant (~30 MB) FROM python:3.12-slim # BEST: Alpine (~5 MB) FROM node:20-alpine # ULTIMATE: Scratch (0 MB, for compiled binaries) FROM scratch
| Base Image | Size | Use When |
|---|---|---|
scratch | 0 MB | Go, Rust (static binaries) |
alpine | ~5 MB | Most apps |
*-slim | ~25-80 MB | When alpine causes issues |
distroless | ~20 MB | Security-focused production |
| Full OS | ~70-200 MB | Rarely; debugging |
Reduce Layers and Clean Up
dockerfile# BAD: Multiple layers, cache left behind RUN apt-get update RUN apt-get install -y curl wget git RUN apt-get clean # GOOD: Single layer, clean in same layer RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ wget \ git && \ rm -rf /var/lib/apt/lists/*
Multi-Stage Builds
dockerfile# Build stage: 1 GB+ with all build tools FROM node:20 AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Production stage: ~150 MB runtime only FROM node:20-alpine WORKDIR /app COPY /app/dist ./dist COPY /app/node_modules ./node_modules CMD ["node", "dist/server.js"]
Use .dockerignore
node_modules
.git
.env
*.md
.vscode
coverage
dist
.DS_Store
Security Best Practices
Run as Non-Root
dockerfile# Create and switch to non-root user RUN addgroup --system --gid 1001 appgroup && \ adduser --system --uid 1001 --ingroup appgroup appuser # Copy files with correct ownership COPY . /app USER appuser CMD ["node", "server.js"]
Use Specific Image Tags
dockerfile# BAD: Unpredictable FROM node:latest # BAD: Major version can break FROM node:20 # GOOD: Specific version FROM node:20.11-alpine # BEST: Pin by digest (immutable) FROM node:20.11-alpine@sha256:abc123...
Don't Store Secrets in Images
dockerfile# BAD: Secret baked into image layer (visible in history) ENV API_KEY=super-secret COPY .env /app/.env # GOOD: Pass at runtime # docker run -e API_KEY=super-secret myapp # GOOD: Use BuildKit secrets for build-time secrets RUN npm ci # Build with: docker build --secret id=npmrc,src=.npmrc .
Scan Images for Vulnerabilities
bash# Docker Scout (built-in) docker scout cves myimage:latest # Trivy (popular open-source scanner) trivy image myimage:latest # Snyk snyk container test myimage:latest # Grype grype myimage:latest
Read-Only Filesystem
bash# Run container with read-only root filesystem docker run --read-only \ --tmpfs /tmp \ --tmpfs /var/run \ myapp
Dockerfile Best Practices
Leverage Build Cache
dockerfile# Order from least-changed to most-changed FROM node:20-alpine WORKDIR /app # 1. System deps (rarely change) RUN apk add --no-cache curl # 2. Package manifest (occasionally changes) COPY package.json package-lock.json ./ # 3. Install deps (cached if lockfile unchanged) RUN npm ci --only=production # 4. Source code (changes often -- only this rebuilds) COPY . . RUN npm run build CMD ["node", "dist/server.js"]
Use COPY Not ADD
dockerfile# GOOD: Explicit, no surprises COPY requirements.txt . # AVOID: Implicit behaviors (auto-extract, URL download) ADD requirements.txt . # Only use ADD when you specifically need tar extraction
One Process Per Container
dockerfile# BAD: Multiple services in one container CMD service nginx start && node server.js # GOOD: One service per container, use Compose to orchestrate # Container 1: nginx # Container 2: node server # Container 3: database
Use HEALTHCHECK
dockerfileHEALTHCHECK \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
Set Proper STOPSIGNAL
dockerfile# Some apps need specific signals for graceful shutdown STOPSIGNAL SIGQUIT # nginx uses SIGQUIT for graceful stop
Production Checklist
Container Configuration
bash# Always set resource limits docker run -d \ --memory=512m \ --cpus=1.0 \ --pids-limit=100 \ --restart=unless-stopped \ --read-only \ --tmpfs /tmp \ myapp
Logging
bash# Configure log rotation (prevent disk filling) docker run -d \ --log-driver=json-file \ --log-opt max-size=10m \ --log-opt max-file=5 \ myapp
Image Tagging Strategy
bash# Use semantic versioning + git SHA docker build -t myapp:1.2.3 -t myapp:latest -t myapp:abc123 . # In CI/CD: # myapp:main-abc123 (branch + commit) # myapp:1.2.3 (release tag) # myapp:latest (latest release)
Development Best Practices
Docker Compose for Local Dev
yamlservices: api: build: context: ./api target: development volumes: - ./api/src:/app/src # Live code reload - /app/node_modules # Preserve container's node_modules environment: - NODE_ENV=development ports: - "3000:3000" - "9229:9229" # Debugger
Consistent Environments
dockerfile# Use exact versions everywhere FROM node:20.11.1-alpine3.19 # Pin dependency versions RUN apk add --no-cache curl=8.5.0-r0 # Use lockfiles COPY package-lock.json ./ RUN npm ci # ci uses lockfile exactly
Common Anti-Patterns
| Anti-Pattern | Why It's Bad | Fix |
|---|---|---|
| Running as root | Security risk | USER nonroot |
Using :latest tag | Unpredictable builds | Pin specific version |
| Storing secrets in ENV | Visible in inspect/logs | Use Docker secrets or vault |
| Fat images | Slow pulls, larger attack surface | Multi-stage + alpine |
| Not using .dockerignore | Slow builds, secrets leaked | Add comprehensive .dockerignore |
| Multiple processes | Hard to scale, monitor | One process per container |
| No health checks | Can't detect unhealthy state | Add HEALTHCHECK |
| No resource limits | Noisy neighbor problem | Set memory + CPU limits |
| Ignoring build cache | Slow rebuilds | Order least→most changed |
| Building in production | Slow, includes build tools | Multi-stage builds |
FAANG Interview Angle
Common questions:
- "How would you optimize a Docker image for production?"
- "What security measures would you apply to containers?"
- "How do you handle secrets with Docker?"
- "Describe your ideal Docker development workflow"
- "How do you ensure reproducible builds?"
Key answers:
- Multi-stage builds, alpine base, .dockerignore, layer caching
- Non-root user, read-only filesystem, image scanning, specific tags
- Runtime env vars, Docker secrets, external vault (never in image)
- Compose with bind mounts for dev, multi-stage for prod
- Pinned versions, lockfiles, content-addressable digests