Docker Compose: Orchestrating Multi-Container Applications

Define and run multi-container Docker applications using Docker Compose. From local development environments to complex microservice topologies.

published: reading time: 25 min read author: GeekWorkBench

Docker Compose: Orchestrating Multi-Container Applications

Docker Compose turns a multi-container application into a single deployable unit. Instead of running a dozen docker commands to start your stack, you write a YAML file describing your services, networks, and volumes, then run docker-compose up.

This tutorial covers Docker Compose from basics to advanced usage: service definitions, networking, environment variables, scaling, and production considerations.

Introduction

Docker Compose turns a multi-container application into a single deployable unit. Instead of running a dozen docker commands to start your stack, you write a YAML file describing your services, networks, and volumes, then run docker-compose up. It works well for local development — spinning up a database, cache, backend, and frontend without installing each dependency separately. CI/CD testing, single-host deploys, and prototyping are also natural fits. Beyond that scale, you start hitting limits that Kubernetes handles better.

Compose handles the orchestration problem for one host. You declare how containers communicate, which ports to expose, what environment variables are needed, and where data persists. Compose creates the networks, resolves DNS between services, and starts everything in the right order with health checks. Your whole stack configuration lives in version control — the difference between “works on my machine” and “here is exactly what we run” is a properly structured docker-compose.yml.

This tutorial walks through Docker Compose from the basics to advanced usage: service definitions, networking models, environment variable handling, scaling behavior, and production concerns including security checklists and failure scenarios. By the end you will know when Compose is the right tool and when to move up to something more complex.

When to Use Docker Compose

Docker Compose excels in specific scenarios but has natural limits. Understanding when to use each tool helps you architect appropriately for your scale.

Use Docker Compose when:

  • Local development — spinning up the full stack on your machine with hot reload and debugging
  • CI/CD testing — running integration tests in isolated containers without external dependencies
  • Single-host deployment — deploying a complete stack to a single VM or dedicated server
  • Prototyping — rapidly iterating on architecture without infrastructure complexity
  • Small to medium workloads — managing under 20 containers with straightforward networking

Signs you are outgrowing Compose:

  • Horizontal scaling limits — docker-compose up --scale does not provide automatic load balancing or self-healing
  • Multi-host networking — Compose networking is host-local; communication between hosts requires additional tooling
  • Rolling updates — manual image updates with downtime or complex blue-green scripts
  • Service discovery at scale — DNS-based discovery works for handfuls of services, not hundreds
  • Resource isolation — no built-in CPU/memory limits across the entire stack, only per-container

Graduate to Kubernetes when:

  • Multiple nodes required — workloads exceed what a single host can handle or you need high availability
  • Enterprise compliance — role-based access control, network policies, and audit logs are requirements
  • Complex CI/CD pipelines — automated rollouts, rollbacks, and canary deployments are daily operations
  • Service mesh needs — traffic management, circuit breaking, and observability beyond basic health checks
  • Multi-environment consistency — same manifests across dev, staging, production with different configurations

Compose File Structure

A docker-compose.yml file describes your entire application stack:

version: "3.8"

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: user
      POSTGRES_PASSWORD: secret

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

networks:
  default:
    driver: bridge

The version field specifies the Compose file format version. 3.8 is current for most use cases.

Service Dependency Topology

The example stack from the YAML above has the following dependency graph:

graph LR
    User([Browser]) --> Web[Web Service<br/>:3000]
    Web --> API[API Service<br/>:4000]
    API --> DB[(PostgreSQL<br/>:5432)]
    API --> Redis[(Redis<br/>:6379)]

Each arrow represents a network connection. Docker Compose automatically creates a shared bridge network where services can reach each other by service name.

Service Definitions and Dependencies

Each entry under services is a container. Docker Compose builds or pulls the image, then starts the container with the specified configuration.

Building from Dockerfile

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        NODE_ENV: production
    image: myapp:latest

The build instruction tells Compose to build the image from a Dockerfile. The image instruction names the resulting image. If you omit image, Compose names it projectname_web.

Using Pre-Built Images

services:
  db:
    image: postgres:15-alpine

Docker pulls the image if not present locally.

depends_on

The depends_on directive ensures services start in the correct order:

services:
  web:
    build: .
    depends_on:
      - db
      - redis

  api:
    build: ./api
    depends_on:
      - db

  db:
    image: postgres:15-alpine

