Using Anchors to Reduce Repetition in YAML Config Files
Put shared settings in one anchored block and merge it into each service. The web service replaced the whole environment block, since the merge is shallow.

A Compose file with eight services, each with the same restart policy, the same logging driver and the same time zone. A CI configuration with twelve jobs that share a block of cache settings. The first draft repeats the lines. YAML's answer is the anchor: define the block once, refer to it by name wherever you need it. It removes repetition and adds a form of inheritance, and the inheritance has a rule that catches people. This post builds a working example, shows the rule, and gives guidelines for using anchors without making the file harder to read.
The pattern
x-defaults: &defaults
restart: unless-stopped
logging: {driver: json-file}
environment: {TZ: UTC}
services:
web:
<<: *defaults
image: nginx:1.25
environment: {TZ: Europe/Oslo, MODE: prod}
worker:
<<: *defaults
image: app:2Three pieces of syntax. &defaults names the mapping under x-defaults. <<: *defaults inside a service is the merge key: it copies the entries of the anchored mapping into this one. Keys written locally take precedence. The key x-defaults starts with x- on purpose: Docker Compose and several other tools reserve top-level keys with that prefix for extensions, which they ignore, so the reusable block does not become a service or fail validation. Other tools have their own conventions (a leading dot in GitLab CI, for example); use the one your tool documents.
What each service gets
Loading the file and printing the two services:
web : {"restart": "unless-stopped", "logging": {"driver": "json-file"}, "environment": {"TZ": "Europe/Oslo", "MODE": "prod"}, "image": "nginx:1.25"}
worker : {"restart": "unless-stopped", "logging": {"driver": "json-file"}, "environment": {"TZ": "UTC"}, "image": "app:2"}Both inherited restart and logging, which is the saving: two lines were not repeated, and a change to the restart policy is one edit. The environments are the lesson. worker has {"TZ": "UTC"} from the anchor. web defined its own environment, and its result is exactly its own mapping, {"TZ": "Europe/Oslo", "MODE": "prod"}. The anchored environment is not combined with it; it is replaced. If the anchor had had a third variable, web would have lost it.
That is what "the merge is shallow" means (YAML anchors, aliases and merge keys without surprises states the rule). The merge key copies top-level keys, and a key that exists locally wins as a whole. It does not look inside the values. People expect inheritance, where web's environment would extend the default, and get replacement. If web should keep the anchored variables and add MODE, you must restate TZ:
environment: {TZ: UTC, MODE: prod}or restructure so the thing that differs is a separate key (extra_environment) and let the application combine them. Do not rely on the reader to remember the rule; put a comment where it matters.
Guidelines
Anchor settings that are genuinely repeated. Two to five lines used by several entries. A single repeated value is clearer written out.
Name anchors for what they are. &default_logging, &common_env, not &a. The name is documentation.
Put them together and early. One block at the top, labeled, so a reader finds the definitions in one place. An anchor must be defined before it is used in the file.
Keep chains short. An anchor that merges another anchor that merges a third is an inheritance tree the reader must hold in their head. One level is easy, two is tolerable, three is a puzzle.
Mind the shared object. In memory, aliased nodes may be the same object. If your program modifies a loaded structure, a change to one service's logging block can change the others'. Deep-copy before modifying, or treat configuration as immutable.
Check the tools that read the file. Some tools expand anchors before processing, some do not support the merge key, and some validate the file before expansion and reject the extension key. Test with the real consumer. The merge key is also not part of the YAML 1.2 core schema, though most libraries support it (YAML 1.1 versus 1.2).
Lint. yamllint has rules for anchors (unused, undeclared and duplicated), which catch typos in names.
When not to use anchors
If the repeated part differs slightly in each use, anchors fit badly: you end up overriding half the keys, and the reader has to work out which half. If the file is edited by people under pressure, the indirection costs more than the repetition. If the consumer needs to read the file with a tool that does not expand anchors, you have introduced a compatibility problem.
Alternatives for the same goal:
Layered files: shared settings in a base file, differences in overlays, merged by a loader or a tool with an explicit deep merge (environment specific YAML config without copy and paste).
Templates: generate the repetitive file from a short description. Helm and Jsonnet do this for Kubernetes, and a ten-line script can do it for your own format. Review the generated output.
Tool-native reuse: Compose
extendsandinclude, CI templates and reusable workflows. They are understood by the tool's validator.
Anchors sit between plain repetition and these heavier options. They are the right tool when you have a short, stable block repeated across entries in one file, and the people reading the file know the syntax.
Reviewing a file with anchors
When you review a change in a file that uses anchors, look at the definition and at every use. A change to the anchored block changes every alias, and the diff shows one hunk. Check the overrides: does a local key replace something the author thinks it extends? Render the effective configuration for each entry, with yq or a few lines of Python, and read that instead of reasoning about inheritance in your head.
yq '.services | to_entries | .[] | .key + ": " + (.value.environment | to_json)' compose.yamlprints each service with its effective environment as indented JSON (the version tested expanded the merge keys as shown, and a flag for strict spec behavior can change that, so test your version). A short printout like this belongs in a pull request description for any change to a shared anchor.
Related reading
The cluster hub is structuring YAML configuration files that last. The syntax and the shared-object behavior are in YAML anchors, aliases and merge keys without surprises, and the alternative of layered files in environment specific YAML config without copy and paste. Docker documents the x- extension fields in the Compose specification, and the anchors in the YAML 1.2.2 specification. See the expanded result in the YAML to JSON tool.
When anchors make a config harder to maintain
The reader must scroll to understand a line. If every service needs the anchor definition open beside it, the saving is smaller than the cost.
Overrides are common. A block overridden in most uses is not a default.
The merge depth matters. If nested settings must extend and not replace, anchors cannot do it. Use a loader with a deep merge.
Different tools read the file. Anchors are a feature of the parser. Tools that preprocess or reimplement parsing may treat them differently.


