Skip to main content
Self-Hosting & Privacy

Docker Compose YAML File Explained

Duplicate keys, mixed env syntax, unquoted booleans - none of them throw an error. A structural walkthrough of docker-compose.yml with YAML anchors to cut repetition and the docker compose config habit that catches mistakes before they reach a running container.

Milan BuhaOctober 24, 20267 min read
ShareXin
Docker Compose YAML File Explained

Docker Compose YAML File Explained

Your docker-compose.yml ran without a single error. The stack came up clean, every container shows running. And one service is still pulling the wrong image tag - because a duplicated key further down the file silently overwrote the first one, and YAML never said a word about it.

TL;DR
  • services: is the only required top-level key in a compose file; networks:, volumes:, configs: and secrets: are optional declarations people reach for before they need to, per the Compose file reference.
  • The syntax mistakes that actually bite - duplicate keys, mixed list/map env syntax, unquoted port strings - don't throw errors. They just silently do the wrong thing.
  • YAML anchors (&) and aliases (*) let you define a block once and reuse it across services, which removes the copy-paste step that causes most of those mistakes in the first place.
  • The version: top-level key is obsolete in Compose v2 and should be deleted outright.
  • docker compose config -q validates the file and fails loud, before up ever runs - run it as a habit, not a last resort.

What a Compose File Is Actually Built From

A compose file has exactly one required top-level key: services. Everything else - networks, volumes, configs, secrets - is an optional declaration that most people reach for before they need to, because tutorials show them side by side as if they're equally mandatory. They aren't. The official services reference is explicit that services is where containers are actually defined; the rest exist to be referenced from a service, not to stand alone.

That distinction matters for debugging: if docker compose up fails with something like "service must be a mapping," the problem is almost always indentation under services, not a missing networks or volumes block. Start there before touching anything else.

The Syntax Mistakes That Don't Throw Errors

The mistakes that actually waste an evening aren't the ones Compose rejects outright - those at least point you at a line number. It's the ones that parse fine and run fine, just not the way you meant:

Mistake What happens The fix
Duplicate key in the same mapping (e.g. pasting a second environment: block into one service) The last one silently wins; the first is discarded with no warning Run docker compose config and read the resolved output, not the source file
Mixing list and map syntax for environment: in the same service Compose accepts either form individually but errors or misparses when they're combined Pick one form (- KEY=value list, or KEY: value map) per service and stay consistent
Unquoted port mapping like 8080:8080 Usually fine, but 8080:8080:extra or a value containing a # can be parsed as a comment or mapping, not a string Quote port strings: "8080:8080"
yes / no / true / false as a bare value YAML 1.1 parsers coerce these to booleans even when you meant the literal string Quote them: "yes"
Tabs for indentation YAML's spec forbids tabs; some parsers silently fail, others error with a line number that doesn't point at the actual tab Use spaces only, consistently (2-space is the de facto standard)

None of these produce a stack trace. That's the common thread - a compose file with one of these mistakes looks, to docker compose up, exactly like a correct one.

Warning: the duplicate-key mistake is the one most likely to cost you time, because it happens silently during a copy-paste edit - duplicating a whole service block to create a second one, then forgetting to remove an old key from the original. Nothing in the logs flags it. The only way to catch it is to look at the resolved config, not the file you wrote.

Stop Repeating Yourself: YAML Anchors and Aliases

In my homelab compose file, three services used to share almost the same environment: block - timezone, log level, a shared DB host - copy-pasted into each one. Every time I changed the timezone, I had three places to remember to update, and the duplicate-key mistake above happened to me exactly this way once, when I pasted a fourth service and left a stray leftover key behind.

YAML anchors fix this at the file level, not the Compose level - they work because Compose files are plain YAML, and a walkthrough of anchors and extensions in Compose builds on the same x- prefix convention the wider ecosystem uses for anchor-only blocks:

x-common-env: &common-env
  TZ: Europe/Berlin
  LOG_LEVEL: info

services:
  app:
    image: myapp:latest
    environment:
      <<: *common-env
      SERVICE_NAME: app

  worker:
    image: myapp-worker:latest
    environment:
      <<: *common-env
      SERVICE_NAME: worker

The x-common-env block is ignored by Compose itself - any top-level key starting with x- is - which is exactly why it's safe to use purely as an anchor target. &common-env names the block; <<: *common-env merges its keys into each service's environment:, and a key defined locally (like SERVICE_NAME) overrides the anchored value if the names collide. Change the timezone once, in one place, and both services pick it up.