Compose starts db first, then web and api in parallel. The directive does not wait for the database to be ready, only for the container to start. For databases and similar services, implement application-level retry logic or use healthcheck with condition.

Health Checks and Conditions

services:
  db:
    image: postgres:15-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app"]
      interval: 5s
      timeout: 3s
      retries: 5

  web:
    build: .
    depends_on:
      db:
        condition: service_healthy

Now web waits for db to be healthy, not just started.

Networking with Compose

Compose creates a default network for your stack. All services join this network and can reach each other by service name.

Automatic DNS Resolution

services:
  web:
    build: .
    environment:
      - DATABASE_URL=postgres://db:5432/app

  db:
    image: postgres:15-alpine

The web service can reach db at postgres://db:5432/app. Docker embedded a DNS resolver that resolves service names to container IPs.

Custom Networks

For more control, define explicit networks:

services:
  frontend:
    build: ./frontend
    networks:
      - frontend_net

  backend:
    build: ./backend
    networks:
      - frontend_net
      - backend_net

  db:
    image: postgres:15-alpine
    networks:
      - backend_net

networks:
  frontend_net:
    driver: bridge
  backend_net:
    driver: bridge

The frontend can reach the backend, and the backend can reach the database. The frontend cannot reach the database directly. This segmentation adds security.

External Networks

Use an existing network instead of creating one:

networks:
  default:
    external: true
    name: my_pre-existing_network

Environment Variables and Secrets

Compose provides several ways to inject configuration into services.

Basic Environment Variables

services:
  web:
    environment:
      - NODE_ENV=production
      - API_KEY=secret123
      - DEBUG=false

Environment from .env File

Create a .env file in the same directory as docker-compose.yml:

NODE_ENV=production
API_KEY=secret123
DATABASE_URL=postgres://user:pass@db:5432/app

Reference variables in docker-compose.yml:

services:
  web:
    environment:
      - NODE_ENV=${NODE_ENV}
      - API_KEY=${API_KEY}
      - DATABASE_URL=${DATABASE_URL}

Secrets in Docker Swarm

For production secrets, use Docker secrets (requires Swarm mode):

services:
  db:
    image: postgres:15-alpine
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

The secret file content gets mounted at /run/secrets/db_password inside the container. The file never appears in environment variables.

Security Note

Never commit .env files with real secrets to version control. Add them to .gitignore. For CI/CD, inject secrets from your pipeline’s secret management system.

Development vs Production Workflows

Compose files often differ between development and production.

Development Compose File

# docker-compose.yml
services:
  web:
    build: .
    volumes:
      - ./src:/app/src:ro # Hot reload
    environment:
      - NODE_ENV=development
    ports:
      - "3000:3000"
      - "9229:9229" # Debug port

Mounting source code as a volume enables hot reload. Changes on your host appear immediately inside the container.

Production Compose File

# docker-compose.prod.yml
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.prod
    environment:
      - NODE_ENV=production
    ports:
      - "3000:3000"
    restart: unless-stopped
    healthcheck:
      test:
        [
          "CMD",
          "wget",
          "--no-verbose",
          "--tries=1",
          "--spider",
          "http://localhost:3000/health",
        ]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 10s

No volumes for source code (code is baked into the image). Explicit health checks. Restart policy.

Using Multiple Compose Files

# Start with both files (base + override)
docker-compose up -d

# Use production file instead of development
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

The prod file overrides settings from the base file. Build context, environment variables, and volumes get replaced.

Scaling Services

Compose can run multiple replicas of a service:

docker-compose up -d --scale web=3

This runs 3 instances of the web service. However, port mapping becomes tricky with multiple replicas since you cannot map the same host port to multiple containers.

Scaling with a Load Balancer

For actual load balancing, use a tool like nginx or Traefik in front of your scaled services:

services:
  web:
    build: .
    scale: 3

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - web

Each web container gets a unique name (web_1, web_2, web_3). Configure nginx to balance across web_1, web_2, web_3.

Comparison: Docker Compose vs Kubernetes

Compose is excellent for local development and single-host deployment. For production at scale, Kubernetes handles automatic load balancing, rolling updates, and self-healing. The concepts translate, but the tooling differs significantly.

Common Commands

Starting Your Stack

# Start all services
docker-compose up -d

# Start and rebuild if images are outdated
docker-compose up -d --build

