Docker Compose — Multi-Container Applications

Beginner | Intermediate | Advanced | Docker Compose

1. What is Docker Compose and when should I use it?

Docker Compose lets you define a whole multi-container application — your API, its database, a cache, a queue — in a single declarative YAML file, then manage the entire stack with one command: docker compose up. It creates the network, resolves service names, wires volumes, and handles startup order for you.

Typical uses: local development environments, integration/system test environments, demos, and simple single-host deployments. It is not a cluster orchestrator — that is Kubernetes territory (see the Advanced page).

Command syntax note: Docker Compose v2 is a plugin invoked as docker compose (a space). The legacy v1 standalone binary was docker-compose (a hyphen). Use v2.

2. The anatomy of a docker-compose.yml

A complete example: a web API, a PostgreSQL database, and a Redis cache.

services:
  api:
    build:                          # build the image from a Dockerfile
      context: ./api
      dockerfile: Dockerfile
    image: myapp/api:dev            # tag given to the built image
    ports:
      - "8080:8000"                 # host:container
    environment:
      - DATABASE_URL=postgres://app:secret@db:5432/app
      - REDIS_URL=redis://cache:6379
    volumes:
      - ./api:/code                 # bind mount: live code for development
    depends_on:
      db:
        condition: service_healthy  # wait until db is really ready
      cache:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - db_data:/var/lib/postgresql/data   # named volume: persists data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10

  cache:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - cache_data:/data

volumes:
  db_data:
  cache_data:

Top-level keys: services (the containers), volumes (named volumes), networks (custom networks), and name (project name). Each service is roughly a docker run translated to YAML.

3. Building images vs. pulling images

4. Ports, volumes, and environment in Compose

5. Startup order: depends_on and healthchecks

depends_on controls start order, not readiness — Postgres being "started" does not mean it accepts connections yet. Combine healthchecks with conditions:

depends_on:
  db:
    condition: service_healthy    # wait for the healthcheck to pass
  cache:
    condition: service_started    # plain "it is running" is enough here

This is the Compose equivalent of waiting for dependencies — it removes most "app crashes at boot because the DB wasn't ready" flakiness.

6. Networking in Compose

services:
  api:
    networks: [frontend, backend]
  db:
    networks: [backend]           # not reachable from "frontend" services

networks:
  frontend:
  backend:
    internal: true                # no outbound internet access
  • external: true networks connect Compose to pre-existing Docker networks (e.g., another project's network).
  • 7. The essential commands

    docker compose up -d            # create + start everything, detached
    docker compose up -d api        # start just one service (and its deps)
    docker compose up --build       # rebuild images first
    docker compose ps               # stack status
    docker compose logs -f api      # follow one service's logs
    docker compose logs -f          # follow everything, interleaved
    docker compose exec api sh      # shell inside a running service
    docker compose run --rm api pytest   # one-off command (new container)
    docker compose stop             # stop without removing
    docker compose start            # resume
    docker compose restart api
    docker compose down             # stop + remove containers and networks
    docker compose down -v          # ... and volumes (DATA LOSS for db_data!)
    docker compose config           # validate + view the merged configuration
    docker compose pull             # pull latest images for all services

    Know the difference: down removes containers (their writable layers) but keeps named volumes unless you pass -v. Most "I lost my database" accidents are down -v.

    8. Reusing configuration: overrides, profiles, .env

    9. Scaling and one-off tasks

    docker compose up -d --scale api=3      # 3 replicas behind the same network
    docker compose run --rm api python manage.py migrate   # one-off migration
    docker compose run --rm api pytest -q                  # run tests in the stack

    Scaling a service with a published host port conflicts (port already allocated) — for scaled services, remove fixed host ports and put a load balancer in front.

    10. Is Compose suitable for production?

    For a small, single-host deployment with modest availability needs — yes, especially with restart: unless-stopped and healthchecks. Compose limits become visible when you need:

    At that point move to Kubernetes or a managed container service (ECS/EKS/AKS — see the Advanced page). Compose files also translate well: many teams use them as the source of truth for what runs, then deploy the same images to an orchestrator.

    ← Back to IT Knowledge Base