Override Files and the Dev/Prod Split
The same stack runs differently in two places. Locally a developer wants source bind-mounted for live edits and a debug port exposed; in production the image is fixed, no source is mounted, and resource limits apply. Compose handles this with file layering — a base compose.yaml holds what is common, and override files layer on top to add or change fields per environment.
Compose even auto-merges a compose.override.yaml for local development without any flags, which is convenient and occasionally surprising. The surprise is the whole reason this topic needs care: a file that merges automatically can shape a stack you did not mean to change.
-f for prodcompose.yamlcompose.override.yaml-f compose.yaml -f compose.prod.yamlThe Two-File Mental Model
compose.yaml is the base — the shared truth: the services, the network, the volume, the images. An override file is a partial document with the same structure that Compose deep-merges on top, so you state only the deltas, not the whole stack twice. A list like ports is appended, a scalar like image is replaced, and a nested map is merged key by key. You write the difference and let the merge produce the full picture.
The Auto-Merged compose.override.yaml
When a file named compose.override.yaml is present, Compose loads it on top of compose.yaml automatically on every up, with no -f needed. That makes it the local-dev layer by convention — and it is exactly why a teammate's checked-out override can quietly change your stack. The auto-merge is a convenience that turns into a footgun the moment those dev settings end up somewhere they should not run.
# merged on top of compose.yaml automatically on a bare `docker compose up`
services:
web:
build: ./web
command: ["flask", "run", "--host", "0.0.0.0", "--reload"]
volumes:
- ./web:/app
ports:
- "8000:8000"
This override bind-mounts ./web into the container for live reload, swaps gunicorn for Flask's reload-capable dev server, and publishes web's 8000 directly so a developer can hit it without going through proxy. The base file knows none of this; the dev layer adds it, and a bare up picks it up.
The Prod Composition
Production uses explicit files instead of the auto-merge: docker compose -f compose.yaml -f compose.prod.yaml up. The prod override pins the built image so no source is mounted, sets a restart policy and resource limits, and keeps only proxy exposed. Naming files explicitly with -f defeats the auto-merge entirely, so a compose.override.yaml sitting in the directory is ignored and the dev layer can never leak into a prod bring-up.
# docker compose -f compose.yaml -f compose.prod.yaml up -d
services:
web:
image: registry.driftwood.example/driftwood/web:1.4.0
restart: always
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
proxy:
restart: always
No bind mount, no dev server, no published 8000 — the prod layer pins a registry image by tag, adds restart: always, and caps web's CPU and memory. Because the command names both files with -f, the auto-override is excluded by construction.
The Single-Host Ceiling
Override files cover development and small single-host production well. They do not turn Compose into a fleet scheduler. When "production" means many hosts, rolling updates, and self-healing, the answer is Kubernetes (Chapter 12, topic 76), not a taller stack of override files. Stacking more -f layers can express more configuration, but it cannot give Compose a scheduler it does not have — that is a different tool, across the orchestration boundary.
compose.override.yaml — loaded automatically on top of compose.yaml with a bare docker compose up. That makes it the local-dev layer, but it also means it merges whether you intended it or not. Use it for dev settings that should apply by default on a developer's machine.
Explicit -f base -f prod composition — merges only the files you name, in order, ignoring the auto-override. This is exactly what production wants, so the dev layer can never leak into a prod bring-up. Default merge for convenience locally; explicit -f for any environment where surprises are unacceptable.
- Putting prod settings in
compose.override.yaml— it auto-merges into every developer's localup, so production resource limits or image pins silently shape the dev stack and vice versa. - Running a bare
docker compose upon a prod host that happens to have acompose.override.yamlchecked out — the dev bind mounts and exposed ports merge in automatically; prod must always name files with-fto exclude it. - Duplicating the entire stack across base and override instead of stating only deltas — the two copies drift, and a change made in one file but not the other produces an environment-specific bug that is hard to spot.
- Misordering
-ffiles and being surprised which value wins — later files override earlier ones in the merge, so-f base -f prodlets prod win; reversing it lets base clobber prod.
- Keep the shared truth in
compose.yamland express only per-environment deltas in override files, so there is one source for what is common and a thin diff for what differs. - Reserve
compose.override.yamlfor local development — bind mounts, exposed debug ports, reload servers — since it auto-merges, and never put production settings there. - Compose production explicitly with
-f compose.yaml -f compose.prod.yamlso the dev auto-override is excluded and the prod bring-up is deterministic. - Treat override files as the dev and small-single-host-prod tool they are, and move to Kubernetes when production means multiple hosts, rollouts, and self-healing (Chapter 12, topic 76).
docker run scripts per environment the manual version of this layering
Podman podman-compose supports the same override merging
Kubernetes Kustomize overlays and Helm values are the multi-host analog
Knowledge Check
What does an override file contain relative to the base compose.yaml?
- Only the per-environment deltas, which Compose deep-merges on top of the base
- A complete second copy of the entire stack, with the per-environment differences edited directly in
- A unified text diff that Compose applies line by line as a patch against the base file
- A list of the services to delete from the base stack before Compose brings the project up
When is compose.override.yaml applied, and why does that matter for production?
- It auto-merges on any bare
up, so prod must use explicit-fto avoid silently picking up dev settings - It only applies when explicitly named with
-fon the command line, so production is never at risk from it - It is applied only in production, the one environment where Compose looks for it automatically by default
- It applies only when the
COMPOSE_ENV=prodenvironment variable is exported in the shell first
In docker compose -f compose.yaml -f compose.prod.yaml up, which file's values win on a conflict?
- The later file,
compose.prod.yaml, because later-ffiles override earlier ones - The base
compose.yaml, because it is the source of truth and is the one named first - Neither — Compose errors and refuses to start whenever two files happen to set the same field
- Whichever of the two file names happens to sort last alphabetically by character order
What is the ceiling that override files cannot raise?
- Compose stays single-host — multi-host scheduling, rollouts, and self-healing need Kubernetes
- There is a hard limit of two override files per project that Compose will allow you to stack
- Override files cannot change environment variables at all, only the published ports and the volumes
- Override files cannot reference named volumes for persistence, only host bind mounts into the container
You got correct