Docker Compose for Production: The Hard Truths They Don't Tell You
Docker Compose for production is not just docker-compose up -d.
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
- ✓Solid Docker fundamentals (images, containers, networks, volumes)
- ✓Experience running Docker in development
- ✓Basic understanding of Linux process management and resource limits
- ✓Familiarity with YAML syntax
Docker Compose for production requires explicit resource limits (memory, CPU), health checks, restart policies, logging drivers, and network isolation. You must also handle secrets, configs, and rolling updates without downtime. The default compose file from a dev environment will fail in production — often catastrophically.
Think of Docker Compose as the blueprint for a food truck kitchen. In development, you just need the fryer and the fridge to work. In production, you need fire suppression, a backup generator, a grease trap, and a schedule for when each station starts up so the power doesn't trip. The blueprint is the same, but the details are completely different.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
You've been using Docker Compose for local dev. It's great. Now your boss says 'ship it to production with compose.' That's where the pain starts. I've seen a simple compose file bring down a payments service because someone forgot to set a memory limit — the container ate all the RAM, the kernel OOM-killed the database, and the entire platform went dark at 3 AM. Docker Compose for production is not docker-compose up -d. It's a completely different beast. By the end of this, you'll know exactly what to add, what to remove, and what to watch for when running compose in production. You'll be able to write a compose file that survives a traffic spike, a node failure, and a junior dev's 'quick fix'.
Why Your Dev Compose File Will Burn Production
The compose file you use for local development is optimized for convenience, not reliability. It probably has ports exposed to the world, no resource limits, and a restart policy of 'no'. In production, that's a disaster. I've seen a dev compose file with depends_on: - db and no health check — the app started before the database was ready, failed, and the restart policy wasn't set, so it stayed dead. The classic rookie mistake here costs you a full table lock on writes because the app connects before the DB is accepting queries. You need health checks, condition: service_healthy on depends_on, and proper restart policies. Don't assume order — enforce it.
Resource Limits: The One Thing Everyone Forgets
Without resource limits, a single container can consume all host memory and get OOM-killed, taking down other services. Worse, a CPU-hungry container can starve the rest. I've seen a log aggregator with a memory leak eat 8GB before the kernel killed it — and the database was on the same host. The fix: always set deploy.resources.limits.memory and deploy.resources.limits.cpus. Use reservations to guarantee minimum resources. For memory, also set --memory-swap to allow controlled swap usage. The rule of thumb: limit memory to 80% of what you expect the container to need under peak load. Monitor with docker stats and adjust.
Health Checks: The Difference Between Self-Healing and Self-Destructing
A container is 'running' if its main process is alive. That doesn't mean your app is healthy. I've seen a Node.js app that was running but stuck in an infinite loop — the health endpoint never responded, but Docker thought it was fine. The load balancer kept sending traffic, users got timeouts, and the monitoring didn't catch it because the container was 'up'. Health checks fix this. They tell Docker (and orchestrators) when a container is truly ready. Use them in depends_on with condition: service_healthy to ensure startup order. Also use them in restart policies — if health check fails, Docker can restart the container. The key is setting a proper start_period: give your app time to initialize before health checks start.
Logging: Don't Let Logs Fill Your Disk
Default Docker logging uses json-file driver with no rotation. In production, a chatty app can fill a disk in hours. I've seen a misconfigured logger write 50GB of logs in a day, causing the host to run out of space, which killed all containers. The fix: configure logging driver with max-size and max-file. Better yet, use a centralized logging system (ELK, Loki, Datadog) with the syslog or fluentd driver. But even with centralized logging, always set local rotation as a safety net. Also consider log levels — in production, set to warn or error unless debugging.
Secrets and Configs: Never Hardcode in Production
In dev, you might set environment variables directly in compose. In production, that's a security risk. Anyone with access to the compose file or the host can see secrets. Use Docker secrets (swarm mode) or bind-mount secret files. For compose without swarm, use the secrets: directive with file: or external: true. Never use environment: for passwords, API keys, or tokens. Also, use configs for non-sensitive configuration files. The pattern: write secrets to files outside the compose directory, set permissions to 600, and mount them. The app reads them at startup.
Networking: Isolation and Service Discovery
In production, you don't want all containers on the same network. Isolate tiers: a frontend network, a backend network, and a database network. Only expose necessary ports. Use internal networks for databases. Docker Compose creates a default network, but you should define custom networks with driver: overlay for multi-host (swarm) or bridge for single host. Also, use container_name for stable DNS resolution. I've seen a service discovery fail because the container name changed after a restart — use container_name or service name (which is stable).
Volumes: Persistent Data and Performance
In production, database data must survive container restarts. Use named volumes, not bind mounts, for database data. Bind mounts are fine for configs but not for data — they have permission issues and are harder to back up. For performance, consider volume drivers like local (default) or use a network filesystem (NFS, EFS) for multi-host setups. Also, set volume labels for backup automation. I've seen a production database lose data because someone used a bind mount and the host directory got deleted. Named volumes are managed by Docker and safer.
When Not to Use Docker Compose in Production
Docker Compose is great for single-host deployments or small clusters. But if you need multi-host orchestration, rolling updates, auto-scaling, or service discovery across nodes, you need Kubernetes, Docker Swarm, or Nomad. Compose doesn't handle node failures — if the host goes down, everything goes down. Also, Compose doesn't have built-in secrets management (without swarm), and its health check auto-recovery is limited. Use Compose when: you have a single server, you need simplicity, and you can tolerate downtime during updates. Use Kubernetes when: you need high availability, scaling, or multi-host deployments. The rule of thumb: if you have more than 3 services or need zero-downtime deploys, move to an orchestrator.
Rolling Updates with Compose? Not Really — But Here's the Hack
Docker Compose doesn't support rolling updates natively. docker-compose up -d recreates all containers at once, causing downtime. The hack: use docker-compose scale to run multiple replicas, then update one by one. But this is fragile. Better: use Docker Swarm with docker stack deploy, which supports rolling updates. Or use a reverse proxy (nginx, HAProxy) with blue-green deployment: start new containers on different ports, then switch traffic. I've used a simple script that updates containers in sequence with health checks between each. It's not perfect, but it works for low-traffic services.
The 4GB Container That Kept Dying
- Always set memory limits on every container.
- Unlimited memory is a production accident waiting to happen.
docker inspect <container> | grep -i oomdocker stats --no-stream| File | Command / Code | Purpose |
|---|---|---|
| production-compose-basics.yml | version: '3.8' | Why Your Dev Compose File Will Burn Production |
| resource-limits.yml | services: | Resource Limits |
| healthcheck-example.yml | services: | Health Checks |
| logging-config.yml | services: | Logging |
| secrets-config.yml | services: | Secrets and Configs |
| network-isolation.yml | networks: | Networking |
| volumes-production.yml | services: | Volumes |
| rolling-update-hack.sh | SERVICE="api" | Rolling Updates with Compose? Not Really |
Key takeaways
Interview Questions on This Topic
How does Docker Compose handle container startup order, and what are the pitfalls in production?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
That's Docker. Mark it forged?
4 min read · try the examples if you haven't