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 ImageSizeUse When
scratch0 MBGo, Rust (static binaries)
alpine~5 MBMost apps
*-slim~25-80 MBWhen alpine causes issues
distroless~20 MBSecurity-focused production
Full OS~70-200 MBRarely; 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 --from=builder /app/dist ./dist COPY --from=builder /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 --chown=appuser:appgroup . /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 --mount=type=secret,id=npmrc,target=/root/.npmrc 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

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ 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

yaml
services: 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-PatternWhy It's BadFix
Running as rootSecurity riskUSER nonroot
Using :latest tagUnpredictable buildsPin specific version
Storing secrets in ENVVisible in inspect/logsUse Docker secrets or vault
Fat imagesSlow pulls, larger attack surfaceMulti-stage + alpine
Not using .dockerignoreSlow builds, secrets leakedAdd comprehensive .dockerignore
Multiple processesHard to scale, monitorOne process per container
No health checksCan't detect unhealthy stateAdd HEALTHCHECK
No resource limitsNoisy neighbor problemSet memory + CPU limits
Ignoring build cacheSlow rebuildsOrder least→most changed
Building in productionSlow, includes build toolsMulti-stage builds

FAANG Interview Angle

Common questions:

  1. "How would you optimize a Docker image for production?"
  2. "What security measures would you apply to containers?"
  3. "How do you handle secrets with Docker?"
  4. "Describe your ideal Docker development workflow"
  5. "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

Official Links