# Start specific services
docker-compose up -d web db

# Start with override file
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

Viewing Logs

# Follow logs from all services
docker-compose logs -f

# Follow logs from specific service
docker-compose logs -f web

# Tail logs with timestamps
docker-compose logs -f --tail=100 --timestamps web

Checking Status

# List running services
docker-compose ps

# List images used by services
docker-compose images

# Inspect service configuration
docker-compose config

Stopping and Cleaning Up

# Stop services (containers remain)
docker-compose stop

# Stop and remove containers
docker-compose down

# Stop and remove containers and volumes
docker-compose down -v

# Stop and remove everything including images
docker-compose down --rmi local

Executing Commands in Services

# Run a command in a service
docker-compose exec web node --version

# Run with interactive shell
docker-compose exec web sh

# Run database migrations
docker-compose exec api npm run migrate

Building Multi-Architecture Images

For services that need to run on different architectures:

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    platforms:
      - linux/amd64
      - linux/arm64

Use Docker buildx for the actual build:

docker buildx bake -f docker-compose.yml
docker buildx push --platforms linux/amd64,linux/arm64 myapp:latest

Extending Compose Files

Compose supports extending services from other files:

# docker-compose.yml (base)
services:
  web:
    build: .
    environment:
      - NODE_ENV=${NODE_ENV}

# docker-compose.dev.yml (extends base)
services:
  web:
    volumes:
      - ./src:/app/src:ro
    ports:
      - "3000:3000"

Run with:

docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

The dev file adds volumes and ports to the base service without duplicating the build configuration.

Troubleshooting

Service Fails to Start

# Check logs first
docker-compose logs web

# Verify service configuration
docker-compose config

# Recreate containers
docker-compose up -d --force-recreate

Port Conflicts

# Check what is using the port
ss -tlnp | grep 3000

# Find containers using the port
docker-compose ps -a | grep 3000

Volume Permission Issues

Containers often run as non-root. If your application cannot read a mounted volume:

# Check current ownership
ls -la ./data

# Fix ownership in a temporary container
docker-compose run --rm alpine chown -R 1001:1001 /data

DNS Resolution Failures

# Check if container can resolve names
docker-compose exec web ping api

# Check DNS configuration
docker-compose exec web cat /etc/resolv.conf

# Restart the network
docker-compose down
docker-compose up -d

Production Failure Scenarios

Compose stacks fail in ways that are not always obvious. Here are the most common issues.

Volume Permission Issues After Image Update

When you update an image, the UID/GID the container runs as may change. If the volume has data owned by the old UID, the new container cannot read or write.

Symptoms: “Permission denied” errors immediately after docker-compose pull.

Diagnosis:

# Check container user
docker-compose exec web id

# Check volume ownership
ls -la ./data

Mitigation: Always test image updates in staging. Use a named volume instead of a bind mount for application data so ownership is managed by Docker.

Circular Dependency Deadlock

If service A depends on B and B depends on A, Compose may hang at startup.

Symptoms: docker-compose up hangs, services never start, logs show services waiting on each other.

Diagnosis:

# Check your depends_on configuration
docker-compose config | grep -A5 depends_on

Mitigation: Review depends_on configuration. Use condition: service_healthy with health checks instead of simple dependency ordering.

Secrets File Missing on First Start

If a service requires a secret file that does not exist when Compose starts, the service fails.

Symptoms: ERROR: file not found at startup, even though the file was supposed to be created by another service.

Diagnosis:

# Check if secret file exists
ls -la ./secrets/db-password.txt

Mitigation: Use docker-compose up --exit-code-from to capture failures, or add a startup health check that validates dependencies exist before starting dependent services.

Capacity Estimation

How many containers can a single host support? This depends on CPU, memory, and network capacity, not just Docker.

Rough guidelines for a typical host (4 cores, 8GB RAM):

Container TypeContainers per Host
Lightweight (nginx, redis)50-100
Medium (Node.js, Python)10-30
Heavy (JVM, databases)3-8

These are soft limits. The real constraint is your application resource usage. Monitor actual consumption with docker stats.

Memory estimation:

# Check container memory usage
docker stats --no-stream

# Each Node.js container ~100-300MB
# Each Python worker ~200-500MB
# Each PostgreSQL ~500MB-2GB

Observability

Logging Configuration

Configure Compose to send logs to a centralized system:

