Intro
Docker Compose streamlines multi-container development, but production deployments demand more than a working docker compose up. A misconfigured Compose file, incorrect volume mapping, or insecure secret handling can cause outages, data loss, or security breaches. This guide addresses common Docker Compose production configuration mistakes with practical examples and recovery steps.
You will learn to identify version and environment issues, validate configurations before deployment, diagnose failures, and roll back safely. Each section includes concrete commands, expected outputs, and failure signals to help you move from observed problem to verified result.
The goal is operational safety: observe before changing, limit the blast radius, use placeholders instead of secrets, verify the result, and document recovery procedures before an incident forces a rushed decision.
Version and Environment Inventory
Before making any changes, establish exactly what you are running. A production incident often stems from a mismatch between the Compose file format, the Docker Engine, and the Docker Compose plugin version. Check the installed versions first:
docker --version
# Docker version 24.0.7, build afdd53b
docker compose version
# Docker Compose version v2.23.0-desktop.1
If your Compose file specifies a version: key, ensure it is compatible with your Compose plugin. As of Compose v2, the version key is obsolete and ignored, but removing it may confuse older tooling. Keep the file format up to date by using the latest schema without a version key, unless you must support legacy systems.
Next, inventory the running containers and their health:
docker compose ps
# NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
# myapp-web-1 myapp/web:1.2.0 "nginx -g 'daemon of…" web 2 hours ago Up 2 hours 0.0.0.0:8080->80/tcp
# myapp-db-1 postgres:16.1 "docker-entrypoint.s…" db 2 hours ago Up 2 hours 5432/tcp
If a container is unhealthy or restarting, inspect it. Use Docker's built-in health status field for a quick overview, then check restart count separately:
docker ps --filter name=myapp-web-1 --format '{{.Names}} {{.HealthStatus}}'
# myapp-web-1 healthy
docker inspect myapp-web-1 --format '{{.State.RestartCount}}'
# 0
Capture logs to identify errors:
docker compose logs web --tail 100
Data Persistence Verification
A common production mistake is storing application data inside the container filesystem. When the container is recreated, data is lost. Always confirm where data lives:
services:
db:
image: postgres:16.1
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Here, db_data is a named volume managed by Docker. It persists across container rebuilds. Alternatively, a bind mount ./data:/var/lib/postgresql/data maps a host directory but can introduce permission and portability issues.
Test persistence with a restart:
docker compose stop db
docker compose up -d db
docker compose exec db ls /var/lib/postgresql/data
# If the directory is empty or missing expected files, the data was not persisted correctly.
Safe Configuration Path
Production configuration should be deliberate, versioned, and reviewable. Avoid editing docker-compose.yml directly on the server. Instead, use environment-specific override files and environment variables for values that change between environments.
Using Override Files
A common mistake is maintaining multiple almost-identical Compose files or hardcoding production values into the base file. Use a base file with overrides:
# docker-compose.yml
services:
web:
image: nginx:1.25
ports:
- "80:80"
environment:
- LOG_LEVEL=info
# docker-compose.prod.yml
services:
web:
environment:
- LOG_LEVEL=warn
- VIRTUAL_HOST=example.com
deploy:
replicas: 3
Deploy with:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
This keeps the base file clean and production-specific settings separate.
Environment Variables and Secrets
Never hardcode secrets like database passwords or API keys in the Compose file, even if it is in a private repository. Use environment variables with a .env file excluded from version control:
services:
db:
image: postgres:16.1
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
Create a .env file (ensure it is in .gitignore):
DB_PASSWORD=strongpassword123
For more sensitive data, use Docker secrets (in Swarm) or external secret managers. At minimum, avoid printing environment variables in logs:
docker compose exec db env
# This will output all environment variables including secrets. Avoid in production.
Resource Limits
Without resource limits, one container can exhaust host resources and affect other services. Define limits in Compose:
services:
web:
image: nginx:1.25
deploy:
resources:
limits:
cpus: '0.50'
memory: 256M
reservations:
cpus: '0.25'
memory: 128M
Note that deploy.resources is ignored by docker compose up and is only applied in Swarm mode (docker stack deploy). For non-Swarm Compose, use mem_limit and cpus keys (deprecated but still functional) or rely on external orchestration.
Verification and Diagnostics
After deployment, verify that the application is actually working, not just that containers are running.
Health Checks
Define health checks for long-running services so Docker can mark them unhealthy:
services:
web:
image: nginx:1.25
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
Check status:
docker compose ps
# NAME STATUS
# myapp-web-1 Up 30 seconds (healthy)
If a container is unhealthy, inspect the health check output:
docker inspect --format '{{json .State.Health}}' myapp-web-1 | jq
Network Connectivity Testing
Containers on the same Compose network can reach each other by service name. Verify connectivity from within a container:
docker compose exec web curl http://db:5432
# Should return a response or connection refused if DB is not listening.
If the service name does not resolve, check that both containers are on the same network:
docker network inspect myapp_default
All services defined in the same Compose file are automatically on the same default network unless specified otherwise.
Log Analysis
Use logs to diagnose issues. For a specific service:
docker compose logs db --since 10m
For troubleshooting, you can attach to a container:
docker compose exec web sh
Inside, run diagnostic commands like curl, ping, or env (but avoid exposing secrets).
Failure Modes and Recovery
Mistakes happen; plan for recovery before you need it.
Configuration Rollback
If a new deployment introduces a problem, the quickest rollback is to revert to the previous Compose file version and redeploy:
# Assuming you have the previous version in git
git checkout <previous-commit> -- docker-compose.yml docker-compose.prod.yml
docker compose up -d --force-recreate
Alternatively, if using tagged images, switch back to the previous image tag:
services:
web:
image: nginx:1.24 # previously working version
Then run docker compose up -d.
Data Recovery
If data is lost or corrupted, restore from backups. Ensure you have automated backups for volumes. For a named volume, you can back it up using a temporary container:
docker run --rm -v db_data:/data -v $(pwd):/backup alpine tar czf /backup/db_data_backup.tar.gz -C /data .
Restore:
docker run --rm -v db_data:/data -v $(pwd):/backup alpine sh -c "cd /data && tar xzf /backup/db_data_backup.tar.gz"
Test restores regularly.
Common Pitfalls and How to Avoid Them
Below are frequent Docker Compose production mistakes, their causes, and recovery strategies.
| Mistake | Why It Happens | How to Avoid or Recover |
|---|---|---|
Using latest image tag in production | Assumes latest is stable; can introduce breaking changes unexpectedly | Pin image tags to specific versions or digests; use docker compose pull only after testing |
| Exposing debug ports (e.g., 5432 for PostgreSQL) to the internet | Misconfigured ports mapping | Bind ports to localhost only: 127.0.0.1:5432:5432 |
Storing secrets in plain text in Compose file or .env committed to git | Convenience, lack of secret management | Use Docker secrets, external vaults, or at least .env with restricted permissions |
Running with privileged: true or unnecessary capabilities | Misunderstanding security requirements | Avoid privileged mode; add only required capabilities with cap_add |
Ignoring restart: always policy | Containers exit on error but are not restarted | Set appropriate restart policies: restart: unless-stopped for services that should always run |
| Not setting resource limits | One container can consume all CPU/memory | Define deploy.resources.limits (Swarm) or mem_limit/cpus (Compose) |
Using docker compose down without -v flag during debugging | down removes containers and networks but not volumes by default; manually removing volumes loses data | Always specify -v only when intending to delete volumes; backup first |
| Overwriting host files with bind mounts | Bind mount path mistakes can overwrite host directories | Use named volumes for persistence, bind mounts only for code/config that you intentionally share |
Operations Checklist
Use this checklist before and after every production deployment to catch common mistakes.
Before deployment:
- [ ] Verify Docker and Compose versions:
docker version && docker compose version - [ ] Run
docker compose configto validate the Compose file syntax and merge overrides - [ ] Check for hardcoded secrets; replace with environment variables or secrets
- [ ] Confirm image tags are pinned (not
latest) - [ ] Review volume mounts and ensure data persistence paths are correct
- [ ] Set resource limits for each service
- [ ] Define health checks for critical services
- [ ] Backup existing data (especially if modifying volumes)
- [ ] Have a rollback plan (previous image tag or Compose file version)
After deployment:
- [ ] Run
docker compose psand confirm all services are up and healthy - [ ] Check logs:
docker compose logs --tail=200 - [ ] Verify application functionality from an external endpoint (if applicable)
- [ ] Monitor resource usage:
docker stats - [ ] Confirm that no unexpected ports are exposed:
docker port <container> - [ ] Test a container restart to ensure data persistence:
docker compose restart <service> - [ ] Document any deviations and update runbooks
Ownership and Review Cadence
For each major decision (e.g., image tag promotion, secret rotation, resource limit changes), assign a single accountable owner. For example:
- Image tag promotion: Priya Shah, Engineering Lead - reviewed weekly
- Secret rotation: DevOps team member John Doe - reviewed monthly
- Resource limit adjustments: Maria Garcia, Infrastructure Engineer - reviewed quarterly
These owners are responsible for verifying the change and updating documentation.
Conclusion
Avoiding Docker Compose production configuration mistakes requires a disciplined approach: understand your environment, use safe configuration practices, verify rigorously, and plan for recovery. The commands and examples in this guide provide a starting point for building resilient deployments.
Start by applying one practice, such as pinning image tags or adding health checks, and observe the impact. Then expand to a full deployment checklist. Remember that production safety relies on observability, least privilege, and documented rollback procedures.
With these tools, you can deploy with confidence and recover quickly when things go wrong.