Docker & DevOps

Docker Compose override files: separate dev and production configs

Keep one base Compose file and layer per-environment settings with a dev override file and a production file, merged with -f or COMPOSE_FILE.

IntermediateLinux server6 min read

A Docker Compose override file lets you keep one base service definition and layer environment-specific settings on top of it, so development and production stop drifting apart. In this guide you build a small web plus database stack, split it into a base file, a dev override and a production file, and merge them with -f and COMPOSE_FILE.

Before you start

You need:

  • A Linux VPS running Ubuntu 22.04/24.04, Debian 12 or AlmaLinux/Rocky 8–9.
  • Docker Engine and the Compose v2 plugin. Check with docker compose version.
  • SSH access as a user that can run Docker (a member of the docker group, or use sudo).
  • Comfort reading YAML. Indentation matters, and tabs are not allowed.

If Docker is not installed yet, follow Install Docker and Docker Compose on a Linux VPS first.

Create a project directory and move into it:

Bash
mkdir -p /home/deploy/app && cd /home/deploy/app

Compose reads compose.yaml, compose.yml, then docker-compose.yml, in that order of preference. This guide uses docker-compose.yml because the override naming convention builds on it.

Create the base Compose file

The base file holds everything that is true in every environment: images, service names, restart policy, volumes.

Bash
nano docker-compose.yml

Paste:

YAML
services:
  web:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
    environment:
      APP_ENV: production

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: change-me
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

Save with Ctrl+O, Enter, then Ctrl+X. If nano is new to you, Edit files with nano covers the keys.

Note what is deliberately missing: no bind mounts, no debug flags, no extra published ports. Those belong in the environment layers.

Add the development Docker Compose override file

Compose automatically loads a file named docker-compose.override.yml next to the base file when you run docker compose up with no -f flags. That makes it the natural place for developer-only settings.

Bash
nano docker-compose.override.yml
YAML
services:
  web:
    ports:
      - "8080:80"
    volumes:
      - ./site:/usr/share/nginx/html:ro
    environment:
      APP_ENV: development

Create the content directory so the bind mount has something to serve:

Bash
mkdir -p site && echo '<h1>dev build</h1>' > site/index.html

Now docker compose up -d uses the base file plus this override, and the site is reachable on port 8080 while the database keeps its named volume.

Tip: Keep docker-compose.override.yml out of your production deploy. It is meant to be convenient on a workstation, not on a server.

Add a production override file

Production settings go in a separate file that you apply on purpose. Create docker-compose.prod.yml:

Bash
nano docker-compose.prod.yml
YAML
services:
  web:
    ports:
      - "80:80"
    environment:
      APP_ENV: production
    deploy:
      resources:
        limits:
          memory: 512M

  db:
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

The ${POSTGRES_PASSWORD} reference is filled from the environment or a .env file in the same directory. Create it with restrictive permissions:

Bash
printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 16)" > .env
chmod 600 .env

Warning: Never commit .env to version control, and never hard-code a real database password in a Compose file that lives in a repository.

How Docker Compose override files merge

When you pass more than one file, Compose merges them left to right. The rules are simple but they surprise people:

  • Mappings such as environment, labels and deploy are merged key by key. A key set in a later file wins.
  • Single values such as image, restart and command are replaced outright by the last file that sets them.
  • Lists such as ports, volumes and dns are appended, not replaced.
  • Relative paths in volumes and build are resolved against the directory of the first file, not the file that declares them.

That last point about lists is why the dev and production port blocks should not both be active at once: merging 8080:80 with 80:80 publishes both ports. Keep environments in separate files and select exactly one.

Run Compose with explicit -f files

For production, list the files in order. The base file always comes first.

Bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml config

config prints the merged result and exits without starting anything. Read it before you deploy.

Bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

If you deploy often, set the file list once instead of repeating flags. COMPOSE_FILE uses a colon separator on Linux:

Bash
export COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml
docker compose config
docker compose up -d

Add that export to your shell profile or a small deploy script so every command in the session targets production. To go back to the dev defaults, run unset COMPOSE_FILE.

Useful variations:

Bash
docker compose config --services
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f web

Warning: docker compose down -v deletes named volumes, including the database volume. Use plain down unless you intend to destroy data.

Check it works

Confirm the merged configuration, then the running state:

Bash
docker compose config | grep -A4 'ports:'
docker compose ps
curl -I http://203.0.113.10

With the production file applied, the web container should listen on port 80 and curl should return HTTP/1.1 200 OK. With no -f flags, the same command on port 8080 should serve your dev index.html.

Check the environment variable made it through:

Bash
docker compose exec web printenv APP_ENV

Expected output is production with the prod file and development with the override. If you changed a Compose file, recreate the containers so the new settings apply:

Bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --force-recreate

Troubleshooting

  • Error response from daemon: port is already allocated — the dev override is still being loaded, so both port 80 and 8080 are requested. Pass -f explicitly or set COMPOSE_FILE, and stop the old stack with docker compose down.
  • An environment variable keeps the old value — environment in list form (- KEY=value) is appended rather than merged, so duplicates confuse the result. Switch both files to map form and let the last file win.
  • config output is too long to read — narrow it with docker compose config --services for service names, or docker compose config | less to scroll the full merge.
  • The web container returns 403 or 404 after a bind mount — the host path does not exist or is empty, so Compose creates an empty directory. Check the path with ls -la./site and confirm the files are there.
  • Settings changed but nothing happened — running containers keep their original configuration. Recreate them with up -d --force-recreate rather than restarting.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.