Skip to content
Home
Docker Compose: Multi-Container App Orchestration Guide

Docker Compose: Multi-Container App Orchestration Guide

Developer Tools Developer Tools 7 min read 1421 words Beginner ExcellentWiki Editorial Team

Docker Compose lets you define and run multi-container Docker applications with a single command. Instead of remembering and typing multiple docker run commands every time you start your development environment, you declare all services, networks, volumes, and environment variables in a YAML file and launch everything with docker compose up. For development teams, Docker Compose solves the “it works on my machine” problem — every developer gets identical database, cache, API server, and frontend containers, eliminating environment discrepancies.

Installation and Setup

Docker Compose is included with Docker Desktop for macOS and Windows. On Linux, install the plugin:

sudo apt-get install docker-compose-plugin

Or manually install the latest version:

DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
curl -SL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o $DOCKER_CONFIG/cli-plugins/docker-compose
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose

Verify the installation with docker compose version.

Compose File Structure

A docker-compose.yml file defines the application stack in three top-level sections: services, networks, and volumes.

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: secret

volumes:
  pgdata:

Docker Compose creates a default network for the application, making services discoverable by their service names.

Service Configuration in Depth

Building Images versus Using Pre-Built Images

Each service either builds from a Dockerfile (using build: .) or uses a pre-built image (using image: postgres:16-alpine). The build context specifies the directory containing the Dockerfile. You can target specific multi-stage build stages:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
      target: development

Development builds typically mount the source code as a bind mount for hot reloading:

volumes:
  - .:/app
  - /app/node_modules  # Named volume for dependencies

The second volume (/app/node_modules) prevents the bind mount from overwriting the container’s node_modules directory — a common pitfall in Node.js development.

Dependency Management with depends_on

The depends_on directive controls service startup order. Basic depends_on only waits for the container to start, not for the service inside to be ready:

depends_on:
  - db

For production, use health check conditions to wait until the service is actually accepting connections:

depends_on:
  db:
    condition: service_healthy

This requires the depended-on service to define a health check.

Networking: Service Discovery and Isolation

Docker Compose creates a default bridge network for the application. Services communicate using their service name as the hostname — the app service connects to the database at db:5432.

Custom Networks

Define explicit networks for service isolation:

services:
  api:
    networks:
      - backend
  frontend:
    networks:
      - frontend
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

This pattern ensures the frontend cannot directly access the database — it only connects to the API service. The API service is accessible from both the frontend and backend networks.

Volumes: Persistent and Ephemeral Storage

Three types of volumes exist in Docker Compose:

Named volumes persist data across container restarts and are managed by Docker. They are ideal for database data:

volumes:
  pgdata:

services:
  db:
    volumes:
      - pgdata:/var/lib/postgresql/data

Bind mounts map a host directory into the container. They are ideal for development with hot reloading:

volumes:
  - .:/app
  - ./config:/etc/config:ro  # Read-only mount

tmpfs mounts store data in memory, discarded when the container stops. They are useful for sensitive data or cache:

tmpfs:
  - /tmp:size=100M

Environment Variables and Secrets Management

Environment Files

Use .env files for environment-specific configuration. Docker Compose automatically reads the .env file in the current directory:

services:
  app:
    env_file:
      - .env
      - .env.production  # Overrides .env values

Secrets

For sensitive data like database passwords and API keys, use Docker secrets:

services:
  app:
    secrets:
      - db_password

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

Secrets are mounted as files in /run/secrets/ inside the container, avoiding environment variable leaks in logs or process listings.

Health Checks and Restart Policies

Health checks ensure containers are truly ready before receiving traffic:

services:
  app:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Restart policies control behavior after failures:

PolicyBehavior
noNever restart (default)
alwaysAlways restart
unless-stoppedRestart unless manually stopped
on-failureRestart only on non-zero exit codes

Profiles: Selective Service Activation

Profiles enable and disable groups of services. Core services (app, db, cache) always start. Optional services start only when their profile is activated:

services:
  adminer:
    profiles:
      - tools
  prometheus:
    profiles:
      - monitoring
docker compose --profile tools up -d
docker compose --profile tools --profile monitoring up -d

Production Deployment Considerations

