A reference guide for production Docker Compose deployments. Covers networking, volumes, health checks, secrets management, resource limits, and multi-service design patterns.
4. Environment Variables & Secrets
8. Logging
9. Startup Order & Dependencies
Each Compose stack should define its own bridge network. Services within the same network can reach each other by container name. Services in different stacks are isolated by default.
services:
app:
networks:
- backend
db:
networks:
- backend
networks:
backend:
driver: bridgeWhen two stacks need to talk (e.g., an app stack reaching a shared database), use an external network:
# In the database stack
networks:
shared-db:
name: shared-db
driver: bridge
# In the app stack
networks:
shared-db:
external: trueOnly expose ports that external clients need. Internal service-to-service traffic should use the Docker network, not published ports.
services:
# Public-facing — expose port
nginx:
ports:
- "443:443"
# Internal only — no port mapping needed
api:
expose:
- "3000"Bind to localhost during development to avoid exposing services to your LAN:
ports:
- "127.0.0.1:5432:5432"| Type | Use Case | Example |
|---|---|---|
| Named volume | Database data, persistent state | db-data:/var/lib/postgresql/data |
| Bind mount | Config files, source code (dev only) | ./nginx.conf:/etc/nginx/nginx.conf:ro |
| tmpfs | Temporary files, caches | tmpfs: /tmp |
services:
postgres:
volumes:
# Named volume — Docker manages the directory
- pgdata:/var/lib/postgresql/data
# Bind mount — read-only config file
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
volumes:
pgdata:
driver: localMount config files as read-only (:ro) to prevent accidental writes:
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro# Backup a named volume to a tar archive
docker run --rm \
-v pgdata:/data:ro \
-v "$(pwd)":/backup \
alpine tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .Every long-running service should define a health check. This enables depends_on with condition: service_healthy and lets Docker report accurate status.
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30sFor services that don't have an HTTP endpoint:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres || exit 1"]
interval: 10s
timeout: 5s
retries: 5| Parameter | Purpose | Recommended |
|---|---|---|
interval | Time between checks | 10–30s |
timeout | Max time to wait for a check to respond | 5–10s |
retries | Failures before marking unhealthy | 3–5 |
start_period | Grace period for slow-starting services | 30–120s |
Docker Compose automatically loads a .env file from the project directory:
services:
app:
environment:
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}For production deployments on Docker Swarm, use secrets instead of environment variables:
services:
app:
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txtKeep different types of configuration separate:
.env # Shared defaults (ports, versions)
.env.local # Machine-specific overrides (not committed)
.env.production # Production values (committed, no real secrets)
Never commit real passwords or tokens. Use a secrets manager (Vault, AWS Secrets Manager) for production.
Always set memory and CPU limits in production to prevent a single container from consuming all host resources.
services:
elasticsearch:
deploy:
resources:
limits:
cpus: "2.0"
memory: 2g
reservations:
cpus: "0.5"
memory: 512mNote: deploy.resources works in both Swarm mode and standalone Compose (with Docker Compose v2+).
For Elasticsearch, Logstash, and other JVM services, set both the container memory limit and the JVM heap:
environment:
- ES_JAVA_OPTS=-Xms1g -Xmx1g # JVM heap
deploy:
resources:
limits:
memory: 2g # Container limit (heap + overhead)Rule of thumb: container limit = JVM heap × 2 (to allow for off-heap, GC, and OS overhead).
Run helper containers alongside your main service:
services:
app:
image: myapp:latest
# Log shipper sidecar
filebeat:
image: docker.elastic.co/beats/filebeat:8.13.4
volumes:
- app-logs:/var/log/app:ro
depends_on:
- appRun a one-shot container to prepare state before the main service starts:
services:
migrate:
image: myapp:latest
command: ["python", "manage.py", "migrate"]
depends_on:
db:
condition: service_healthy
app:
image: myapp:latest
depends_on:
migrate:
condition: service_completed_successfullyScale background workers independently from the web process:
services:
web:
image: myapp:latest
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]
worker:
image: myapp:latest
command: ["celery", "-A", "tasks", "worker", "--loglevel=info"]
deploy:
replicas: 3Traefik auto-discovers services via Docker labels. No manual Nginx config files needed.
services:
app:
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=3000"labels:
- "traefik.http.routers.api.rule=Host(`example.com`) && PathPrefix(`/api`)"
- "traefik.http.middlewares.api-strip.stripprefix.prefixes=/api"
- "traefik.http.routers.api.middlewares=api-strip"labels:
- "traefik.http.middlewares.rate-limit.ratelimit.average=100"
- "traefik.http.middlewares.rate-limit.ratelimit.burst=50"
- "traefik.http.routers.app.middlewares=rate-limit"Configure structured logging for easier parsing by ELK or Loki:
services:
app:
logging:
driver: json-file
options:
max-size: "10m" # Rotate after 10 MB
max-file: "3" # Keep 3 rotated files
tag: "{{.Name}}" # Tag with container namelogging:
driver: gelf
options:
gelf-address: "udp://localhost:12201"
tag: "myapp"depends_on:
- db
- redisThis only waits for the container to *start*, not for the service to be *ready*.
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthyThis is the correct approach — it waits until the health check passes before starting the dependent service.
depends_on:
migrate:
condition: service_completed_successfullyUseful for database migrations or seed scripts that must finish before the app starts.
Before deploying to production, verify each item:
start_period values.env or Docker secretsunless-stopped or always for every servicemax-size and max-file):latest) for reproducibility*Part of Docker Compose Templates — (c) 2026 Datanest Digital (datanest.dev)*
Production-ready Docker Compose stacks for web apps, monitoring, logging, databases, and CI runners.
Drop-in Compose files for the most common self-hosted infrastructure. Each stack is tested, commented, and ready to deploy with a single docker compose up -d. Includes a Traefik reverse proxy with automatic TLS and a stack manager script.
Get the full Docker Compose Templates and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.