Profiles & Overrides
Summary
- Profiles and override files let one base stack adapt to different contexts without duplicating the whole file.
- Insight: overrides change how services run by merging YAML; profiles change which services run by tagging them.
- Key Concepts: file merge with
-f, the auto-loadedcompose.override.yaml, per-serviceprofiles, and activation via--profileorCOMPOSE_PROFILES. - When to Use: separate dev and CI settings, or keep optional tooling (a debugger, seed job, admin UI) out of the default
up. - Limitations: merge rules differ per field - maps merge, most lists replace. Know which is which before you rely on it.
Recipe
- Put the shared topology in
compose.yaml. - Add dev-only tweaks to
compose.override.yaml; Compose loads it automatically. - For other environments, make a named file like
compose.ci.yamland pass it with-f. - Tag optional services with
profilesso they stay off by default. - Activate them with
--profile <name>orCOMPOSE_PROFILES.
Working Example
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.
Deep Dive
How the merge works
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.
The default override file
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: choosing which services run
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.
Overrides versus profiles
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.
Gotchas
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.
Alternatives
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.
FAQs
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.
Related
- Compose Basics
- Compose for Local Dev
- The Compose Model: Declaring a Local Stack
- Compose vs Kubernetes Gap
- Compose Best Practices
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).