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