Docker Compose for Production Environments -- Multi-Service Orchestration, Networking, and Deployment
In this tutorial, you'll learn about Docker Compose for Production Environments. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Docker Compose defines multi-container applications in a single YAML file, managing services, networks, volumes, and dependencies through a declarative configuration that works for development, staging, and production environments with environment-specific overrides.
What You'll Learn
Why It Matters
Not every workload needs Kubernetes. For small teams, single-server deployments, Edge Computing, and local development, Docker Compose provides powerful Orchestration without the complexity of a full container platform. Production-ready Compose files include health checks, resource limits, restart policies, logging drivers, secrets management, and reverse proxy configuration -- patterns that transform Compose from a development tool into a production deployment platform.
Real-World Use
DodaTech runs the internal version of Durga Antivirus Pro's management console on a single VPS using Docker Compose with 7 services: Nginx reverse proxy with SSL termination, API server, worker, PostgreSQL, Redis, Prometheus, and Grafana. The setup has been running for 18 months with zero downtime, handling 50,000 requests per day.
flowchart TD
A["Internet"] --> B["Nginx Reverse Proxy:443"]
B --> C["API Service:3000"]
C --> D["Worker Service"]
C --> E["PostgreSQL:5432"]
C --> F["Redis:6379"]
G["Prometheus"] --> C
G --> E
G --> F
H["Grafana:3000"] --> G
I["Certbot"] --> B
subgraph "Docker Compose: prod.yml"
B
C
D
E
F
G
H
end
style B fill:#269539,color:#fff
style C fill:#326CE5,color:#fff
Prerequisites: Basic Docker and Docker Compose knowledge, a Linux server with Docker Engine and Compose plugin installed, and basic networking understanding.
Production-Grade Compose File
# docker-compose.yml
version: "3.9"
services:
nginx:
image: nginx:1.25-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/sites:/etc/nginx/conf.d:ro
- certbot-www:/var/www/certbot
- certbot-certs:/etc/letsencrypt
depends_on:
api:
condition: service_started
networks:
- frontend
- backend
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 10s
retries: 3
api:
build:
context: ./api
dockerfile: Dockerfile.prod
image: dodatech/api:latest
restart: unless-stopped
expose:
- "3000"
environment:
- NODE_ENV=production
- DB_HOST=postgres
- REDIS_HOST=redis
- LOG_LEVEL=info
env_file:
- ./api/.env.production
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
deploy:
resources:
limits:
cpus: "1.0"
memory: "512M"
reservations:
cpus: "0.25"
memory: "128M"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
networks:
- backend
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
postgres:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
- ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
POSTGRES_DB: dodatech
POSTGRES_USER: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
deploy:
resources:
limits:
memory: "1G"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d dodatech"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
deploy:
resources:
limits:
memory: "256M"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- backend
worker:
build:
context: ./worker
dockerfile: Dockerfile
image: dodatech/worker:latest
restart: unless-stopped
environment:
- NODE_ENV=production
- REDIS_HOST=redis
env_file:
- ./worker/.env.production
depends_on:
redis:
condition: service_healthy
deploy:
replicas: 2
resources:
limits:
cpus: "0.5"
memory: "256M"
networks:
- backend
prometheus:
image: prom/prometheus:v2.51.0
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=30d"
deploy:
resources:
limits:
memory: "512M"
networks:
- backend
grafana:
image: grafana/grafana:10.4.0
restart: unless-stopped
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
- ./grafana/datasources:/etc/grafana/provisioning/datasources:ro
environment:
- GF_SECURITY_ADMIN_PASSWORD__FILE=/run/secrets/grafana_admin_password
- GF_INSTALL_PLUGINS=grafana-piechart-panel
secrets:
- grafana_admin_password
depends_on:
- prometheus
networks:
- backend
volumes:
postgres-data:
driver: local
redis-data:
driver: local
prometheus-data:
driver: local
grafana-data:
driver: local
certbot-www:
certbot-certs:
secrets:
db_password:
file: ./secrets/db_password.txt
grafana_admin_password:
file: ./secrets/grafana_admin.txt
networks:
frontend:
backend:
internal: true
Expected behavior: The API service depends on PostgreSQL and Redis being healthy (not just started). The internal: true network for backend means database and cache services are not accessible from outside the Docker host. Secrets are mounted as files at /run/secrets/<name> inside containers. Resource limits prevent one service from starving others. Health checks ensure the orchestrator detects and restarts unhealthy containers.
Environment-Specific Overrides
# docker-compose.override.yml (development)
version: "3.9"
services:
api:
build:
dockerfile: Dockerfile.dev
volumes:
- ./api/src:/app/src:ro
environment:
- NODE_ENV=development
- LOG_LEVEL=debug
ports:
- "9229:9229" # debugger
postgres:
ports:
- "5432:5432"
redis:
ports:
- "6379:6379"
# docker-compose.prod.yml (production overrides)
version: "3.9"
services:
nginx:
ports:
- "80:80"
- "443:443"
api:
deploy:
replicas: 3
environment:
- NODE_ENV=production
worker:
deploy:
replicas: 5
# Deploy production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Expected output:
# [+] Running 8/8
# - Container nginx Started
# - Container postgres Healthy
# - Container redis Healthy
# - Container api Started
# - Container worker Started
# - Container prometheus Started
# - Container grafana Started
# Scale a service
docker compose up -d --scale worker=10
# View resource usage
docker stats
Logging and Log Management
# docker-compose.yml (logging section)
services:
api:
logging:
driver: "fluentd"
options:
fluentd-address: "localhost:24224"
tag: "dodatech.api"
nginx:
logging:
driver: "fluentd"
options:
fluentd-address: "localhost:24224"
tag: "dodatech.nginx"
Common Errors
Not setting restart policies: Without
restart: unless-stopped, a container that crashes stays down until manually restarted. In production, every service should have a restart policy unless you explicitly want to inspect the failure.Using
depends_onwithoutcondition: service_healthy: The defaultdepends_ononly waits for the container to start, not for the service inside to be ready. PostgreSQL starts in under a second but takes 5-10 seconds to accept connections. Without health check dependency, the API starts and immediately crashes because the database is not accepting connections.Exposing database ports to the internet: Services like PostgreSQL and Redis should not have
ports:mappings unless absolutely necessary. Use a dedicated network withinternal: truefor backend services. This prevents external access to your database.Hard-coding secrets in the Compose file: Writing
POSTGRES_PASSWORD=mypasswordindocker-compose.ymlcommits the secret to version control. Usesecrets:with external files or environment variables with.envfiles that are not committed.Forgetting to prune old containers and images: Long-running Docker hosts accumulate stopped containers, unused networks, and dangling images. Run
docker system prune -fregularly (via cron or a scheduled task) to free disk space and prevent "no space left on device" errors.
Practice Questions
What is the difference between
portsandexposein a Compose service? Answer:portspublishes the container port to the host, making it accessible from outside the Docker host.exposedocuments the port without publishing it -- the port is accessible only to other services on the same Docker network. For backend services like databases, useexposeonly.How does Docker Compose handle service ordering at startup? Answer: The
depends_ondirective controls startup order. Withoutcondition, it waits only for the container to start. Withcondition: service_healthy, it waits for the health check to pass. Compose does not wait for services to stop in reverse order -- use<a href="/devops/docker-compose/">Docker Compose</a> stopexplicitly.What is the purpose of the
deploysection in a Compose file? Answer: Thedeploysection configures resource limits (CPU, memory), replica count (when used with Docker Swarm), restart policies, and placement constraints. It is supported natively by Docker Swarm and is used as documentation for third-party orchestrators.How do secrets work in Docker Compose? Answer: Secrets are defined under the top-level
secretskey and mounted to/run/secrets/<name>inside the container. They can reference a file (file: ./secrets/password.txt) or an external secret store. The file on the host must be readable by the UID the Docker daemon runs as.
Challenge
Design a production-ready Docker Compose setup for a Laravel application: PHP-FPM service with Nginx, MySQL 8 with a persistent volume and health check, Redis for caching and sessions, a queue worker (Horizon), and cron service for scheduled tasks. Implement health checks for all services, resource limits, secrets for database credentials, a custom network topology (frontend network for Nginx, backend network for PHP/MySQL/Redis), and environment-specific override files for development (with Xdebug, mailhog, and exposed ports) and production (with scaled workers and logging to Fluentd).
Mini Project
Build a complete production deployment platform using Docker Compose: create a Compose file for a full-stack application with Nginx (reverse proxy + SSL termination with Let's Encrypt via certbot/caddy), a Node.js API with health checks and resource limits, PostgreSQL with automated backup script (scheduled via host cron), Redis for caching, a queue worker with replicated service, Prometheus for monitoring, Grafana with pre-configured dashboards, and Loki + Promtail for log aggregation. Implement secrets management with .env files and Docker secrets, configure logrotate for container logs on the host, set up Docker system pruning as a cron job, write a deployment script that pulls the latest images, runs migrations, and performs a zero-downtime restart using service scaling, and document the entire setup with a runbook for Incident Response.
Related Resources
| Resource | Description |
|---|---|
| Docker Basics | Container fundamentals |
| Docker Compose Basics | Foundational Compose patterns |
| Kubernetes vs Compose | When to use which orchestrator |
| Monitoring Tools | Production Observability |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro