Most teams' local environment and integration tests run on Compose; these features keep it maintainable.
Once a project grows, a single flat compose.yaml starts to strain. Compose has tools for that.
Override files. docker compose up automatically merges compose.override.yaml on top of compose.yaml. Put production-like settings in the base and dev conveniences (bind mounts, debug ports) in the override. For other environments, pass files explicitly: docker compose -f compose.yaml -f compose.ci.yaml up. Later files override or extend earlier ones.
Profiles. Mark optional services with profiles: [tools]; they start only with --profile tools. Good for admin UIs, seeders and load generators.
Startup ordering. depends_on conditions: service_started, service_healthy (requires a healthcheck) and service_completed_successfully, which is ideal for one-shot migration jobs that must finish before the app starts.
Interpolation. ${VAR}, ${VAR:-default} and ${VAR:?error message} (fail if unset) come from the shell or the project's .env. Check the final, merged result with docker compose config; it's the best debugging tool for Compose files.
Watch mode. A develop.watch section plus docker compose watch syncs changed source files into running containers (action: sync), restarts on config changes (sync+restart) or rebuilds when dependencies change (rebuild). It replaces fragile bind mounts on Docker Desktop.
Reuse. include: pulls in other Compose files, such as a shared infrastructure stack, and YAML anchors or extends remove repetition.
CI. docker compose up -d --wait blocks until services are healthy, and docker compose run --rm tests runs one-off containers on the project network.
Example
services:
migrate:
build: .
command: ["alembic", "upgrade", "head"]
depends_on: { db: { condition: service_healthy } }
api:
build: .
ports: ["127.0.0.1:${API_PORT:-8000}:8000"]
environment:
SECRET_KEY: ${SECRET_KEY:?set SECRET_KEY in .env}
depends_on:
migrate: { condition: service_completed_successfully }
develop:
watch:
- { action: sync, path: ./src, target: /app/src }
- { action: rebuild, path: requirements.txt }
db:
image: postgres:17
environment: { POSTGRES_PASSWORD: dev }
healthcheck: { test: ["CMD-SHELL", "pg_isready -U postgres"], interval: 5s }
adminer:
image: adminer
profiles: [tools]
# docker compose config ; docker compose watch ; docker compose --profile tools up -dRemember
- compose.override.yaml is merged automatically; -f stacks files explicitly
- profiles make services optional; service_completed_successfully gates on jobs
- docker compose config shows the fully merged, interpolated file
- compose watch syncs or rebuilds on file changes
Watch out for
- Debugging a confusing setup without running docker compose config to see what Compose actually resolved.