Skip to content

Docker Compose for Production Environments -- Multi-Service Orchestration, Networking, and Deployment

DodaTech Updated 2026-06-22 8 min read

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
â„šī¸ Info

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

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

  2. Using depends_on without condition: service_healthy: The default depends_on only 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.

  3. Exposing database ports to the internet: Services like PostgreSQL and Redis should not have ports: mappings unless absolutely necessary. Use a dedicated network with internal: true for backend services. This prevents external access to your database.

  4. Hard-coding secrets in the Compose file: Writing POSTGRES_PASSWORD=mypassword in docker-compose.yml commits the secret to version control. Use secrets: with external files or environment variables with .env files that are not committed.

  5. Forgetting to prune old containers and images: Long-running Docker hosts accumulate stopped containers, unused networks, and dangling images. Run docker system prune -f regularly (via cron or a scheduled task) to free disk space and prevent "no space left on device" errors.

Practice Questions

  1. What is the difference between ports and expose in a Compose service? Answer: ports publishes the container port to the host, making it accessible from outside the Docker host. expose documents the port without publishing it -- the port is accessible only to other services on the same Docker network. For backend services like databases, use expose only.

  2. How does Docker Compose handle service ordering at startup? Answer: The depends_on directive controls startup order. Without condition, it waits only for the container to start. With condition: 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> stop explicitly.

  3. What is the purpose of the deploy section in a Compose file? Answer: The deploy section 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.

  4. How do secrets work in Docker Compose? Answer: Secrets are defined under the top-level secrets key 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.

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