← Back to Blog

YAML Anchors and Aliases: Stop Repeating Yourself (DRY Config)

If you maintain Docker Compose files, Kubernetes manifests, or CI/CD pipelines, you have almost certainly copy-pasted the same environment variables, resource limits, or job templates dozens of times. YAML anchors and aliases exist precisely to fix that problem. Here is everything you need to know.

The Problem: Config Duplication Is Everywhere

Consider a typical Docker Compose file for a web application with three services: a web server, a background worker, and a scheduler. All three need the same environment variables - database URL, Redis URL, secret key - and the same logging configuration. Without anchors, you end up writing this:

services:
  web:
    image: myapp:latest
    environment:
      DATABASE_URL: postgres://db:5432/myapp
      REDIS_URL: redis://redis:6379
      SECRET_KEY: changeme
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  worker:
    image: myapp:latest
    environment:
      DATABASE_URL: postgres://db:5432/myapp   # duplicated
      REDIS_URL: redis://redis:6379             # duplicated
      SECRET_KEY: changeme                      # duplicated
    logging:
      driver: json-file
      options:
        max-size: "10m"     # duplicated
        max-file: "3"       # duplicated

  scheduler:
    image: myapp:latest
    environment:
      DATABASE_URL: postgres://db:5432/myapp   # duplicated again
      REDIS_URL: redis://redis:6379
      SECRET_KEY: changeme
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

When you need to rotate SECRET_KEY or change the database URL, you must update it in three places. Miss one and you have a subtle bug. YAML anchors eliminate this entirely.

Anchors and Aliases: The Core Syntax

YAML provides two operators for reuse:

  • &anchor-name - defines an anchor, marking a node for later reference
  • *anchor-name - creates an alias, inserting the anchored value verbatim

The simplest case - reusing a scalar value:

default_image: &image myapp:latest

services:
  web:
    image: *image   # expands to: image: myapp:latest
  worker:
    image: *image   # expands to: image: myapp:latest

Anchors can be placed on any YAML node: scalars, sequences (lists), and mappings (objects). The anchor name can be any alphanumeric string with hyphens or underscores. The & symbol goes immediately before the anchor name with no space.

The Merge Key Operator: <<

Anchors by themselves replace entire nodes. The real power comes from the merge key operator <<, which lets you merge an anchored mapping into another mapping while overriding individual keys:

defaults: &defaults
  adapter: postgres
  host: localhost
  port: 5432
  pool: 5

development:
  <<: *defaults
  database: myapp_dev

test:
  <<: *defaults
  database: myapp_test
  pool: 2    # override the pool size for test

production:
  <<: *defaults
  host: prod-db.example.com    # override host
  database: myapp_prod
  pool: 20                     # override pool

After parsing, development is equivalent to:

development:
  adapter: postgres
  host: localhost
  port: 5432
  pool: 5
  database: myapp_dev

And production gets adapter: postgres, port: 5432 from the anchor, but its own host, database, and pool values. Keys defined in the local mapping take precedence over keys from the anchor. This is the fundamental rule of the merge key operator.

Real Example: Docker Compose with Anchors

Here is the Docker Compose file from the introduction, rewritten using anchors:

x-common-env: &common-env
  DATABASE_URL: postgres://db:5432/myapp
  REDIS_URL: redis://redis:6379
  SECRET_KEY: changeme

x-logging: &logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:
  web:
    image: myapp:latest
    environment:
      <<: *common-env
    logging:
      <<: *logging

  worker:
    image: myapp:latest
    environment:
      <<: *common-env
      WORKER_CONCURRENCY: "4"   # extra key for worker only
    logging:
      <<: *logging

  scheduler:
    image: myapp:latest
    environment:
      <<: *common-env
    logging:
      <<: *logging

The x- prefix is a Docker Compose convention for extension fields that are not interpreted as service definitions. This is the standard pattern for defining reusable blocks at the top of a Compose file.

The x- extension field convention is not YAML - it is a Docker Compose feature. In plain YAML for other tools (like Kubernetes or Ansible), you would typically define anchors under a custom top-level key or use a YAML document header block.

Real Example: GitHub Actions CI/CD

YAML anchors are extremely useful in CI/CD pipelines where multiple jobs share common steps:

x-node-setup: &node-setup
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'
  - run: npm ci

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - *node-setup   # note: list item alias, not merge
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - *node-setup
      - run: npm test

  build:
    runs-on: ubuntu-latest
    steps:
      - *node-setup
      - run: npm run build

Note the difference here: when the anchor is a sequence (list), you use - *anchor-name to insert it as a list item. This inserts the entire anchored sequence as a single element. If you want to merge sequences, you need to use a different approach or restructure your anchor.

Real Example: Rails Database Configuration

The Rails database.yml file is the canonical example of YAML anchors in the wild:

default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV["DB_USERNAME"] %>
  password: <%= ENV["DB_PASSWORD"] %>
  host: <%= ENV["DB_HOST"] %>

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

staging:
  <<: *default
  database: myapp_staging
  pool: 10

production:
  <<: *default
  database: myapp_production
  pool: 25

If you open the config/database.yml in any Rails app generated with PostgreSQL, you will see exactly this pattern. It is the framework's official recommendation.

