← Blog
Three stacked cream and green ceramic plates with a coral inset on a pale lavender background.
Published on
2026-11-09

Docker Compose override files: merge settings and check the result

A Docker Compose override file changes selected parts of a base configuration. You can keep shared service settings in compose.yaml, then add the differences needed for local development or another environment. The important step is to inspect the merged model: a later value may replace, extend, or merge with an earlier one.

This guide uses a small configuration lab to check file selection, environment mappings, commands, ports, and bind mounts. Fourteen checks passed with Docker Compose v5.1.4 on Windows. They exercised the real Compose CLI, including rejected configurations. Docker’s engine was unavailable, so no images were pulled or containers started.

Choose files explicitly when the result matters

By default, Compose combines compose.yaml with an optional compose.override.yaml. Use repeated -f options to select another set, in order. Docker’s multiple-file guide describes this behavior. A command with only -f compose.yaml excludes the default override file.

Run the lab from its project directory, without a COMPOSE_FILE setting that changes default discovery. Check your installed implementation with docker compose version. The examples use the current docker compose command and Compose-specific YAML tags, not a generic YAML merge tool.

Create a base file and one override

Create this directory layout, with a short index.html file in each content directory. The review directory and empty overlays directory are used later.

override-lab/
  compose.yaml
  compose.override.yaml
  site/index.html
  preview/index.html
  review/index.html
  overlays/

Save this as compose.yaml:

services:
  web:
    image: python:3.13-alpine
    command: ["python", "-m", "http.server", "8000", "--directory", "/site"]
    environment:
      LOG_LEVEL: info
      KEEP_ME: base
    ports:
      - "127.0.0.1:8080:8000"
    volumes:
      - type: bind
        source: ./site
        target: /site
        read_only: true
        bind:
          create_host_path: false

The Python image and HTTP server make the command and mount examples concrete. This is a configuration exercise, not a production web-server template. LOG_LEVEL, KEEP_ME, and APP_MODE are demonstration environment variables; Python’s HTTP server does not interpret them as application settings.

Save the next file as compose.override.yaml. It changes the command to include Python’s unbuffered-output option, adjusts environment values, adds a host port, and chooses another directory for the same container mount destination.

services:
  web:
    command: ["python", "-u", "-m", "http.server", "8000", "--directory", "/site"]
    environment:
      LOG_LEVEL: debug
      APP_MODE: development
    ports:
      - "127.0.0.1:8081:8000"
    volumes:
      - type: bind
        source: ./preview
        target: /site
        read_only: true
        bind:
          create_host_path: false

Compare the base and merged models

Run these commands from the project directory. The first reads only the base, the second uses default discovery, and the third names the two files explicitly.

docker compose -p override-lab -f compose.yaml config
docker compose -p override-lab config
docker compose -p override-lab -f compose.yaml -f compose.override.yaml config --format json

In our checks, default discovery and the explicit two-file command produced identical JSON models. The base-only command retained LOG_LEVEL=info, port 8080, and the site directory. The merged model contained the following fields. This excerpt omits unrelated fields and shortens the absolute mount source to <project>/preview.

{
  "environment": {
    "APP_MODE": "development",
    "KEEP_ME": "base",
    "LOG_LEVEL": "debug"
  },
  "command": [
    "python",
    "-u",
    "-m",
    "http.server",
    "8000",
    "--directory",
    "/site"
  ],
  "ports": [
    {
      "mode": "ingress",
      "host_ip": "127.0.0.1",
      "target": 8000,
      "published": "8080",
      "protocol": "tcp"
    },
    {
      "mode": "ingress",
      "host_ip": "127.0.0.1",
      "target": 8000,
      "published": "8081",
      "protocol": "tcp"
    }
  ],
  "volumes": [
    {
      "source": "<project>/preview",
      "target": "/site",
      "read_only": true
    }
  ]
}

docker compose config parses the files, resolves values, and renders the model that Compose would apply. Its command reference also documents JSON output and -q, which validates without printing the model. Successful validation does not establish image availability, host-path permissions, port availability, or application health.

Read each merge according to its field

The example shows four different outcomes:

  • Environment: LOG_LEVEL becomes debug, KEEP_ME remains, and APP_MODE is added. Keys are merged individually.
  • Command: the seven-element override command replaces the six-element base command. The two lists are not concatenated.
  • Ports: both host ports 8080 and 8081 remain, each targeting container port 8000. Changing the published port creates a distinct mapping.
  • Volumes: one mount remains at /site, using preview as its source. The container destination identifies the mount being changed.

