Docker Compose is often dismissed as a "development-only" tool, but with the right patterns it can power small-to-medium production workloads reliably. This guide covers the practices that separate a throwaway dev stack from a production-grade Compose deployment.
Resource Limits
Every service should declare CPU and memory limits. Without them a single runaway process can starve the entire host.
services:
api:
image: myorg/api:1.4.2
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
Reservations guarantee a baseline; limits cap the maximum. Use docker stats to baseline real usage before setting values.
Health Checks
Compose can restart unhealthy containers automatically when you define health checks:
services:
api:
image: myorg/api:1.4.2
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
The start_period gives the container time to initialize before health checks count against it.
Restart Policies
| Policy | Behavior |
|---|---|
no | Never restart (default) |
always | Restart on any exit |
unless-stopped | Restart unless explicitly stopped |
on-failure | Restart only on non-zero exit code |
For production, unless-stopped is usually the best choice. It survives host reboots (when the Docker daemon starts) but respects intentional docker compose stop commands.
Logging Configuration
Default JSON-file logs grow unbounded. Always set limits:
services:
api:
image: myorg/api:1.4.2
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
For centralized logging, use the fluentd or gelf driver to ship logs to an aggregator.
Environment Variable Management
Never hard-code secrets in docker-compose.yml. Use a .env file that is excluded from version control:
# .env (git-ignored)
POSTGRES_PASSWORD=supersecretvalue
API_SECRET_KEY=anothersecret
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
For extra security, use Docker secrets with docker secret create or mount secret files as read-only volumes.
Named Volumes and Backups
Always use named volumes for persistent data:
volumes:
pgdata:
driver: local
services:
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
Back up named volumes with:
docker run --rm -v pgdata:/data -v $(pwd):/backup alpine tar czf /backup/pgdata-backup.tar.gz -C /data .
Network Isolation
Create explicit networks so services only communicate with what they need:
networks:
frontend:
backend:
services:
nginx:
networks: [frontend, backend]
api:
networks: [backend]
db:
networks: [backend]
The database is not reachable from the frontend network, reducing the attack surface.
Pinning Image Versions
Never use :latest in production. Pin to a specific tag or digest:
services:
api:
image: myorg/api:1.4.2@sha256:abc123...
This guarantees reproducibility across deployments and makes rollbacks straightforward.
Deployment Checklist
- All images pinned to specific versions
- Resource limits set on every service
- Health checks defined for application containers
- Restart policy set to
unless-stopped - Log rotation configured
- Secrets managed via
.envor Docker secrets - Named volumes for all persistent data
- Explicit networks for service isolation
- Backups automated and tested
Docker Compose is not Kubernetes, but for single-host deployments it is simple, predictable, and battle-tested when configured properly.
Enjoyed this article?
Get more DevOps insights delivered to your inbox.
Get new posts by email
Subscribe to get an email when a new blog post is published. Skip anytime.
No spam, unsubscribe anytime.
Related Posts
Discussion
0 comments
Sign in to join the conversation.
Be the first to comment
Start a conversation about this post