Step-by-Step: Adding Anchors to an Existing Config

  1. Identify duplicated blocks. Look for mappings that appear identically (or near-identically) in multiple places. Common candidates: environment variables, resource limits, labels, logging config, timeout settings.
  2. Extract the common block. Move it to a top-level key (with an x- prefix in Docker Compose, or any descriptive name in plain YAML) and add &anchor-name after the key.
  3. Replace the original occurrences. Use <<: *anchor-name to merge the block. Add any per-instance overrides as sibling keys after the merge.
  4. Validate the result. Run the file through a YAML parser or validator to confirm it parses correctly. python3 -c "import yaml; yaml.safe_load(open('docker-compose.yml'))" is a quick sanity check.
  5. Test the actual tool. Run docker compose config to see the fully expanded YAML, or kubectl apply --dry-run=client for Kubernetes manifests. Confirm the expanded output matches your expectations.

Merging Multiple Anchors

You can merge more than one anchor into a single mapping by passing a list to <<:

x-env: &env
  DATABASE_URL: postgres://localhost/mydb
  REDIS_URL: redis://localhost:6379

x-limits: &limits
  cpus: '0.5'
  memory: 512M

services:
  web:
    image: myapp:latest
    environment:
      <<: *env
    deploy:
      resources:
        limits:
          <<: *limits
          memory: 1G    # override just memory for web

When merging multiple anchors, use a YAML sequence for the merge key:

combined:
  <<:
    - *anchor-one
    - *anchor-two
  local-key: local-value

In this case, anchor-one takes precedence over anchor-two for conflicting keys, and local-key takes precedence over both.

Validate Your YAML Instantly

Paste any YAML with anchors and aliases into our free validator to check syntax, see parse errors, and confirm your anchors expand correctly. 100% client side.

Open YAML Validator

Important Limitations to Know

YAML anchors are powerful but have constraints that trip up developers who do not know about them:

  • Same document only. Anchors cannot reference nodes in a different YAML file. If you need cross-file reuse, use a tool like Helm (for Kubernetes), Jsonnet, or CUE, which are specifically designed for this purpose.
  • Anchors are resolved at parse time. The anchor definition must appear before any alias that references it in the document. A forward reference to an anchor that has not been defined yet is a parse error.
  • Merge key is a YAML 1.1 feature. The << merge key is specified in YAML 1.1 but was NOT included in the YAML 1.2 specification. Most parsers support it anyway, but strictly compliant YAML 1.2 parsers may reject it. In practice, all major tools (PyYAML, Go's yaml.v3, js-yaml, Ruby's Psych) support it.
  • Tool support varies. Kubernetes' kubectl does not support YAML anchors (it processes YAML files individually through a Go YAML parser that strips anchors). Anchors only work in contexts where a single YAML file is parsed at once by a supporting parser. Docker Compose supports anchors. Ansible supports anchors. GitHub Actions supports anchors in workflow files.
  • Anchors on sequences use different syntax. Merging lists requires special handling; the << merge key only works for mappings (objects), not sequences (arrays).

Frequently Asked Questions

Can I use anchors across multiple YAML files?

No. YAML anchors are scoped to a single document. For multi-file reuse, you need a higher-level templating tool. In Kubernetes, Helm uses Go templates with {{ template "common.labels" . }}. In Terraform, you use modules. In Ansible, you use roles and variable files. Jsonnet and CUE are dedicated configuration languages that solve cross-file reuse natively.

Does Kubernetes support YAML anchors in manifests?

Not directly. The kubectl CLI parses YAML using a strict Go YAML parser that does not preserve anchors after parsing. Each manifest file is processed independently. If you need anchor-like reuse in Kubernetes, use Helm, Kustomize, or a GitOps tool that preprocesses YAML before applying it. Kustomize's commonLabels and patches cover many of the same use cases as anchors.

What is the difference between an alias and a merge?

An alias (*anchor-name) replaces the entire node with the anchored value. If you alias a mapping, you get that exact mapping with no ability to override individual keys. A merge (<<: *anchor-name) inserts the anchored mapping's key-value pairs into the current mapping, but allows you to override specific keys by defining them locally. Use aliases when you want an exact copy; use merges when you want to inherit and override.

Can anchors cause circular references?

Technically, the YAML specification allows nodes to be referenced multiple times (aliases), but circular references where a node refers back to itself are not valid. Most parsers will detect and reject circular references as parse errors. In practice, circular references do not occur in configuration files - you are always anchoring a block defined earlier in the document and referencing it later.

Are YAML anchors supported in JSON output?

When a YAML file with anchors is parsed, the anchors are resolved into their full values before the data is handed to your application. The resulting in-memory structure has no knowledge of anchors. If you serialize that structure back to JSON, you will get normal JSON with all values fully expanded. Anchors are a YAML authoring convenience; they do not exist in the parsed representation.

Why does my YAML validator say anchors are not supported?

Some strict JSON-schema-based validators process YAML by converting it to JSON first, which resolves anchors but may fail on certain anchor patterns before conversion. If your validator rejects valid anchor syntax, try a dedicated YAML validator that parses the full YAML spec. Our YAML Validator supports anchors, aliases, and the merge key operator.

The Bottom Line

YAML anchors and aliases are one of the most underused features in the YAML specification. They eliminate the copy-paste duplication that makes large configuration files fragile and hard to maintain. The syntax is minimal: &anchor-name to define, *anchor-name to reference, and <<: *anchor-name to merge with overrides.

The pattern pays off immediately in any file with more than two or three duplicated blocks. If you maintain Docker Compose files, Rails database configs, or GitHub Actions workflows, anchors should already be part of your toolbox.

Use our free tool here → YAML Validator to check your anchor syntax, inspect parse errors, and confirm your config expands correctly before committing.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.