sundaydeploys.dev — bash
  ┌─────────────────────────────────────────┐
  │                                         │
  │    ☽  S U N D A Y   D E P L O Y S  ☽    │
  │                                         │
  │         ~  code · hobbies  ~            │
  │                                         │
  └─────────────────────────────────────────┘
~/sundaydeploys $ cat welcome.txt
I'm a software engineer who spends weekdays writing code and weekends chasing my hobbies

Docker Compose Overrides

docker devops

Docker Compose Overrides

I run a handful of Python services in Docker, and for a while I had a single compose.yaml that defined the whole stack. Once I needed separate configs for local dev, CI, staging, and production, maintaining copies of that file turned into a real headache.

Compose has an override system that fixes this. You stack multiple compose files on top of each other, and later files win. Your base config stays clean, and each environment only defines what it changes.

Here’s what my project structure looks like now:

project/
  compose.yaml              # production baseline
  compose.override.yaml     # local dev extras (gitignored)
  compose.ci.yaml           # CI pipeline
  compose.prod.yaml         # production deploy overrides

The Default Override File

By default, Compose reads two files: a compose.yaml and, if it exists, a compose.override.yaml. If a service is defined in both, Compose merges the configurations automatically when you run docker compose up.

So my base file has the production-ready definitions:

# compose.yaml
services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://db:5432/app

And the override adds dev conveniences without touching the base:

# compose.override.yaml
services:
  web:
    volumes:
      - .:/app
    ports:
      - "5678:5678"
    environment:
      - DEBUG=true

Volume mounts for live-reloading my Python code, an exposed debug port, DEBUG=true. All of that merges in automatically.

Stacking Files with -f

For more than two layers, you can use the -f flag to specify compose files in order:

docker compose -f compose.yaml -f compose.prod.yaml up -d

This lets you maintain named environment files like compose.staging.yaml or compose.ci.yaml. Compose merges files in the order they’re specified on the command line, so values in later files take precedence.

Watch out for paths, though. They all resolve relative to the first compose file you pass with -f. You can verify the merged result at any time with docker compose config. I run this whenever I change an override, just to make sure the merge looks right.

Reusing Services with extends

There’s also an extends keyword for reusing service definitions across files. I use this when services overlap. My web app and Celery worker share the same build context, env vars, and volume mounts. The only difference is the entrypoint command.

# common.yaml
services:
  base-app:
    build: .
    environment:
      - DATABASE_URL=postgres://db:5432/app
    volumes:
      - .:/app
# compose.yaml
services:
  web:
    extends:
      file: common.yaml
      service: base-app
    command: gunicorn app:main

  worker:
    extends:
      file: common.yaml
      service: base-app
    command: celery -A tasks worker

The shared config lives in one place, and each service overrides only what differs.

Isolating with include

include is built for bigger repos where multiple teams each own a service. Each path you list loads as its own Compose application with its own project directory.

# compose.yaml
include:
  - ./api/compose.yaml
  - ./frontend/compose.yaml
  - ./worker/compose.yaml

Relative paths resolve independently, which sidesteps the path headaches you hit with extends.

I’d reach for include over extends when each service has its own directory and its own team maintaining it. extends works better when services live in the same project and just share config.

Keeping It Manageable

You can set the COMPOSE_FILE environment variable (e.g., in a .env or your shell profile) to avoid typing -f flags repeatedly. I keep mine in .env so the whole team uses the same file order without thinking about it.

Sources