services:
  web:
    image: myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

For production, use the syslog or fluentd logging driver to send logs to a central log aggregator.

Centralized Logging Example

services:
  web:
    image: myapp:latest
    logging:
      driver: fluentd
      options:
        fluentd-address: localhost:24224
        tag: web

Health Checks for Services

Add health checks to ensure services are genuinely ready:

services:
  api:
    image: myapi:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 10s

Use condition: service_healthy in depends_on to ensure services only start after their dependencies are genuinely ready.

Security Checklist

Use this checklist when deploying Docker Compose to production:

  • Store secrets in environment variables or Docker secrets, not in YAML files committed to version control
  • Use specific image tags, not latest — pins versions for reproducibility
  • Scan images for vulnerabilities before deployment (trivy image)
  • Run services as non-root user (user: "1001:1001")
  • Use read-only root filesystems where possible (read_only: true)
  • Limit container capabilities (cap_drop: ALL)
  • Separate services by trust boundary — do not put untrusted services in the same Compose stack
  • Rotate secrets regularly — implement secret rotation procedures
  • Use TLS for any internal service-to-service communication
  • Set resource limits (memory, CPU) to prevent one service from starving others

Trade-off Analysis

Tool Selection Criteria

FactorDocker ComposeKubernetes
ScopeSingle hostMulti-node cluster
ScalingManual (--scale), no auto-balancingHorizontalPodAutoscaler with auto-balancing
Service discoveryDNS via bridge networkDNS via Services with multiple discovery options
Rolling updatesManual with downtime or blue-greenBuilt-in with zero-downtime via Deployment
Self-healingContainer restart onlyAutomatic pod rescheduling, node replacement
SecretsDocker secrets (Swarm mode)Kubernetes Secrets with encryption at rest
Load balancingManual (nginx sidecar)Built-in via Service type LoadBalancer
Learning curveGentleSteeper
ComplexityLow (single file)High (YAML, Helm, operators)
Best forLocal dev, CI/CD, single-host prodMulti-node production, enterprise scale

Compose wins: Teams just starting with containers, local development workflows, CI/CD pipelines, single-server deployments, and prototyping. The mental model is simple and the configuration is straightforward.

Kubernetes wins: Production at scale requiring multi-node scheduling, automatic failover, sophisticated traffic management, compliance requirements (RBAC, audit logs, network policies), and teams with dedicated DevOps capacity.

Interview Questions

1. What is the difference between `depends_on` and `condition: service_healthy` in Docker Compose?

`depends_on` only waits for the container to start, not for the service inside to be ready. A database container might be running but still initializing. `condition: service_healthy` uses a health check you define to wait until the service is actually ready to accept connections before starting dependent services. This prevents race conditions where your API tries to connect to the database before it is ready.

2. How do you manage secrets in Docker Compose for production environments?

For Swarm mode, use Docker secrets: define secrets in the compose file, reference them in services, and access them at `/run/secrets/secret_name` inside containers. Never put real secrets in environment variables or the compose file itself — they end up in image layers and version control. For non-Swarm setups, use environment files (`.env`) that are gitignored, or inject secrets from an external secrets manager at runtime via environment variable substitution from a secrets vault.

3. How does Docker Compose networking work, and how do you isolate services?

Compose creates a default bridge network for your stack. All services on the default network can reach each other by service name. To isolate services, create custom networks and assign services to specific networks. Services on different networks cannot communicate unless you explicitly connect them. This lets you create security boundaries — for example, a database-only network that the frontend cannot reach directly, only the backend service can.

4. What is the difference between `docker-compose up` and `docker-compose up -d`?

`docker-compose up` runs services in the foreground, interleaving logs from all services in your terminal. This is useful for development. `docker-compose up -d` runs services in the background (detached mode), returning control to your terminal immediately. In detached mode, you use `docker-compose logs -f` to follow logs and `docker-compose ps` to check status.

5. How do you scale a service in Docker Compose, and what are the limitations?

Use `docker-compose up -d --scale web=3` to run 3 replicas of the web service. However, port mapping becomes problematic with scaling — you cannot map the same host port to multiple containers. So scaling only works for services that do not expose ports directly or when you use a load balancer in front of the scaled services. For true load balancing and auto-scaling, you need Kubernetes. Compose scaling is useful for workers and background tasks that do not need port exposure.

