DRY configuration in Docker Compose
Once a compose.yaml contains more than three or four services, logging configuration, environment variables and healthcheck definitions repeat almost identically in every block. YAML anchors, aliases and merge keys let you define these blocks once and reuse them everywhere, without Docker Compose needing its own dedicated feature for it.
Table of Contents
- 1. Why repetition in compose.yaml becomes a problem
- 2. YAML anchors and aliases: the basics
- 3. Merge keys: extending an anchor instead of fully replacing it
- 4. Extension fields: the x- prefix as a clean storage place
- 5. Practical example: shared logging configuration
- 6. Practical example: shared environment variables and healthchecks
- 7. Limits of anchors compared to include and override files
- 8. Common mistakes when using anchors
- 9. Anchors compared to alternatives
- 10. Summary
- 11. FAQ
1. Why repetition in compose.yaml becomes a problem
The more services a Docker Compose file contains, the more certain blocks repeat. Logging drivers, restart policies, healthcheck definitions and entire environment variable lists show up identically or nearly identically in almost every service. If a setting changes, for example the maximum log file size, the same change has to be repeated in five, ten or more places, which invites mistakes and makes reviews harder.
This is exactly where YAML anchors and aliases come in. YAML, the format behind every compose.yaml, has long supported its own referencing syntax that lets a block be defined once and reused at any number of places in the document. Docker Compose itself does not need its own feature for this, because the anchor and alias syntax is already part of the YAML specification and is understood by every conforming YAML parser, including the one Docker Compose uses internally.
The advantage over plain repetition is obvious: changes at a central location automatically apply to every service that references the corresponding anchor. This not only reduces file size, but above all the risk that a service gets forgotten or misconfigured during manual copying. For teams with multiple microservices or a Magento stack with PHP-FPM, Nginx, Node and several databases, this is a direct maintainability win.
2. YAML anchors and aliases: the basics
An anchor is defined with the & character before a key name, for example &common-logging. This anchor marks the following block as reusable. An alias, introduced with the * character, references this block at another point in the document and inserts it exactly as it was defined at the anchor. These two characters, & and *, form the complete vocabulary for simple reuse in YAML.
Important to understand: anchors and aliases are a pure YAML feature, not a Docker Compose specific extension. That means they work regardless of where in the document they sit, as long as the anchor is defined before the alias. Inside a compose.yaml, anchors can be placed at the top level, inside a services block, or in extension fields specifically created for that purpose, which matters for organizing the file.
# compose.yaml — basic anchor and alias usage
services:
api:
image: myapp/api:latest
logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
worker:
image: myapp/worker:latest
logging: *default-logging # Reuses the exact same logging block
scheduler:
image: myapp/scheduler:latest
logging: *default-logging # Same logging config, defined only once
In this example, the logging configuration is defined once at the api service and reused exactly through the default-logging anchor in worker and scheduler. If the max-size value changes, a single change at the anchor location is enough, all three services automatically pick up the new configuration on the next docker compose up.
3. Merge keys: extending an anchor instead of fully replacing it
A plain alias inserts the referenced block unchanged, which is not enough in many cases. Often a service needs most of the settings from a shared block, but with one or two different values. For this case there is the merge key, written as <<: *anchor-name. The merge key inserts all keys of the referenced anchor into the current mapping and at the same time allows individual keys to be overridden right after it.
This combination of merge key and local override is the real added value of YAML anchors in Docker Compose, because real services are rarely one hundred percent identically configured. A typical pattern: a shared base block defines restart, logging and network settings, while each service inherits this base through the merge key and only adds its own image plus individual environment variables.
# compose.yaml — merge key extends a common base
x-common-service: &common-service
restart: unless-stopped
networks:
- backend
logging:
driver: json-file
options:
max-size: "10m"
services:
api:
<<: *common-service
image: myapp/api:latest
environment:
SERVICE_NAME: api
worker:
<<: *common-service
image: myapp/worker:latest
environment:
SERVICE_NAME: worker
restart: on-failure # Overrides the inherited "unless-stopped"
networks:
backend:
In the example, the worker service inherits all settings from common-service, but specifically overrides the restart policy to on-failure. This combination of central definition and local override makes merge keys significantly more flexible than plain aliases and is the reason why most production compose.yaml files with many services rely on this pattern.
4. Extension fields: the x- prefix as a clean storage place
Docker Compose validates the root level of a compose.yaml against a fixed schema that only allows certain top level keys such as services, networks, volumes and secrets. A custom key like common-service without a prefix would cause a validation error. For this reason, Docker Compose supports extension fields, top level keys starting with x- that Compose ignores while parsing, but that are fully valid as a storage place for anchors.
The x-name prefix convention is not a Docker Compose invention, but follows the same logic as x- headers in HTTP or x- prefixes in other configuration formats: a reserved namespace for extensions that the actual schema is allowed to ignore. For organizing large compose.yaml files, it is a good idea to bundle all shared anchors under several x- keys at the top of the file, for example x-common-service, x-healthcheck-defaults and x-logging-defaults, instead of scattering them between the services.
# compose.yaml — organizing anchors under x- extension fields
x-healthcheck-defaults: &healthcheck-defaults
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
x-php-base: &php-base
build:
context: .
dockerfile: docker/php/Dockerfile
volumes:
- ./src:/var/www/html
networks:
- backend
services:
php-fpm:
<<: *php-base
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "php-fpm-healthcheck"]
php-worker:
<<: *php-base
command: ["php", "bin/console", "worker:run"]
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "pgrep", "-f", "worker:run"]
networks:
backend:
5. Practical example: shared logging configuration
Logging is one of the most common examples of useful YAML anchors in practice, because almost every service needs the same logging strategy. Without anchors, the json-file driver with max-size and max-file would have to be manually repeated in every single service. With a central anchor, a single definition is enough, included through a merge key or a direct alias in every service, and a later adjustment to log rotation automatically affects the entire stack.
Especially in production like local development environments where multiple containers write logs in parallel, a central logging configuration also prevents individual services from accidentally running without a size limit and letting the Docker log files on the host grow uncontrolled. This is a detail that is easily overlooked when copied manually, but automatically applies to all services with a central anchor definition.
6. Practical example: shared environment variables and healthchecks
A second common use case is environment variables shared by multiple services, for example database credentials or a shared API endpoint. Instead of listing these variables again in every service, you define an anchor with the shared values and only add each service's individual variables through a second environment section or an env_file reference.
Healthchecks also benefit strongly from anchors, because interval, timeout and retries rarely differ in a stack with several similar services. Only the actual test command is usually service specific. With a merge key, the healthcheck framework can be defined once, while each service only adds its own test command, which ensures consistency of the healthcheck parameters across the entire stack.
# compose.yaml — shared environment variables via anchor
x-db-credentials: &db-credentials
DB_HOST: mysql
DB_PORT: "3306"
DB_NAME: magento
DB_USER: magento
services:
php-fpm:
image: myapp/php:8.4-fpm
environment:
<<: *db-credentials
DB_PASSWORD: ${DB_PASSWORD}
APP_ENV: development
cron:
image: myapp/php:8.4-cli
command: ["php", "bin/magento", "cron:run"]
environment:
<<: *db-credentials
DB_PASSWORD: ${DB_PASSWORD}
APP_ENV: cron
7. Limits of anchors compared to include and override files
YAML anchors solve repetition within a single file, but they do not work across multiple files. An anchor defined in compose.yaml cannot be referenced in compose.override.yaml, because YAML parses every file as its own standalone document. For reuse across file boundaries, Docker Compose instead offers the include directive or multiple -f flags on the docker compose command, which operate on a completely different level than YAML anchors.
Another point: anchors cannot be applied conditionally. There is no way to include an anchor only under certain conditions, for example depending on a Compose profile. Anyone who needs conditional configuration still has to work with profiles, separate override files, or environment variables combined with the default value syntax ${VAR:-default}. YAML anchors are a tool against textual repetition, not a tool for conditional logic.
8. Common mistakes when using anchors
The most common mistake is using an alias where a merge key was actually needed. A direct alias like logging: *default-logging replaces the entire value, while a merge key with <<: *anchor inserts the keys into the surrounding mapping and allows local overrides. Anyone who tries to define additional keys in the same mapping after a direct alias gets a YAML parser error, because an alias represents a complete value and does not tolerate further sibling keys in the same block.
# WRONG: alias replaces the whole value, cannot add sibling keys after it
services:
api:
logging: *default-logging
# driver: override <- would be a duplicate mapping key, invalid
# RIGHT: merge key allows extending and overriding
services:
api:
logging:
<<: *default-logging
driver: syslog # Overrides only the driver, keeps other options
A second common mistake concerns ordering: an anchor must be defined before the first alias that references it in the YAML document. If the anchor appears further down in the file than the alias, the parser reports an error, because the reference does not yet exist at the time it is processed. For this reason it makes sense to bundle all shared anchors under extension fields right at the beginning of the compose.yaml, before the services block.
9. Anchors compared to alternatives
Besides YAML anchors, there are other ways to reduce repetition in Docker Compose configurations. Each approach has a different scope, and the choice depends on whether the repetition occurs within a single file or across multiple files.
| Approach | Scope | Conditional logic possible | Use case |
|---|---|---|---|
| YAML anchors/aliases | Within a single file | No | Repeated blocks like logging, healthchecks |
| Merge keys (<<) | Within a single file | No | Base blocks with local overrides |
| include directive | Across multiple files | Partially (per file) | Entire service definitions from other files |
| compose.override.yaml | Across multiple files | Yes, per environment | Dev/test/CI specific deviations |
| ${VAR:-default} | Individual values | Yes, per variable | Parameterizing individual configuration values |
In practice these approaches do not exclude each other, they complement each other. YAML anchors reduce repetition within a file, while override files and the include directive avoid repetition across multiple environments. A well structured stack typically combines anchors for shared service building blocks with a compose.override.yaml for environment specific adjustments.
Mironsoft
Docker Compose architecture and multi service stacks
A compose.yaml that is not copy pasted for every service?
We restructure existing Docker Compose stacks with anchors, merge keys and extension fields, reduce repetition and make changes in one place instead of ten.
Compose refactoring
Converting existing stacks to anchors and merge keys without changing behavior
Extension fields
Introducing shared base blocks for logging, healthchecks and networks
Magento stacks
Consistently configuring PHP-FPM, cron, worker and Node build services
10. Summary
YAML anchors, aliases and merge keys solve a problem that every growing compose.yaml runs into sooner or later: identical blocks for logging, healthchecks and environment variables that get repeated manually in every service. With the & character, a block is marked as an anchor, with * it is referenced as an alias, and with the merge key << an anchor can be inserted into a mapping while also being locally overridden. Extension fields with the x- prefix provide a clean, Compose ignored storage place for this at the top of the file.
The biggest win lies in maintainability: a change at a central anchor location automatically applies to all referencing services, instead of having to be manually applied in multiple places. For repetition across multiple files, for example between compose.yaml and compose.override.yaml, anchors are not suitable, here the include directive or separate override files take on that role instead.
YAML Anchors and Aliases in Docker Compose — The Essentials at a Glance
Anchor (&)
Marks a block as reusable, directly at the point of definition in the document.
Alias (*)
Inserts the referenced block exactly and unchanged at another point.
Merge key (<<)
Inserts the keys of an anchor into a mapping and allows local overrides.
Extension fields (x-)
Top level keys ignored by Compose, an ideal storage place for shared anchors.