For production deployments, avoid bind mounts (they bypass image immutability). Pin specific image tags — never use :latest:

services:
  app:
    image: myapp:1.2.3
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: "512M"
    read_only: true
    user: "1000:1000"

Resource limits prevent runaway containers from starving other processes. Read-only filesystems improve security — processes cannot write to writable filesystem layers. Non-root users reduce the impact of container breakout vulnerabilities.

Advanced Compose Features

Extending Services with extends

The extends keyword allows sharing common configuration across services:

# base.yml
services:
  app-base:
    image: node:20-alpine
    working_dir: /app
    environment:
      NODE_ENV: production

# docker-compose.yml
services:
  web:
    extends:
      file: base.yml
      service: app-base
    ports:
      - "3000:3000"
    command: npm start

  worker:
    extends:
      file: base.yml
      service: app-base
    command: npm run worker

This pattern eliminates duplication when multiple services share the same base configuration.

Watch Mode for Development

Docker Compose Watch automatically rebuilds services when source files change:

services:
  app:
    build: .
    develop:
      watch:
        - path: ./src
          action: rebuild
        - path: ./config
          action: sync
        - path: ./package.json
          action: rebuild

Run docker compose watch to enable automatic rebuilds — more efficient than bind mounts for languages that require compilation.

Init Containers

Use init containers for setup tasks that must complete before the main service starts:

services:
  app:
    image: myapp
    depends_on:
      db-migrate:
        condition: service_completed_successfully

  db-migrate:
    image: myapp
    command: npx prisma migrate deploy

Init containers run to completion before the dependent service starts, ensuring the database schema is ready.

Frequently Asked Questions

How is Docker Compose different from Kubernetes?

Docker Compose is designed for single-host deployments — development environments, CI runners, and small-scale production. Kubernetes manages multi-host clusters with auto-scaling, self-healing, rolling updates, and service discovery. Compose is simpler and faster for local development; Kubernetes handles production at scale.

Can I use Docker Compose in production?

Yes, Docker Compose is suitable for small to medium production deployments (single host or small cluster). Docker Swarm mode extends Compose files to multi-host deployments. For large-scale production, consider Kubernetes for its advanced scheduling, auto-scaling, and self-healing capabilities.

How do I debug a service that fails to start?

Check logs with docker compose logs service-name. Verify the health check configuration — start_period gives services time to initialize before health checks begin. Use docker compose exec service-name sh to inspect the running container. Check network connectivity with docker compose exec app ping db.

What is the difference between docker compose down -v and docker compose down?

docker compose down stops and removes containers and networks but preserves volumes. docker compose down -v also removes named volumes, destroying all persisted data. Use the latter carefully — it erases databases, caches, and any other volume-stored data.

How do I scale a service with Docker Compose?

Use the --scale flag: docker compose up -d --scale worker=3. This starts three instances of the worker service. Each instance is load-balanced by Docker’s DNS round-robin. For proper load balancing, consider adding a reverse proxy like Nginx or Traefik.

Docker Compose vs Docker Swarm vs Kubernetes

Docker Compose is designed for single-host deployments and development environments. Docker Swarm mode extends Compose files to multi-host deployments with built-in load balancing and rolling updates. Kubernetes provides the most comprehensive orchestration features but has a steeper learning curve. For teams starting with containerization, the recommended progression is: Docker Compose (development), Docker Swarm (simple production), Kubernetes (complex production at scale).

Troubleshooting Common Issues

Port conflicts: If a service fails to start with “port already in use,” check the container logs with docker compose logs service-name. Use docker compose down followed by docker compose up -d to ensure clean port release. On Linux, sudo lsof -i :port identifies the process using the port.

Volume permission issues: Containers often run as root while the host user owns mounted files. Use user: "${UID}:${GID}" in the service definition. Export UID and GID in your shell profile: export UID GID.

DNS resolution: Services should resolve each other by service name. If not, verify custom network configuration and check that depends_on is configured correctly. Use docker compose exec app ping db to test connectivity.

Build cache invalidation: If changes to Dockerfile or source files do not trigger rebuilds, use docker compose build --no-cache for a clean build.

Related Articles

Section: Developer Tools 1421 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top