Docker’s merge specification defines these rules. Port uniqueness considers the IP address, target port, published port, and protocol. Volumes use the target path. Command-like fields, including command, entrypoint, and healthcheck.test, have replacement behavior.

Both published addresses in this lab are bound to 127.0.0.1 in the model. For the difference between a host mapping and communication between services, see our Compose networking guide.

Replace or remove inherited ports deliberately

An empty list is easy to misread. Save this as compose.empty.yaml:

services:
  web:
    ports: []

Add it last:

docker compose -p override-lab -f compose.yaml -f compose.override.yaml -f compose.empty.yaml config

Our resolved model still contained both port mappings. The untagged empty sequence did not erase the inherited entries.

To keep only port 8081, save this as compose.replace.yaml:

services:
  web:
    ports: !override
      - "127.0.0.1:8081:8000"

The !override tag replaces the whole attribute instead of applying its normal merge rule. Docker documents support from Compose 2.24.4 onward. Validate and inspect the exact three-file set:

docker compose -p override-lab -f compose.yaml -f compose.override.yaml -f compose.replace.yaml config -q
docker compose -p override-lab -f compose.yaml -f compose.override.yaml -f compose.replace.yaml config --format json

Our result contained only 127.0.0.1:8081 targeting port 8000. The environment and mount changes from compose.override.yaml remained.

To remove all published ports instead, save this separate alternative as compose.reset.yaml:

services:
  web:
    ports: !reset []
docker compose -p override-lab -f compose.yaml -f compose.override.yaml -f compose.reset.yaml config

The !reset example removed the service’s port declarations while leaving its merged environment intact. Removing a host port declaration does not remove the service from its Compose network. The lab verifies these tags for ports; inspect any other tagged field with your installed Compose version before relying on it.

Resolve paths from the base file

An override in a subdirectory does not give its relative paths a new base. Save this fragment as overlays/compose.review.yaml:

services:
  web:
    volumes:
      - type: bind
        source: ./review
        target: /site
        read_only: true
        bind:
          create_host_path: false
docker compose -p override-lab -f compose.yaml -f overlays/compose.review.yaml config

In our resolved model, the source was <project>/review, not <project>/overlays/review. The first -f file establishes the default base directory. The Compose CLI reference documents --project-directory as an explicit way to change that base.

The long bind syntax sets create_host_path: false so a missing directory should not be silently created on startup. Configuration parsing alone does not verify that the Docker daemon can access that directory. See the volume reference for bind-mount options.

This fragment fails validation when used alone because it supplies neither an image nor a build definition. It works with the base file because the completed service inherits its image. For independently maintained Compose applications that need their own path bases, read Docker’s include reference; this lab does not test that separate mechanism.

Keep file order and variable resolution visible

Reverse the original two files to see why order belongs in the review:

docker compose -p override-lab -f compose.override.yaml -f compose.yaml config

Our reversed result restored the base command, LOG_LEVEL=info, and the site mount source. The development-only environment key still remained, and both distinct ports survived. Moving the base last did not undo every addition from the first file.

Variable interpolation happens before merging, separately for each file, as described in Docker’s interpolation reference. An environment entry intended for the container does not automatically supply values for ${...} expressions in another Compose file. Our environment-variable guide covers that distinction.

If the requirement is to select optional services, Compose profiles may fit the task. Overrides change configuration; a profile is not a substitute for reviewing how those changes merge.

Check the final model before starting containers

Keep the ordered file list consistent between validation and any later up, ps, or down command. Check the resolved image, full command, environment, published addresses, mount sources, and mount destinations. Inspect only sanitized model output when sharing it: real configurations can contain resolved secrets.

The fourteen checks covered automatic and explicit selection, replacements and additions, empty lists, port tags, relative paths, reversed order, standalone-fragment failure, invalid port syntax, and quiet validation. They did not test container startup, HTTP responses, or a deployment on Compute with Hivenet. After reviewing the model, runtime checks remain a separate step in your chosen environment.

Your next workload belongs on Hivenet.

Pick one AI, compute, or storage workload and see the difference for yourself. Spin it up in minutes, or let our team map your fastest path to production.