Stop Copy-Pasting YAML: Anchors, Aliases, and Merge Keys in Practice
YAML anchors, aliases, and merge keys let you define shared config once and override only what differs — if your parser supports them. A worked Docker Compose example, verification steps, and the limits.
18 Jul 2025, 09:45 UTC

Open any mature Docker Compose file or CI pipeline and you'll find the same five lines repeated a dozen times: the same retry policy, the same logging config, the same resource limits. Every copy is a future bug, because sooner or later someone updates nine of the ten copies. YAML has a built-in fix for this — anchors, aliases, and merge keys — and you don't need a templating engine to use it.
The thesis is simple: define a block once, reference it everywhere, and override only what's different. Here's how the three pieces fit together, where they break, and how to verify your parser actually supports them.
The three building blocks
An anchor (&name) labels a node in your document. An alias (*name) re-inserts that exact node elsewhere. A merge key (<<: *name) pulls a referenced mapping into the current mapping, letting local keys sit alongside — and override — the merged ones.
Anchors and aliases are core YAML and work in any compliant parser. Merge keys come from the YAML 1.1 spec and are widely implemented, but they're the piece most likely to surprise you, so treat them as "usually supported" rather than guaranteed.
A worked example: Docker Compose services
Say you run three worker services that share restart policy, logging, and resource limits, differing only in image and queue name:
x-worker-defaults: &worker-defaults
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
deploy:
resources:
limits:
memory: 512M
services:
emails:
<<: *worker-defaults
image: registry.example.com/emails:1.4
environment:
QUEUE: emails
reports:
<<: *worker-defaults
image: registry.example.com/reports:2.0
environment:
QUEUE: reports
deploy:
resources:
limits:
memory: 1G
cleanup:
<<: *worker-defaults
image: registry.example.com/cleanup:0.9
environment:
QUEUE: maintenanceThe x-worker-defaults top-level key is a Compose convention: keys starting with x- are ignored by Compose itself, so they exist purely to hold anchors. Each service merges the defaults and overrides only what it needs — reports gets a bigger memory limit while everything else stays shared. Local keys win over merged keys, which is the behavior you want, but confirm it with your parser (more on that below).
The same pattern works in GitLab CI (.hidden-job: &base merged into jobs) and in application config files where per-environment blocks share a common base.
Verify before you commit
Don't assume expansion worked — check it. With Python and PyYAML installed, run this locally:
python3 -c "
import yaml, json, sys
doc = yaml.safe_load(open('docker-compose.yml'))
print(json.dumps(doc['services']['reports'], indent=2))
"You should see restart and logging present on the reports service, and its memory limit showing 1G, not 512M. If the merged keys are missing or the override didn't take, your parser doesn't handle merge keys the way you expect. No special permissions are needed; the only risk is reading the wrong file, so check the path.
Also test the negative case: temporarily remove the anchor definition and confirm parsing fails loudly rather than silently producing a partial config.
Where this technique falls short
Three limitations matter in practice:
- Parser support varies. Merge keys (
<<) are a YAML 1.1 feature. Some strict YAML 1.2-only or minimal parsers ignore or reject them. Plain anchors and aliases are much safer bets; merge keys deserve an explicit test against the tool that will actually read the file. - Aliases don't cross boundaries. They're scoped to a single YAML document. You cannot anchor a block in one file and alias it from another. The moment you need cross-file reuse or parameterization, you've outgrown anchors — that's what Helm, Kustomize, Jsonnet, or CUE are for.
- Deep alias chains hurt humans. A config where
service CmergesB, which mergesA, which overrides half ofA, is harder to review than the duplication it replaced. Diffs also get noisier: changing a shared anchor touches every consumer at once. Keep chains to one or two levels.
One more subtlety: after parsing, some libraries hand you shared references to the same underlying object rather than independent copies. If your application mutates a merged structure at runtime, verify whether that mutation leaks into other aliases of the same anchor.
The practical rule
Use anchors and merge keys when the same tool reads the whole file and the duplication is mechanical — identical logging blocks, retry policies, resource limits. Reach for a real templating tool when you need parameters, conditionals, or reuse across files. And whichever you choose, add one parse-and-print check to your workflow so a silently ignored merge key never ships to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.