
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.
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 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
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.
The example shows four different outcomes:
LOG_LEVEL becomes debug, KEEP_ME remains, and APP_MODE is added. Keys are merged individually./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.
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.
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.
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.
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.
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.