Chapter 8: Docker Compose
Topic 51

Override Files and the Dev/Prod Split

LayeringCompose

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.

Two ways files compose: auto-merge for dev, explicit -f for prod
compose.yaml
+
compose.override.yaml
auto-merged for dev
-f compose.yaml -f compose.prod.yaml
explicit merge for prod

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

compose.override.yaml — the auto-merged local-dev layer
# 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.

compose.prod.yaml — composed explicitly with -f, never auto-merged
# 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.

Auto-merged compose.override.yaml vs explicit -f composition

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.

Common Mistakes
  • Putting prod settings in compose.override.yaml — it auto-merges into every developer's local up, so production resource limits or image pins silently shape the dev stack and vice versa.
  • Running a bare docker compose up on a prod host that happens to have a compose.override.yaml checked out — the dev bind mounts and exposed ports merge in automatically; prod must always name files with -f to 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 -f files and being surprised which value wins — later files override earlier ones in the merge, so -f base -f prod lets prod win; reversing it lets base clobber prod.
Best Practices
  • Keep the shared truth in compose.yaml and 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.yaml for 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.yaml so 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).
Comparable tools Separate 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 -f to avoid silently picking up dev settings
  • It only applies when explicitly named with -f on 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=prod environment 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 -f files 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