Docker Compose Override Merge Previewer

See what a stack of -f files produces. Put the base above a line of three dashes and the override below, in the order you pass them.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Merge. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

Two port lists

Appended, so the override adds a port and cannot take the first one away

services:
  web:
    image: myapp
    ports:
      - "8080:80"
---
services:
  web:
    ports:
      - "9090:80"

Two commands

Replaced wholesale, which is the opposite of how ports behaves

services:
  web:
    image: myapp
    command: ["serve", "--port", "80"]
---
services:
  web:
    command: ["debug"]

The same host port twice

What appending produces, and the error looks like something else holding the port

services:
  web:
    ports:
      - "8080:80"
---
services:
  web:
    ports:
      - "8080:3000"

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Writing an override to stop publishing a port

    ports is appended, so the result publishes both. There is no syntax for removing an entry.

    Instead:Take the port out of the base file and add it in the overrides that want it.

  2. Expecting command to append like ports does

    command, entrypoint and healthcheck are replaced entirely, even though they are written as lists.

    Instead:Write the whole command in the override. Nothing from the base carries over.

  3. Keeping override files in a subdirectory

    Relative paths resolve against the first file's directory, so a build context written relative to the override points somewhere that does not exist.

    Instead:Keep them beside the base file, use absolute paths, or pass --project-directory.

Some lists append and some replace, in the same file

ports, volumes and cap_add are concatenated. command, entrypoint and healthcheck are replaced. Both are lists and there is nothing in the syntax to tell them apart.

Appending means an override can never remove

An override written to stop publishing a port publishes both. A base with a development bind mount cannot have it taken away in a production override. There is no removal syntax at all, so the only fix is restructuring so the base does not have the entry in the first place.

Which is why the base should be minimal

Anything environment-specific belongs in the overrides rather than as a default in the base that one of them then tries to undo. That inverts the instinct to put sensible defaults at the bottom.

command and entrypoint replace wholesale

Despite being lists. They are single values that happen to be written as sequences, so the override's version stands alone and nothing from the base survives. That is the opposite of ports, in the same merge.

environment and labels merge by key

So an override can change one variable without restating the rest, and both the list form and the map form work. This is the behaviour people expect from all of it, which is part of why the appended keys surprise.

Relative paths resolve against the FIRST file's directory

The project directory comes from the first -f. A build context or a bind mount written relative to an override kept in a subdirectory resolves somewhere else entirely, and the error is a missing path that plainly exists.

docker compose config is authoritative

It prints the merged result, and it is worth running before any apply you care about. This page shows the same thing without needing the files on disk, which is useful when you are reading a repository rather than running it.

More docker tools

Docker Container Image Reference Parser Is that a registry or a Docker Hub user? Dockerfile Linter What this image will do to you later docker run to Kubernetes YAML And the flags that have no equivalent Docker Compose to Kubernetes And why depends_on has no equivalent Docker Logging Driver Generator json-file never rotates Dockerfile Non-Root USER Generator Numeric, or Kubernetes rejects it Docker OCI Image Labels Generator One of them actually does something Docker Hub Pull Rate Limit Calculator Counted per IP, not per person Docker Registry Storage Calculator Deleting a tag frees nothing Docker Build Context Analyzer All of it uploads before anything runs Docker Compose to docker run Converter Names stop resolving Docker Compose Version Migrator version: is obsolete now Docker Compose Network and Port Viewer Everything reaches everything Docker Command Explainer Which flags hand over the host Docker BuildKit Cache Mount Generator apt deletes the cache you just mounted Docker BuildKit Secret Mount Generator ARG is in docker history forever Docker .dockerignore Pattern Tester *.log does not match logs/app.log Docker Image Manifest Viewer unknown/unknown is not broken Docker Image Config Viewer Anyone who can pull can read your Env Docker Registry Token Decoder Check the scope before the credentials Docker HEALTHCHECK Generator Docker does nothing when it fails Docker Network Subnet Calculator 31 networks, then it stops Docker History Analyzer The rm did not save anything Docker Run to Compose Converter Some flags have no equivalent Docker CMD and ENTRYPOINT Tester Why docker stop takes ten seconds Docker Port Mapping Tester -p reaches past your firewall Docker -v to --mount Converter -v invents missing directories Docker Restart Policy Simulator The backoff resets after ten seconds Docker Exit Code Explainer 137 is two different problems Docker CPU Limit Converter --cpu-shares limits nothing Docker Memory Limit Calculator -m 512m allows a gigabyte Docker Compose Interpolation Tester An unset variable is an empty string Docker Environment Variable Precedence Tester Your shell does not reach the container Dockerfile Multi-Stage Build Analyzer Which stages reach the image Dockerignore Validator It is not .gitignore

Elsewhere on the site