Docker Best Practices for Production

Docker makes it easy to containerize applications. Docker makes it equally easy to create bloated, insecure, slow-to-build images. The difference is discipline. These practices come from running containers in production—where image size affects deployment speed, security vulnerabilities get exploited, and build times multiply across teams. Start With the Right Base Image Your base image choice cascades through everything else. The options: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Full OS - 900MB+ FROM ubuntu:22.04 # Slim OS - 80MB FROM debian:bookworm-slim # Minimal - 5MB FROM alpine:3.19 # Language-specific slim - varies FROM python:3.12-slim FROM node:20-alpine # Distroless - minimal runtime only FROM gcr.io/distroless/python3 General guidance: ...

March 11, 2026 Â· 7 min Â· 1379 words Â· Rob Washington

Environment Variables Done Right: Configuration Without the Pain

Environment variables seem trivial. Set a value, read it in code. Done. Then you deploy to production and realize the staging database URL leaked into prod. Or someone commits a .env file with API keys. Or your Docker container starts with 47 environment variables and nobody knows which ones are actually required. Here’s how to do it properly. The Basics: Reading Environment Variables Every language has a way to read environment variables: ...

March 10, 2026 Â· 5 min Â· 924 words Â· Rob Washington

Docker Multi-Stage Builds: Smaller Images, Faster Deploys

Your Docker images are probably too big. Most applications ship with compilers, package managers, and debug tools that never run in production. Multi-stage builds fix this by separating your build environment from your runtime environment. The Problem with Single-Stage Builds Here’s a typical Node.js Dockerfile: 1 2 3 4 5 6 7 8 FROM node:20 WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build EXPOSE 3000 CMD ["node", "dist/index.js"] This works, but the resulting image includes: ...

March 9, 2026 Â· 5 min Â· 961 words Â· Rob Washington

Docker Secrets Management: From Development to Production

Secrets management in Docker is where most teams get bitten. Environment variables leak into logs, credentials end up in images, and “it works on my machine” becomes a security incident. Here’s how to handle secrets properly at every stage. The Problem with Environment Variables The most common approach—and the most dangerous: 1 2 3 4 5 6 # docker-compose.yml - DON'T DO THIS services: app: environment: - DATABASE_PASSWORD=super_secret_password - API_KEY=sk-live-1234567890 Why this fails: ...

March 7, 2026 Â· 5 min Â· 944 words Â· Rob Washington

Docker Compose Patterns for Production

Docker Compose is often dismissed as “just for development,” but with the right patterns it handles production workloads well. Here are patterns that have survived contact with real systems. Service Dependencies Done Right The basic depends_on only waits for the container to start, not for the service to be ready: 1 2 3 4 5 6 7 # This doesn't guarantee postgres is accepting connections services: app: depends_on: - db db: image: postgres:15 Use health checks for real dependency management: ...

March 5, 2026 Â· 4 min Â· 832 words Â· Rob Washington

Container Security: Practical Hardening for Production

Containers provide isolation, but they’re not magic security boundaries. A misconfigured container can expose your entire host. Let’s fix that. Don’t Run as Root The single biggest mistake: running containers as root. 1 2 3 4 5 6 7 8 9 10 11 12 # Bad: runs as root by default FROM node:20 COPY . /app CMD ["node", "server.js"] # Good: create and use non-root user FROM node:20 RUN groupadd -r appgroup && useradd -r -g appgroup appuser WORKDIR /app COPY --chown=appuser:appgroup . . USER appuser CMD ["node", "server.js"] Why it matters: if an attacker escapes the container while running as root, they’re root on the host. As a non-root user, they’re limited. ...

March 4, 2026 Â· 5 min Â· 1063 words Â· Rob Washington

Docker Multi-Stage Builds: Smaller Images, Faster Deploys

Your Docker images are probably too big. Build tools, dev dependencies, source files—all shipping to production when only the compiled binary matters. Multi-stage builds fix this by separating build environment from runtime environment. The Problem A typical single-stage Dockerfile: 1 2 3 4 5 6 7 8 9 FROM python:3.11 WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"] This image includes: ...

March 1, 2026 Â· 4 min Â· 852 words Â· Rob Washington

Docker Compose Patterns for Local Development

Docker Compose turns “works on my machine” into “works everywhere.” Here’s how to structure it for real development workflows. Basic Structure 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # docker-compose.yml services: app: build: . ports: - "3000:3000" volumes: - .:/app environment: - NODE_ENV=development db: image: postgres:15 environment: POSTGRES_PASSWORD: devpass Start everything: ...

February 28, 2026 Â· 6 min Â· 1088 words Â· Rob Washington

Environment Variable Patterns for 12-Factor Apps

Environment variables are the 12-factor way to configure applications. But “just use env vars” glosses over real complexity. Here’s how to do it well. The Basics Done Right Type Coercion Environment variables are always strings. Handle conversion explicitly: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import os def get_env_bool(key: str, default: bool = False) -> bool: value = os.getenv(key, "").lower() if value in ("true", "1", "yes", "on"): return True if value in ("false", "0", "no", "off"): return False return default def get_env_int(key: str, default: int = 0) -> int: try: return int(os.getenv(key, default)) except ValueError: return default # Usage DEBUG = get_env_bool("DEBUG", False) PORT = get_env_int("PORT", 8080) WORKERS = get_env_int("WORKERS", 4) Required vs Optional Fail fast on missing required config: ...

February 28, 2026 Â· 5 min Â· 994 words Â· Rob Washington

Container Health Check Patterns That Actually Work

Your container says it’s healthy. Your users say the app is broken. Sound familiar? Basic health checks only tell you if a process is running. Here’s how to build checks that catch real problems. Beyond “Is It Alive?” Most health checks look like this: 1 HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1 This tells you the HTTP server responds. It doesn’t tell you: Can the app reach the database? Is the cache connected? Are critical background workers running? Is the disk filling up? Layered Health Checks Implement three levels: ...

February 28, 2026 Â· 4 min Â· 744 words Â· Rob Washington