YAML Anchors, Aliases, and Merge Keys: Killing Duplication in Config Files
YAML anchors, aliases, and merge keys let you define configuration once and reuse it with local overrides. Here's the mechanism, a worked docker-compose example, and the limits that bite in practice.
27 Aug 2026, 00:08 UTC

Copy-pasting the same retry policy, environment list, or restart settings across a docker-compose file or CI pipeline works until someone edits three of the four copies. YAML has a built-in fix: anchors (&name) mark a node once, aliases (*name) reuse it, and the merge key (<<:) lets one mapping inherit another's keys with local overrides. Used well, this gives you a "defaults plus overrides" pattern without any templating tool.
The mechanism in one worked example
Here is a docker-compose-style file that defines shared defaults once and reuses them across services:
x-defaults: &service-defaults
image: registry.example.com/app:1.4
restart: always
environment: &common-env
LOG_LEVEL: info
TZ: UTC
services:
api:
<<: *service-defaults
ports:
- "8080:8080"
worker:
<<: *service-defaults
restart: unless-stopped
environment:
<<: *common-env
LOG_LEVEL: debug
Three things are happening. First, &service-defaults anchors the whole mapping under x-defaults (the x- prefix is a compose convention for extension fields, so the block itself is not treated as a service). Second, <<: *service-defaults merges those keys into each service. Third, worker overrides restart locally — a key defined directly in a mapping always wins over a merged key. The worker also merges the anchored common-env mapping and then overrides a single variable inside it, which is the standard trick for "same list, one value different."
Merge keys also accept a sequence: <<: [*base, *extra]. On key conflicts, the earlier anchor wins. That ordering is easy to misread, so if you layer merges, add a comment stating the precedence.
Verifying the expansion before you commit
Anchors and aliases are resolved at parse time, so the fastest check is to load the file with the same parser your platform uses and print the result. With Python and PyYAML (run locally, no special permissions needed):
python3 -c "import yaml, json, sys; print(json.dumps(yaml.safe_load(open('docker-compose.yml')), indent=2))"
Confirm that worker shows restart: unless-stopped and LOG_LEVEL: debug while inheriting everything else. Then validate against the actual platform, not just a generic parser — for compose, run docker compose config, which prints the fully merged effective configuration. For Kubernetes manifests, kubectl apply --dry-run=client -f file.yaml checks acceptance. A file can be perfectly valid YAML and still be rejected by a tool that doesn't implement merge keys.
Limits you need to know
Same document only. Aliases cannot reference anchors defined later in the file or in other files — YAML has no import mechanism. Multi-file reuse requires Helm, Kustomize, ytt, or similar templating.
Merge keys are a YAML 1.1 convention. The << key is widely implemented but is not part of the YAML 1.2 core spec. Strict 1.2 parsers may ignore or reject it. Check your loader's documentation — PyYAML's SafeLoader supports it, but support is version- and parser-specific.
Platform support is not guaranteed. Some ecosystems parse YAML with restricted loaders. GitHub Actions, for example, does not support anchors in workflow files even though the YAML is syntactically valid. Validity does not imply acceptance.
No parameterization. You cannot override one field inside an aliased scalar or sequence — merging only works on mappings. If your repeated block is a list that needs per-use variation, anchors won't save you, and teams often end up duplicating anyway.
Common mistakes
Deep merge chains. Stacking three or four merge levels makes the effective config nearly impossible to predict by reading. Keep it to one or two levels and prefer explicit overrides over clever layering.
Mutating aliased structures in code. Some parsers load aliases as shared references. If your application mutates a loaded structure in place, every alias of it changes too. Defensively copy shared nodes (copy.deepcopy in Python) before mutation.
Untrusted input. Aliases expand data, they don't compress it. A nested-alias "billion laughs" document can exhaust memory in naive parsers. Always use a safe loader (yaml.safe_load, SnakeYAML safe settings) for any YAML you didn't write, and check your parser's alias-expansion limits.
Assuming local-wins without testing. The override behavior your config depends on is a convention of the merge-key spec. Write a tiny test file with a conflicting key after a merge and confirm the local value wins in your exact parser version before building a large config on that assumption.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.