6. How do you handle environment-specific configuration in Docker Compose?

Use multiple compose files with the `-f` flag to layer configuration. Your base `docker-compose.yml` has common settings. Override with `docker-compose.prod.yml` for production-specific values like image tags, restart policies, and resource limits. At runtime: `docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d`. The later file overrides the earlier one. This keeps environments reproducible and avoids if/else logic in YAML.

7. What are the common causes of "Permission denied" errors when using volumes in Compose?

When you update an image, the UID/GID the container runs as may change. If a named volume has data owned by the old UID, the new container (running as a different user) cannot read or write. This happens commonly when switching between images or updating base images. Fix by either recreating the volume (`docker-compose down -v` to delete volumes), using a init container to fix ownership, or ensuring your image runs as the same UID consistently. For application data, named volumes are better than bind mounts because Docker manages ownership.

8. How do you troubleshoot DNS resolution failures between services in Compose?

First, verify the service name resolves: `docker-compose exec web ping api`. Check the DNS configuration inside the container: `docker-compose exec web cat /etc/resolv.conf`. If DNS is working but resolution fails, the service name might have a typo or the target service might not be on the same network. Use `docker-compose exec web nslookup api` for more detail. Restarting the network (`docker-compose down && docker-compose up -d`) often clears stale DNS cache entries.

9. When should you migrate from Docker Compose to Kubernetes?

Signs you are outgrowing Compose: you need to run on multiple nodes (Compose is single-host), you need automatic load balancing and self-healing beyond container restart, your rolling update process requires complex scripts, you need multi-environment consistency with different configs, or your service count exceeds what you can manage with manual networking. The migration is not trivial — Compose concepts translate but the tooling differs significantly. Start Kubernetes migration when the pain of managing Compose outweighs the learning investment of Kubernetes.

10. What is the difference between bind mounts and named volumes in Docker Compose?

Bind mounts map a host directory into the container — changes on host and container are immediate. Use for source code during development (hot reload). Named volumes are managed by Docker — data persists across container restarts and is isolated from host filesystem. Use for application data (databases, logs) where you want Docker to manage ownership and lifecycle. Bind mounts are simpler for development; named volumes are more reliable for persistent data in production.

11. How does Docker Compose handle service startup order with depends_on?

`depends_on` ensures services start in the correct order—`db` starts before `web` and `api`. However, `depends_on` only waits for the container to start, not for the service inside to be fully ready. If `db` takes 30 seconds to initialize the database, `web` will try to connect before the database is ready. Use `healthcheck` with `condition: service_healthy` to make dependent services wait until the service is genuinely ready to accept connections. This prevents application errors from race conditions at startup.

12. What is the difference between docker-compose up and docker-compose start?

`docker-compose up` creates containers if they do not exist, recreates them if they have changed (configuration drift), and starts them. It also builds images if `build:` is defined. `docker-compose start` only starts existing containers that were previously created but stopped—it does not create missing containers or rebuild images. Use `up` for initial startup and after configuration changes; use `start` for resuming stopped containers without rebuilding.

13. How do you persist data between container restarts in Docker Compose?

Use named volumes defined at the top level of the compose file under `volumes:`. Volumes persist data outside the container's filesystem—Container restarts do not affect volume data. For databases and application state, always use named volumes rather than bind mounts. For source code during development, bind mounts let you edit files on your host and see changes immediately inside the container. The `volumes:` section defines volume names; services mount them by name under their volume configuration.

14. What is the purpose of the version field in docker-compose.yml?

The `version` field specifies the Compose file format version. It affects which features and syntax Docker Compose supports. Version `3.8` is current for most use cases and supports all standard features. The version also affects the Compose file schema validation. While the version field is mostly historical now (Docker Compose ignores it for compatibility), it remains in files to clarify which format version is expected and to prevent breaking changes when older tools parse the file.

15. How do you share environment variables between services in Docker Compose?

Services reference shared environment variables from a `.env` file or the shell environment. The `.env` file in the same directory as `docker-compose.yml` is automatically loaded. Services access variables using `${VAR_NAME}` syntax in the compose file. For inter-service communication, use the `environment:` section in each service. For secrets, use Docker secrets (requires Swarm mode) or external secret injection. Note that environment variables are not encrypted—do not store real credentials in `.env` files committed to version control.

16. What is the difference between bridge, host, and overlay networks in Docker Compose?

