Skip to main content

Docker Compose Production Guide: Multi-Container Apps Done Right (2026)

· 7 min read
Yassine El Haddad
Software Developer & Automation Specialist

I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.

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

AspectDevelopmentProduction
SecretsPlain .env, sometimes in repo.env gitignored, Docker secrets or Vault
RestartOften omittedrestart: unless-stopped or always
Health checksRareRequired for DB, app, queues
VolumesBind mounts for live reloadNamed volumes for persistence
Resource limitsNonemem_limit, cpus per service
NetworksDefault bridgeCustom bridge for isolation
PortsExpose everythingOnly 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

TypeUse caseProduction?
Named volumeDB data, app uploads, cachesYes
Bind mountConfig files, dev source codeDev only
tmpfsEphemeral cacheOptional

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

FeatureDocker ComposeKubernetesDocker Swarm
ScopeSingle hostMulti-host, cloud-nativeMulti-host, Docker-native
SetupMinimalComplexModerate
ScalingManual replica countAuto-scaling, HPAdocker service scale
SecretsFile, envSecrets, VaultDocker secrets
Load balancingTraefik/NginxIngress, servicesSwarm routing
Best for1–3 servers, simple stacksLarge scale, multi-regionDocker 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.

Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50
Start with one service

Add health checks, limits, and secrets to one service first. Validate behavior, then apply the pattern to the rest. Incremental adoption reduces risk.

Frequently Asked Questions

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/.

Common mistakes and fixes

Container exits immediately after start

Check logs: docker compose logs SERVICE. Verify health check command and interval. Ensure dependencies (DB, Redis) are ready. Check env vars and secrets paths.

Data lost after container restart

Use named volumes, not bind mounts for production data. Verify volume is declared in compose and mounted correctly. Run docker volume ls to confirm.

docker compose up -d recreates everything

Use docker compose up -d --no-deps --build SERVICE to rebuild and restart only that service. Other services stay running for zero-downtime.