Search across all documentation pages
-f, the auto-loaded compose.override.yaml, per-service profiles, and activation via --profile or COMPOSE_PROFILES.up.compose.yaml.compose.override.yaml; Compose loads it automatically.compose.ci.yaml and pass it with -f.profiles so they stay off by default.--profile <name> or COMPOSE_PROFILES.The base file describes the app and database with production-safe defaults.
# compose.yaml
services:
app:
image: myorg/app:latest
environment:
LOG_LEVEL: "info"
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: secretThe override adds dev conveniences: a build context, source mount, and published ports.
# compose.override.yaml (auto-loaded)
services:
app:
build: .
command: npm run dev
environment:
LOG_LEVEL: "debug"
volumes:
- .:/app
ports:
- "3000:3000"
db:
ports:
- "5432:5432"A plain docker compose up merges both, so dev gets debug logging and live reload.
docker compose up # compose.yaml + compose.override.yaml
docker compose config # print the fully merged, resolved configFor CI, skip the override and layer a CI-specific file instead.
# compose.ci.yaml
services:
app:
environment:
LOG_LEVEL: "warn"
CI: "true"docker compose -f compose.yaml -f compose.ci.yaml up --abort-on-container-exitPassing -f explicitly disables the automatic compose.override.yaml, so CI stays clean.
Compose deep-merges files left to right. Later files win on scalar conflicts.
Mappings merge key by key. So environment from the override adds to and overrides the base rather than replacing it wholesale.
Most sequences replace rather than append. If both files define ports or command, the later file's value wins entirely.
Always check the result with docker compose config, which prints the merged, variable-expanded configuration Compose will actually run.
Compose automatically reads compose.override.yaml (or .yml) on top of compose.yaml when you do not pass any -f.
This is the idiomatic place for local-only settings you do not want in production. It keeps the base file honest.
The moment you pass explicit -f flags, auto-loading stops. You are now in full control of the file list.
profiles marks a service as opt-in. By default it does not start.
services:
app:
image: myorg/app:latest
seed:
image: myorg/app:latest
command: npm run seed
profiles: ["tools"]
adminer:
image: adminer:5
ports:
- "8081:8080"
profiles: ["debug"]docker compose up starts only app. The seed and adminer services stay dormant.
docker compose --profile debug up # app + adminer
COMPOSE_PROFILES=tools,debug docker compose up # app + seed + adminerA service with no profiles is always active. Activating a profile adds its tagged services to whatever runs by default.
Use overrides to change configuration of services that run in every context.
Use profiles to include or exclude whole services per context. They compose well together: an override can adjust the base, while a profile gates optional tooling.
Editing the base for a dev-only change. Dev tweaks belong in compose.override.yaml, not compose.yaml. Polluting the base leaks dev settings into other environments.
Expecting lists to merge. command, ports, and most sequences replace, not append. If you meant to add a port, restate the full list in the later file.
Explicit -f silently drops the override. Once you pass any -f, Compose no longer auto-loads compose.override.yaml. List every file you need explicitly.
A profiled service pulled in by depends_on. If an always-on service depends on a profiled one, that dependency is only started when its profile is active, which can surprise you. Keep dependency graphs within the same activation scope.
Forgetting to verify the merge. Different environments produce different merged files. Run docker compose config in each to confirm what actually runs.
Multiple full files, no override auto-load. Some teams keep compose.dev.yaml and compose.prod.yaml as complete files and always pass -f. More explicit, but more duplication.
Environment variables and .env. For values that change but topology that does not, variable substitution alone may be enough, no second file required. Combine with overrides when structure changes too.
Kustomize or Helm. Those are the manifest-world analogs of overrides. They belong to Kubernetes, not Compose, but the layering instinct carries over.
A single file with conditionals. Compose has no if, so people fake it with profiles and env-driven values. Profiles are the supported mechanism; reach for them before clever hacks.
Which file loads by default? compose.yaml plus compose.override.yaml when present, and you pass no -f.
How do I see the final merged config? docker compose config prints the resolved, merged YAML with variables expanded.
Do lists merge or replace? Maps merge key by key; most lists replace. Restate the whole list in the winning file.
How do I turn on a profile? Use --profile <name> on the command, or set COMPOSE_PROFILES=a,b.
Are unprofiled services affected by profiles? No. Services without profiles always run; activating a profile only adds its tagged services.
Can I combine overrides and profiles? Yes. Overrides reshape running services; profiles decide which services exist in the run.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026