Note: the << merge-key syntax is a YAML extension, not part of the strict YAML 1.2 core spec - but Compose's parser supports it, which is why this pattern works reliably in docker-compose.yml even though some generic YAML tools reject it.

Delete the version: Key

If your compose file still starts with version: "3.8", delete that line. In Compose v2 the schema is unified and versionless - the key is ignored, and recent releases print a deprecation warning rather than respecting the value at all. There's no behavior it still controls; keeping it only adds a line that looks load-bearing and isn't.

Validate Before You Deploy: docker compose config

docker compose config renders the fully resolved configuration - every anchor expanded, every ${VAR} interpolated, every short-form shortcut written out in full - without starting a single container. That resolved output is what actually gets sent to the Docker engine, which makes it the fastest way to catch a duplicate key, a bad interpolation, or a typo'd service name before up ever runs.

Run it in validate-only mode, which prints nothing on success and a real error on failure - scriptable, unlike squinting at the full rendered YAML:

docker compose config -q
echo $?   # 0 = valid, non-zero = broken, with a message explaining why

For the anchor example above, intentionally breaking it by duplicating the SERVICE_NAME key inside one service's environment: block still parses as valid YAML - but running docker compose config (without -q) shows you the merged result, and the duplicate collapses to whichever value came last, exactly as the table above describes. Reading that resolved output is how you catch it before the container ever starts with the wrong value.

Tip: add docker compose config -q as a pre-commit or pre-deploy step if you keep your compose files in git - it costs under a second and catches the exact class of mistake that otherwise surfaces as "why is this container doing the wrong thing" twenty minutes later. If the container starts but misbehaves anyway, our guide to debugging a broken Compose stack covers the next layer down.

A Pre-Deploy Checklist

  1. Run docker compose config -q - exit code 0 means the file at least parses and resolves cleanly.
  2. Run docker compose config (no -q) and scan for any value that doesn't match what you wrote in the source file - that mismatch is a duplicate key.
  3. Confirm there's no version: line left at the top.
  4. Confirm every environment: block in the file uses one syntax (list or map), not both.
  5. Confirm port mappings and any value that could be misread as yes/no/true are quoted.
3
distinct YAML mistakes in this guide that produce zero error output on docker compose up - my own test on this homelab stack

None of the three - duplicate keys, mixed env syntax, unquoted booleans - fail loudly. They fail by doing something other than what you wrote, which is a worse failure mode because nothing tells you to go look. Catching them is a habit (run config before up), not a one-time fix. Once your file validates cleanly, our guide to Docker Compose restart policies is a reasonable next stop - it covers what happens when a container does start correctly but keeps dying anyway.

FAQ

What is a docker-compose.yml file used for?

It defines one or more containers (services) and how they're built, configured, and connected - including optional shared networks and persistent volumes - so docker compose up can start the whole stack from one file instead of a long sequence of docker run commands.

Why is my docker-compose.yml not working?

Most failures that produce an actual error point at indentation or a missing services key. The failures that don't produce an error - a duplicate key, mixed environment: syntax, an unquoted value Compose reads as a boolean - are the ones worth checking for specifically, because the stack starts fine and misbehaves anyway.

How do I validate a docker-compose file before deploying?

Run docker compose config -q. It resolves the full configuration - anchors expanded, variables interpolated - without starting any container, and exits non-zero with a message if anything is invalid.

Is the version key required in docker-compose.yml?

No. In Compose v2 it's obsolete - the schema is unified and versionless, and the key is ignored (with a deprecation warning on recent releases). It's safe to delete.

Can I use YAML anchors in a docker-compose file?

Yes. Define a block once under an x--prefixed key (ignored by Compose itself), anchor it with &name, and merge it into any service's mapping with <<: *name. It's the standard way to share config like timezone or log level across services without copy-pasting it.

A compose file earns trust the same way any config file does: by being boring. If it only ever does exactly what the source text says, you stop needing to re-read it every time something looks off. For the broader setup this fits into, see our guide to Docker Compose for self-hosting.

Related stories

More from Self-Hosting & Privacy

Stay in the loop

Get the latest articles delivered to your inbox. No spam, unsubscribe anytime.

Read next

Google Password Manager PIN: What It Protects

A new phone demanded a Google Password Manager PIN before your logins synced. Here's exactly what it protects, what it doesn't, and what happens if you lose it.

Continue Reading