Docker Compose Production Guide: Multi-Container Apps Done Right (2026)
Development docker-compose.yml works for local runs — production needs secrets management, health checks, resource limits, named volumes, and zero-downtime deploys. This guide shows what sets production Docker Compose apart and provides an annotated template you can adapt. Run it on a Liquid Web VPS for a stable, performant host with SSD storage and managed monitoring.
What Sets Production Docker Compose Apart
| Aspect | Development | Production |
|---|---|---|
| Secrets | Plain .env, sometimes in repo | .env gitignored, Docker secrets or Vault |
| Restart | Often omitted | restart: unless-stopped or always |
| Health checks | Rare | Required for DB, app, queues |
| Volumes | Bind mounts for live reload | Named volumes for persistence |
| Resource limits | None | mem_limit, cpus per service |
| Networks | Default bridge | Custom bridge for isolation |
| Ports | Expose everything | Only what's needed for ingress |
Production docker-compose.yml Template
services:
app:
image: myapp:latest
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
memory: 512M
cpus: "0.5"
env_file:
- .env
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://user:${DB_PASSWORD}@db:5432/mydb
secrets:
- db_password
volumes:
- app_data:/app/data
networks:
- backend
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 256M
cpus: "0.25"
volumes:
- db_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=user
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
networks:
- backend
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
app_data:
db_data:
networks:
backend:
driver: bridge
Secrets Management
Option 1: .env file — For non-sensitive config (debug flags, feature toggles). Add .env to .gitignore. Load with env_file: - .env.
Option 2: Docker secrets — For passwords, API keys. Create files in ./secrets/ (gitignored), reference in compose. Files are mounted read-only at /run/secrets/.
Option 3: External vault — Use HashiCorp Vault or cloud secret managers. Inject at runtime via entrypoint or init container. Best for large teams and compliance.
Never commit .env or secrets/ to version control.
Health Checks
Health checks let Docker and orchestrators know when a container is ready and when it's unhealthy. Unhealthy containers can trigger restarts and be removed from load balancing.
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
- test — Command that exits 0 if healthy. Use
curl,wget, or app-specific CLI. - interval — How often to run the check.
- retries — Failures before marking unhealthy.
- start_period — Grace period after start before counting failures.
depends_on with condition: service_healthy ensures the app starts only after the database is ready.
Networking
Use a custom bridge network so services communicate by name (e.g. db:5432) and isolate traffic. Don't expose internal ports (PostgreSQL, Redis) to the host unless required. Put Traefik on the same network for HTTPS routing to your app.
Volume Strategy
| Type | Use case | Production? |
|---|---|---|
| Named volume | DB data, app uploads, caches | Yes |
| Bind mount | Config files, dev source code | Dev only |
| tmpfs | Ephemeral cache | Optional |
Named volumes survive container recreation and are managed by Docker. For backups, run pg_dump or sync volume data to object storage.
Resource Limits
On shared hosting, a single runaway container can exhaust memory and CPU. Set limits:
deploy:
resources:
limits:
memory: 512M
cpus: "0.5"
Adjust based on workload. Monitor with docker stats to tune. Liquid Web VPS provides dedicated resources — limits still help prevent one service from affecting others.
Zero-Downtime Deploys
To update one service without restarting the stack:
docker compose build app
docker compose up -d --no-deps --build app
--no-deps avoids restarting dependent services (e.g. DB). The app container is replaced; if you use a reverse proxy like Traefik, it handles the brief gap. For true zero-downtime, use multiple replicas and rolling updates (Docker Swarm or Kubernetes).
Docker Compose vs Kubernetes vs Docker Swarm
| Feature | Docker Compose | Kubernetes | Docker Swarm |
|---|---|---|---|
| Scope | Single host | Multi-host, cloud-native | Multi-host, Docker-native |
| Setup | Minimal | Complex | Moderate |
| Scaling | Manual replica count | Auto-scaling, HPA | docker service scale |
| Secrets | File, env | Secrets, Vault | Docker secrets |
| Load balancing | Traefik/Nginx | Ingress, services | Swarm routing |
| Best for | 1–3 servers, simple stacks | Large scale, multi-region | Docker users needing orchestration |
For single-server production (VPS, small teams), Docker Compose with Traefik is often enough. Move to Kubernetes when you need multi-node, auto-scaling, or complex deployments. Pair with VPS security hardening and Coolify for a complete self-hosted stack.
Production Checklist
Before going live: (1) All secrets in files or vault, not in compose; (2) Health checks on app and DB; (3) Resource limits per service; (4) Named volumes for persistent data; (5) Custom network; (6) Restart policy unless-stopped; (7) Reverse proxy (Traefik) for TLS. Use a Liquid Web VPS for SSD storage, DDoS protection, and 99.99% uptime.
Add health checks, limits, and secrets to one service first. Validate behavior, then apply the pattern to the rest. Incremental adoption reduces risk.
Use named volumes for persistent data (databases, uploads). Bind mounts are fine for read-only config. Avoid bind mounts for critical write data — they're tied to host paths and harder to back up.
Docker marks a container unhealthy after repeated health check failures. With restart: unless-stopped, Docker will restart it. Orchestrators like Traefik can also remove unhealthy containers from load balancing.
Build the new image, then run docker compose up -d --no-deps --build SERVICE to update only that service. Use Traefik or similar to route traffic; brief downtime is usually acceptable for single-replica apps.
When you need multi-node clustering, horizontal auto-scaling, rolling updates with zero downtime, or complex service mesh. For 1–2 servers and a handful of services, Compose + Traefik is sufficient.
Use Docker secrets (file-based) or inject from a vault. Never put plain passwords in docker-compose.yml or commit them to git. The secrets directive mounts files at /run/secrets/.




