Stackbook Logo
← Back to Tools

Docker Compose Guide for Multi-Container Apps

Define and run multi-container applications with Docker Compose: services, networks, volumes, health checks, profiles, environment files and tips.

4 min read

What it is and when to use it

Docker Compose lets you define a multi-container application in a single YAML file and manage it with one command. Instead of running several docker run commands with long flag lists, you describe services, networks and volumes declaratively, then bring the whole stack up or down together.

Use Compose for local development environments, integration tests in CI, demos, and small single-host deployments. It is not a cluster scheduler: for multi-node scaling, self-healing and rolling updates, move to Kubernetes.

Quick start

Modern Docker ships Compose as a plugin, invoked as docker compose. Create compose.yaml next to your code.

services:
  web:
    build: .
    ports:
      - "8080:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

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

  cache:
    image: redis:7-alpine

volumes:
  dbdata:

Run it.

docker compose up -d --build
docker compose ps
docker compose logs -f web
docker compose exec db psql -U app -d app
docker compose down            # keep volumes
docker compose down -v         # also remove volumes

Core concepts

Services are the containers you run, each from an image or a build context. Compose creates a default network so services reach each other by service name, such as db or cache, as hostnames. Published ports are only needed for access from the host.

Volumes persist data. Named volumes are managed by Docker and survive container removal. Bind mounts map a host directory, which is ideal for live code reload in development.

services:
  web:
    build:
      context: .
      target: dev
    volumes:
      - ./src:/app/src
    command: npm run dev

Environment and secrets. Variables can be set inline, loaded from a file, or interpolated from the shell. A file named .env beside the compose file feeds interpolation automatically.

services:
  web:
    image: myorg/web:${TAG:-latest}
    env_file:
      - .env.web
    secrets:
      - api_key

secrets:
  api_key:
    file: ./secrets/api_key.txt

Health checks and ordering. The depends_on keyword with condition service_healthy waits for readiness, not just container start. Without it, your app may race the database.

Profiles make services optional.

services:
  adminer:
    image: adminer
    ports:
      - "8081:8080"
    profiles: ["debug"]
docker compose --profile debug up -d

Overrides and multiple files. Compose automatically merges compose.override.yaml, which is useful for development tweaks. For other environments, combine files explicitly.

docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose config          # print the resolved configuration

Networks can be split to isolate tiers, for example a frontend network for the proxy and web service and a backend network for web and database, so the database is unreachable from the proxy.

Resource and scale controls. Use the deploy resources section for limits, and the scale flag to run replicas of stateless services.

docker compose up -d --scale worker=3

Pitfalls

  • Never commit real secrets in the compose file or .env. Use secrets, a local untracked file, or an external manager.
  • Do not rely on depends_on alone for readiness; add health checks.
  • Port conflicts are the most common startup failure. Change the host side of the mapping, which is the left number.
  • Data loss: docker compose down -v deletes named volumes, including databases.
  • Using localhost inside a container refers to that container. Use the service name for inter-container calls.
  • Bind mounts on macOS and Windows can be slow for large trees such as node_modules; mount only source folders or use the watch feature, docker compose watch.
  • Indentation and quoting errors in YAML cause confusing messages. Quote port mappings and run docker compose config to validate.
  • The obsolete top-level version key is ignored by current Compose and can be removed.
  • Image tags like latest drift. Pin versions for reproducibility.

How it fits an architecture

Compose is the developer inner loop and the test harness: it reproduces production dependencies, such as databases, queues and a reverse proxy, on a laptop or CI runner. A GitHub Actions job can run docker compose up with wait support, execute integration tests, then tear it down. For production on one host, pair it with Nginx for TLS and a restart policy. When you outgrow a single machine, the same images move to Kubernetes, deployed by Helm and Argo CD, and tools can translate Compose files as a starting point.

Key takeaways

  • One YAML file describes services, networks and volumes for the whole stack.
  • Services find each other by name on the default network.
  • Use health checks with depends_on conditions to avoid startup races.
  • Profiles, overrides and env files adapt one definition to many environments.
  • Compose suits development, CI and small hosts; use Kubernetes for clusters.