Most tutorials show you how to get Docker Compose running. Almost none of them show you how to run it safely in production. Here are 12 things the tutorials skip — copy-paste ready.
1. Always Set restart: unless-stopped
Without a restart policy, containers that crash (or the host that reboots) stay down forever.
services:
app:
image: myapp:latest
restart: unless-stopped # Also: always, on-failure
Use unless-stopped for most services. Use on-failure for workers that should restart on crashes but not on a clean docker compose stop.
2. Add Healthchecks to Every Service
Without healthchecks, depends_on only waits for the container to start — not for it to be ready to accept traffic.
services:
postgres:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
app:
image: myapp:latest
depends_on:
postgres:
condition: service_healthy # Wait for DB to be ready
3. Set Resource Limits
Containers without limits can eat all host memory, crashing every other service on the instance.
services:
worker:
image: myworker:latest
deploy:
resources:
limits:
cpus: "1.5"
memory: 1G
reservations:
cpus: "0.25"
memory: 256M
Start with limits.memory = 2x your expected usage. Monitor for a week, then tighten. VeloxaHost's live resource graphs make this easy.
4. Use Docker Secrets (Not Environment Variables) for Passwords
Environment variables are visible in docker inspect, process lists, and crash reports. Docker secrets mount as files — much safer.
# Define secrets at the top level
secrets:
db_password:
file: ./secrets/db_password.txt # chmod 600
services:
app:
image: myapp:latest
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password # App reads the file
5. Configure Log Rotation
The default Docker logging driver writes unbounded JSON files. On a busy server, this fills your disk in days.
services:
app:
image: myapp:latest
logging:
driver: json-file
options:
max-size: "50m" # Rotate when file hits 50MB
max-file: "5" # Keep 5 rotated files max
Or set it globally in /etc/docker/daemon.json for all containers:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "5"
}
}
6. Use Named Networks (Don't Use the Default)
The default Compose network exposes all containers to each other. Use separate networks to segment services — your DB shouldn't be reachable by your frontend container directly.
services:
nginx:
networks: [frontend]
app:
networks: [frontend, backend]
postgres:
networks: [backend] # Not reachable from nginx
networks:
frontend:
backend:
internal: true # No outbound internet access from backend
7. Separate .env Files per Environment
├── .env # Local dev defaults (commit to git)
├── .env.production # Production overrides (NEVER commit)
├── .env.staging # Staging overrides
# Use a specific .env file
docker compose --env-file .env.production up -d
Add .env.production and .env.staging to .gitignore.
8. Name Your Volumes Explicitly
Anonymous volumes (just specifying a path) get deleted on docker compose down -v. Named volumes persist unless explicitly removed.
services:
postgres:
volumes:
- postgres_data:/var/lib/postgresql/data # Named volume — safe
volumes:
postgres_data: # Declared explicitly
9. Set stop_grace_period
By default, Docker gives a container 10 seconds to shut down gracefully, then sends SIGKILL. For databases or message queues, this is often too short.
services:
worker:
image: myworker:latest
stop_grace_period: 60s # Give workers 60s to finish current jobs
app:
stop_grace_period: 30s # Give the app 30s to drain connections
10. Use Profiles for Optional Services
Not every service needs to run all the time. Put infrequently used services (backups, migrations, debug tools) behind profiles.
services:
migrate:
image: myapp:latest
profiles: ["migrate"]
command: python manage.py migrate
backup:
image: postgres:16-alpine
profiles: ["backup"]
command: /backup.sh
# Run only when needed
docker compose --profile migrate up migrate
docker compose --profile backup up backup
11. Use YAML Anchors for DRY Configs
Stop copy-pasting the same environment block across services:
x-common-env: &common-env
DATABASE_URL: postgresql://...
REDIS_URL: redis://...
APP_ENV: production
services:
app:
environment: *common-env # Inherits all of x-common-env
worker:
environment:
<<: *common-env # Merge and add extras
WORKER_CONCURRENCY: "4"
12. Automate Image Updates (With Care)
For non-critical services, Watchtower can automatically pull and restart containers when new images are available:
services:
watchtower:
image: containrrr/watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --schedule "0 0 4 * * *" --cleanup # Daily at 4am
Don't use Watchtower for databases or stateful services — auto-updating those is risky. Pin their image tags explicitly (postgres:16.2-alpine not postgres:latest).
Deploy your production Docker stacks on VeloxaHost — reliable networking, persistent volumes, and monitoring included. Start free →