YAML Anchors and Aliases — Reduce Repeat in Your Configs

If you have ever maintained a Docker Compose file with three services that share the same environment variables, or a Kubernetes manifest where every container has identical resource limits, you have felt the pain of YAML repetition.

YAML has no native "variable" system. No const, no let, no import. Just raw data.

But it does have anchors and aliases — two features that, once understood, dramatically reduce duplication in configuration files.

Anchors (&name) mark a node for reuse. Aliases (*name) reference that node elsewhere. Merge keys (<<:) combine multiple anchors into one structure.

This guide covers exactly how they work, where they shine, and the pitfalls that still trip up experienced developers.


Anchors and Aliases: The Basics

Defining an Anchor

An anchor marks a YAML node so it can be referenced later:

defaults: &defaults
  image: nginx:latest
  ports:
    - "80:80"

The &defaults syntax attaches the name defaults to that mapping node.

Using an Alias

Reference the anchored node with *name:

web:
  <<: *defaults
  hostname: web-server

The <<: merge key tells YAML to merge the anchor's key-value pairs into the current mapping.

Result

web:
  image: nginx:latest
  ports:
    - "80:80"
  hostname: web-server

Without <<:, a bare alias replaces the entire node rather than merging keys. Use <<: *name for merging; use *name for full replacement.


Real-World Docker Compose Example

This is where anchors save the most lines.

version: "3.8"

x-shared: &shared
  restart: unless-stopped
  networks:
    - app-network
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"

x-environment: &env
  NODE_ENV: production
  LOG_LEVEL: info

services:
  api:
    <<: *shared
    image: api:latest
    environment:
      <<: *env
      API_PORT: 3000

  worker:
    <<: *shared
    image: worker:latest
    environment:
      <<: *env
      QUEUE_NAME: tasks

  cron:
    <<: *shared
    image: cron:latest
    environment:
      <<: *env
      SCHEDULE: "*/5 * * * *"

Without anchors, this file would be nearly 60 lines of repetition. With anchors, it is half that size.

The x-shared prefix is a YAML convention — parsers ignore unrecognized top-level keys, so x- namespaces serve as "variable definition" blocks.


Anchors in Kubernetes Manifests

Kubernetes manifests are deeply repetitive. Every container needs resource limits. Every Deployment has metadata. Anchors keep them manageable.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: api
          image: api:latest
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
        - name: sidecar
          image: sidecar:latest
          resources:  # same block repeated
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"

With anchors:

x-resources: &resources
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "500m"

spec:
  containers:
    - name: api
      image: api:latest
      resources: *resources
    - name: sidecar
      image: sidecar:latest
      resources: *resources

Every container inherits the same resource block. When the values change, update them in one place.


Advanced Patterns: Overriding Anchor Values

Anchors support partial overrides through merge keys.

x-base: &base
  image: nginx:latest
  ports:
    - "80:80"
  environment:
    - ENV=production

web:
  <<: *base
  ports:
    - "80:80"
    - "443:443"
  environment:  # overrides the entire environment block
    - ENV=production
    - SSL_ENABLED=true

Important: Overriding a mapping key (environment in the example) replaces the entire mapping from the anchor. There is no deep merge — only top-level key replacement.

This limitation surprises many developers. If the anchor defines five environment variables and the override defines two, only those two exist in the final output. The anchor's other three are lost.


Anchors with Lists

Anchors also work on list nodes:

x-ports: &default-ports
  - "80:80"
  - "443:443"

services:
  web:
    image: nginx
    ports: *default-ports
  admin:
    image: admin-ui
    ports: *default-ports

To extend a list anchor, use merge keys with lists (supported by most parsers):

web:
  image: nginx
  ports:
    - "8080:80"  # additional port
    - *default-ports   # ❌ does not work like this

Lists are not merged by <<:. You must explicitly flatten them, which most YAML libraries do not support natively. This is a genuine limitation — list composition with anchors is awkward in standard YAML.


Nested Anchors

Anchors can reference other anchors:

x-logging: &logging
  logging:
    driver: json-file

x-monitoring: &monitoring
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost"]

x-full: &full
  <<: [*logging, *monitoring]

services:
  app:
    <<: *full
    image: app:latest

The <<: merge key accepts a list of anchors. The resulting mapping contains all keys from both referenced anchors.


Common Pitfalls

Pitfall 1: Anchor Scope

Anchors are scoped to a single YAML file. They do not cross document boundaries (--- separators create new documents where anchors defined earlier are invisible).

# File starts here
x-default: &default
  key: value

---
# New document — &default is NOT available here
data:
  <<: *default  # Error: unknown anchor

If you need shared anchors across files, use file inclusion mechanisms (Docker Compose extends, Kubernetes Kustomize, or a preprocessing step).

Pitfall 2: No Deep Merge

As mentioned earlier, <<: does a shallow merge. Nested mappings under a key are replaced, not merged.

x-base: &base
  spec:
    containers:
      - name: app

web:
  <<: *base
  spec:
    replicas: 3
  # spec is overwritten — containers is lost

