Docker Compose in production: the parts that bite
Restart policies, health-gated dependencies, resource caps, and secrets — what to fix before Compose meets real traffic.
Compose on a laptop forgives you: containers restart when you click, secrets live in .env, and depends_on means started, not ready. In production the same file meets reboots, traffic spikes, and anyone with docker inspect access. Production Compose is mostly discipline — health checks that gate dependencies, restart policies that actually restart, limits that keep one leak from taking the host, and secrets mounted as files instead of environment variables.
This note shapes a compose.yaml you would trust on a single VM or Swarm-style deploy: pinned images, service_healthy dependencies, and resource ceilings. Networking and volume patterns from Containers & Docker fundamentals still apply — Compose is orchestration-lite, not magic.
Without healthchecks, depends_on only waits for container start — your API will race the database and flap until retry logic saves you.
A production-shaped compose file
Pin images by digest or semver tag — not :latest. Declare healthchecks that reflect real readiness (database accepts connections, HTTP /healthz returns 200). Use the Compose spec's deploy.resources limits even on standalone Docker; they map to cgroup caps on the daemon.
services:db:image: postgres:16-alpinerestart: unless-stoppedenvironment:POSTGRES_PASSWORD_FILE: /run/secrets/db_passwordsecrets: [db_password]healthcheck:test: ["CMD-SHELL", "pg_isready -U app"]interval: 10stimeout: 5sretries: 5api:image: ghcr.io/acme/api:1.4.2restart: unless-stoppeddepends_on:db: { condition: service_healthy }healthcheck:test: ["CMD", "/app", "healthcheck"]interval: 15sretries: 3deploy:resources:limits: { cpus: "1.0", memory: 512M }secrets: [db_password, api_token]secrets:db_password:file: ./secrets/db_password.txtapi_token:file: ./secrets/api_token.txt
Secrets are not environment variables
Values in environment: show up in docker inspect, process listings, and crash dumps. Compose secrets mount files under /run/secrets/ with mode 0400 — your app reads the file at startup and never exports it to child shells. For multi-host production, external secret stores beat flat files on disk, but flat files still beat plaintext env in git.
docker compose up -ddocker compose psNAME STATUS HEALTHdb running healthyapi running healthyapi would not start until db healthcheck passedRestart, logs, and host survival
Use restart: unless-stopped for daemons you want after reboot — not always, which fights intentional stops during maintenance. Configure log rotation (logging.options max-size) so one chatty container does not fill the disk. Resource limits are your poor man's Kubernetes requests/limits: without them, a memory leak in the API container becomes an OOM kill for the entire host.
Put an edge proxy in the same compose file with explicit network aliases and read-only root filesystem where the app allows it. Document a runbook for docker compose pull && docker compose up -d upgrades — pinned tags mean you choose when to absorb upstream base image patches instead of silently floating on :latest every restart.
Where this goes next
When replicas, rolling updates, and cluster networking enter the picture, graduate to Kubernetes or Swarm with the same health and secret discipline. Deep dives on Compose networking, volumes, and production ops live in Docker in depth — same containers, harder uptime requirements.
Go deeper in a courseDocker in depthCompose, networking, storage, and running containers reliably in production.View course