`bridge` is the default network mode—Docker creates a virtual bridge network where containers can communicate by service name. `host` mode removes network isolation and attaches the container directly to the host's network stack (useful for performance-sensitive workloads). `overlay` networks connect containers across multiple Docker hosts (requires Swarm mode or Docker in swarm mode). For most Compose usage, the default bridge network works fine. Use custom bridge networks for service isolation by trust boundary—for example, a database-only network that frontend services cannot access directly.

17. How do you debug a service that fails to start in Docker Compose?

Start with `docker-compose logs ` to see what the service printed before failing. Check `docker-compose ps` to see the actual status of all services. Run `docker-compose config` to validate the compose file is parsed correctly. If the container keeps restarting, use `docker-compose exec sh` to get an interactive shell and inspect the environment. For network issues, test DNS resolution: `docker-compose exec web ping api`. For permission issues, check volume ownership with `ls -la` on the host and verify the container user with `docker-compose exec web id`.

18. How does Docker Compose handle service discovery?

Docker Compose creates a DNS resolver that maps service names to container IP addresses. When `web` reaches `postgres://db:5432/app`, Docker's embedded DNS resolves `db` to the IP address of the container running the `db` service. This only works within the same Docker network—services on different networks cannot resolve each other by name. For multi-host setups, use `overlay` networks (Swarm mode) or an external service discovery mechanism like Consul or etcd.

19. What is the difference between docker-compose down and docker-compose rm?

`docker-compose down` stops and removes containers, networks, and the default network. By default it removes named volumes too—use `-v` flag to control this. Named images are not removed. `docker-compose rm` only removes stopped containers, leaving networks and volumes intact. Use `down` for full cleanup between development sessions; use `rm` when you want to remove specific stopped containers without touching networks or volumes.

20. How do you configure resource limits (CPU and memory) for services in Docker Compose?

Use the `deploy.resources` section (Swarm mode) or runtime flags in recent Compose versions. For memory limits: `mem_limit: 512m`. For CPU limits: `cpus: "0.5"` (half a CPU core). In Compose file format 3.x, you can use `mem_limit` and `cpu_shares` directly on the service. For Swarm mode, use `deploy.resources.limits` and `deploy.resources.reservations`. Without limits, one service can starve others—for production, always set appropriate limits based on your application's expected resource consumption.

Further Reading

Docker Compose Patterns for Production

Health check orchestration: Compose can wait for services to be healthy before starting dependencies using condition: service_healthy. This works with custom health checks and is more reliable than simple depends_on ordering.

services:
  api:
    depends_on:
      db:
        condition: service_healthy
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app"]
      interval: 5s
      timeout: 3s
      retries: 5

Restart policies: Use restart: unless-stopped for production services that should automatically recover from crashes. Alternatives: always (restart always), on-failure (restart on non-zero exit), no (never restart).

Resource limits: Set memory and CPU limits per service to prevent one service from starving others. Docker Compose applies these via the container runtime.

services:
  api:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.5"
        reservations:
          memory: 256M
          cpus: "0.25"

Conclusion

Use this checklist when working with Docker Compose:

  • Define all services, networks, and volumes in a docker-compose.yml file
  • Use depends_on with condition: service_healthy for service startup ordering
  • Store secrets outside the YAML file — use .env files or Docker secrets
  • Set resource limits (memory, CPU) for each service
  • Add health checks to critical services
  • Use separate Compose files for dev/staging/prod (-f docker-compose.yml -f docker-compose.prod.yml)
  • Never commit secrets or environment files with credentials to version control
  • Test your Compose configuration with docker-compose config before running
  • Monitor container resource usage with docker stats
  • Log to stdout/stderr and let Docker handle log aggregation

Category

Related Posts

Multi-Stage Builds: Minimal Production Docker Images

Learn how multi-stage builds dramatically reduce image sizes by separating build-time and runtime dependencies, resulting in faster deployments and smaller attack surfaces.

#docker #devops #optimization

Docker Fundamentals: From Images to Production Containers

Master Docker containers, images, Dockerfiles, docker-compose, volumes, and networking. A comprehensive guide for developers getting started with containerization.

#docker #containers #devops

Container Images: Building, Optimizing, and Distributing

Learn how Docker container images work, layer caching strategies, image optimization techniques, and how to publish your own images to container registries.

#docker #containers #devops