The entire spec key is replaced. The containers definition from the anchor disappears.

Pitfall 3: Merge Order Matters

When merging multiple anchors, later keys override earlier ones:

x-base: &base
  image: nginx
  port: 80

x-override: &override
  port: 443

web:
  <<: [*base, *override]
  hostname: web

Result: port: 443 (override wins), image: nginx, hostname: web.


Anchors in CI/CD Pipelines

GitHub Actions workflows benefit from anchors for reusable step configurations:

x-cache: &cache
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

x-notify: &notify
  uses: slack-action/notify@v2
  with:
    status: ${{ job.status }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - *cache
      - run: npm test
      - *notify

  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - *cache
      - run: npm run build
      - *notify

Each job reuses the same cache and notification steps without duplication.


Anchors in Ansible

Ansible playbooks use anchors for reusable task blocks:

x-task-defaults: &task-defaults
  become: yes
  become_user: deploy
  environment:
    RAILS_ENV: production

- name: Deploy application
  <<: *task-defaults
  command: cap production deploy

- name: Restart services
  <<: *task-defaults
  service:
    name: puma
    state: restarted

When Not to Use Anchors

Anchors are powerful but not always the right choice:

1. Simple one-off overrides. If you only repeat a value twice, the indirection of anchors adds more syntax than it saves.

2. Cross-file reuse. Anchors cannot span files. Use Kustomize, Helm templates, or a proper templating engine instead.

3. Dynamic values. Anchors are static. If values change at runtime or across environments, use environment variables or template substitution.

4. Complex nesting. If your anchor structure requires three levels of <<: with list merging, the resulting configuration is harder to debug than the duplication it replaced.


Testing Anchored YAML

Anchors and aliases resolve at parse time. The best way to verify your configuration is to parse it and inspect the output:

import yaml

with open("docker-compose.yml") as f:
    config = yaml.safe_load(f)

print(yaml.dump(config, default_flow_style=False))

This resolves all anchors and shows the final structure. If something looks wrong, you will see it immediately.

If you are troubleshooting an anchored YAML file that produces unexpected results, paste it into a YAML formatter that resolves anchors — the expanded output makes misconfigured overrides obvious.

For more on YAML syntax quirks that affect anchored structures, see Why Your YAML Is Invalid and How to Fix YAML Indentation Errors.


FAQ

What is a YAML anchor?

A YAML anchor is a named reference to a YAML node, defined with &name syntax. Once defined, the anchored node can be referenced elsewhere in the same file using an alias (*name) or merged into another mapping using the merge key (<<: *name). Anchors reduce duplication by allowing you to define a block once and reuse it in multiple places. Common use cases include shared environment variables in Docker Compose, identical resource limits across Kubernetes containers, and reusable CI/CD job steps.

What is the difference between *name and <<: *name in YAML?

*name is a simple alias that inserts the exact anchored node in place — useful for replacing an entire value with the anchored content. <<: *name is a merge key that spreads the anchor's key-value pairs into the current mapping, allowing overrides. Use *name when you are replacing a scalar or list, like resources: *default-resources. Use <<: *name when you want to inherit configuration keys while potentially overriding some of them, like a Docker Compose service that inherits shared restart policy but adds its own ports.

Can YAML anchors be used across multiple files?

No. Anchors and aliases are strictly scoped to a single YAML file. A document separator (---) also resets the anchor namespace — any anchor defined before --- is invisible in subsequent documents. For cross-file reuse, you need platform-specific mechanisms like Docker Compose extends, Kubernetes Kustomize overlays, Helm templates, or a preprocessing step that concatenates files before parsing.

Do all YAML parsers support anchors and aliases?

Most production-grade YAML parsers support anchors and aliases, but support varies. PyYAML and ruamel.yaml support them fully. Go-based parsers (used by Kubernetes and Docker) also support them. However, some online YAML validators and formatters do not resolve anchors — they may display the raw &name and *name syntax without expanding the references. Always test anchor-heavy YAML with the parser your target platform actually uses, not a generic validator.

Can I override specific keys from a YAML anchor?

Yes, but only at the top level of the merged mapping. When you merge an anchor with <<: *name and then define additional keys, overlapping keys from your local mapping override the anchor's values. However, this is a shallow merge — if a key contains a nested mapping, overriding it replaces the entire mapping, not individual nested keys. There is no deep merge in standard YAML. If you need selective nested overrides, combine multiple fine-grained anchors instead of one large monolithic anchor.


Final Thoughts

YAML anchors and aliases are one of the few features that make large configuration files genuinely maintainable.

Without them, Docker Compose files with multiple services become exercises in copy-paste maintenance. Kubernetes manifests balloon to unmanageable sizes. CI/CD workflows repeat the same setup steps across every job.

The key is knowing when anchors help and when they add complexity. Use them for truly repeated blocks — resource limits, environment variables, logging configs, cache steps. Avoid them for one-off values that happen to look similar.

And always verify the resolved output before deploying. A YAML formatter that expands anchors is the fastest way to catch unexpected merge behavior before it reaches production.