Skip to content

Docker Compose Extentions (User Defined)


As a Compose file grows, you may notice the same configuration appearing in several services. The services might use the same environment variables, logging settings, or other shared configuration.

Copying the same block into every service works, but it creates repetition.

Compose provides a useful pattern for this: user-defined top-level elements beginning with x-. Combined with YAML anchors, they let you define reusable configuration once and apply it to multiple services. These are called Extentions in compose file.

Why Would You Need x- Elements?

Imagine three services:

services:
  frontend:
    image: alpine
    environment:
      APP_MODE: development
      LOG_LEVEL: info

  backend:
    image: alpine
    environment:
      APP_MODE: development
      LOG_LEVEL: info

  worker:
    image: alpine
    environment:
      APP_MODE: development
      LOG_LEVEL: info

The configuration is repeated three times.

If you later change:

LOG_LEVEL=debug

you have to remember to change it in every service.

That creates two problems:

  1. The Compose file becomes unnecessarily large.
  2. One service can accidentally end up with different configuration.

A reusable configuration block can give you:

                 shared configuration
                         │
             ┌───────────┼───────────┐
             │           │           │
          frontend     backend     worker

Instead of repeating the same configuration, you define it once and reuse it.

What Is An x- Top-Level Element?

A top-level key beginning with x- is an extension field.

For example:

x-common-environment:
  APP_MODE: development
  LOG_LEVEL: info

services:
  frontend:
    image: alpine

  backend:
    image: alpine

The important part is:

x-common-environment:

Compose recognizes x- fields as user-defined extension fields and does not treat them as application services, networks, or volumes.

They are useful for storing configuration that you want to reuse.

Think of them as:

x-common-environment
        │
        └── reusable configuration

But there is an important detail:

An x- field by itself does not automatically apply its contents to services.

You need a YAML mechanism to reuse it.

That mechanism is called an anchor.

How Do YAML Anchors Help?

YAML allows you to give a piece of configuration a reusable name using &.

For example:

x-common-environment: &common-environment
  APP_MODE: development
  LOG_LEVEL: info

There are two parts:

&common-environment
        │
        └── name given to this configuration

The complete declaration is:

x-common-environment: &common-environment

Now another part of the YAML can reuse that configuration with *:

environment:
  <<: *common-environment

Here:

&common-environment
        │
        │ defines
        ▼
 shared configuration
        ▲
        │ reuses
        │
*common-environment

The <<: syntax tells YAML to merge the anchored mapping into the current mapping.

A Complete Example

Let’s build a small example.

Create a directory:

mkdir compose-extensions
cd compose-extensions

Create compose.yaml:

x-common-environment: &common-environment
  APP_MODE: development
  LOG_LEVEL: info

services:
  frontend:
    image: alpine
    command: ["sh", "-c", "echo frontend; sleep infinity"]
    environment:
      <<: *common-environment

  backend:
    image: alpine
    command: ["sh", "-c", "echo backend; sleep infinity"]
    environment:
      <<: *common-environment

  worker:
    image: alpine
    command: ["sh", "-c", "echo worker; sleep infinity"]
    environment:
      <<: *common-environment

Start it:

docker compose up -d

All three services receive the same environment configuration.

The important structure is:

x-common-environment
        │
        │ anchor
        ▼
&common-environment
        │
        ├──────────────┐
        │              │
        ▼              ▼
   frontend        backend        worker
        │              │             │
        └──────────────┴─────────────┘
              same configuration

You defined the configuration once.

What Does <<: Mean?

The syntax can look strange at first:

environment:
  <<: *common-environment

Break it into pieces:

<<:
 │
 └── merge this mapping here

*common-environment
 │
 └── use the configuration named by the anchor

So:

environment:
  <<: *common-environment

means roughly:

“Take the mapping stored in common-environment and merge it into this environment mapping.”

This is YAML behavior rather than a special Docker command.

Compose reads the YAML configuration after YAML’s anchors and aliases have been resolved.

Can A Service Add Its Own Values?

Yes.

This is where reusable configuration becomes especially useful.

Suppose all services share:

APP_MODE: development
LOG_LEVEL: info

but the worker needs one additional setting.

You can write:

x-common-environment: &common-environment
  APP_MODE: development
  LOG_LEVEL: info

services:
  frontend:
    image: alpine
    environment:
      <<: *common-environment

  backend:
    image: alpine
    environment:
      <<: *common-environment

  worker:
    image: alpine
    environment:
      <<: *common-environment
      WORKER_CONCURRENCY: "4"

The worker gets both the shared values and its own value:

worker
  │
  ├── APP_MODE=development
  ├── LOG_LEVEL=info
  └── WORKER_CONCURRENCY=4

The shared configuration remains centralized.

The service-specific configuration remains local to that service.

This gives you a useful pattern:

shared configuration
        │
        ├── frontend
        ├── backend
        └── worker
              │
              └── additional configuration

What If A Service Needs To Override A Shared Value?

A service can also provide its own value for a key.

For example:

x-common-environment: &common-environment
  APP_MODE: development
  LOG_LEVEL: info

services:
  frontend:
    image: alpine
    environment:
      <<: *common-environment

  backend:
    image: alpine
    environment:
      <<: *common-environment
      LOG_LEVEL: debug

The backend gets:

APP_MODE=development
LOG_LEVEL=debug

while the frontend gets:

APP_MODE=development
LOG_LEVEL=info

The service-specific value takes precedence over the merged value.

This gives you a useful mental model:

shared defaults
      │
      ▼
service configuration
      │
      └── service-specific value can override

Can You Reuse More Than Environment Variables?

Yes.

The same idea can be used for other YAML mappings that you want to keep consistent.

For example:

x-common-labels: &common-labels
  app: demo
  team: platform

services:
  frontend:
    image: alpine
    labels:
      <<: *common-labels

  backend:
    image: alpine
    labels:
      <<: *common-labels

The exact configuration you choose to reuse depends on your application.

The important idea is not a particular field.

The important idea is:

Define shared configuration once, give it an anchor, and reuse it where needed.

Why Not Just Copy And Paste?

Copying configuration is not always wrong.

For a very small Compose file, repetition may actually be easier to understand.

The problem appears when the same configuration is repeated many times.

For example:

Without reuse:

frontend → configuration A
backend  → configuration A
worker   → configuration A
api      → configuration A

If configuration A changes, every copy must be updated.

With an anchor:

             configuration A
                    │
       ┌────────────┼────────────┐
       ▼            ▼            ▼
   frontend      backend       worker

There is one source for the shared configuration.

This reduces duplication and makes the relationship explicit.

Tip

Don’t introduce anchors just to eliminate two repeated lines. They become valuable when several services genuinely share a meaningful configuration block.

x- Elements Are Not Services

This is an important distinction.

Consider:

x-common-environment: &common-environment
  APP_MODE: development

services:
  app:
    image: alpine

Compose does not try to start:

x-common-environment

as a container.

Only entries under:

services:

define services.

The x- element is simply user-defined configuration that can participate in the YAML structure.

Think of the two sections as having different purposes:

x-common-environment
        │
        └── reusable configuration


services
        │
        └── actual application services

How Does This Fit Into The Compose Mental Model?

You’ve now seen several top-level elements:

compose.yaml
     │
     ├── services
     ├── networks
     ├── volumes
     ├── secrets
     │
     └── x-... extension fields

They serve different roles:

ElementPurpose
servicesDefines application services
networksDefines communication networks
volumesDefines persistent storage
secretsDefines sensitive configuration
x-...Defines reusable user-defined configuration

The x- elements are different from the others.

They aren’t another type of Docker resource.

They are a way to organize and reuse configuration.

A More Complete Example

Let’s combine shared configuration with the application structure you’ve already seen.

x-common-environment: &common-environment
  APP_MODE: development
  LOG_LEVEL: info

services:
  frontend:
    image: alpine
    command: ["sleep", "infinity"]
    environment:
      <<: *common-environment
    networks:
      - frontend-network

  backend:
    image: alpine
    command: ["sleep", "infinity"]
    environment:
      <<: *common-environment
      LOG_LEVEL: debug
    networks:
      - frontend-network
      - backend-network

  database:
    image: alpine
    command: ["sleep", "infinity"]
    environment:
      <<: *common-environment
    networks:
      - backend-network
    volumes:
      - database-data:/data

networks:
  frontend-network:
  backend-network:

volumes:
  database-data:

Now the Compose file expresses several things at once:

                 shared environment
                         │
          ┌──────────────┼──────────────┐
          │              │              │
      frontend        backend       database
                         │
                    overrides
                    LOG_LEVEL
                         │

frontend-network:
    frontend ↔ backend

backend-network:
    backend ↔ database

database-data:
    database → persistent storage

This is a good example of why reuse can become useful as Compose files grow.

The infrastructure relationships remain explicit, while repeated configuration is defined once.

Mermaid View Of The Reuse

The reuse pattern can be visualized like this:

    flowchart TD
    shared["x-common-environment<br/>&common-environment"]

    frontend["Frontend"]
    backend["Backend"]
    worker["Worker"]

    shared --> frontend
    shared --> backend
    shared --> worker

    backend --> override["Backend-specific override"]
  

The important relationship is:

shared configuration
       │
       ├── frontend
       ├── backend
       └── worker

The services still remain independent. The shared block simply prevents you from repeating the same configuration.

When Should You Use x- Elements?

Use them when several services genuinely share configuration and keeping that configuration in one place improves the Compose file.

They are particularly useful when:

  • several services have the same configuration
  • a shared block is large enough that duplication becomes annoying
  • you want one place to maintain shared defaults
  • service-specific overrides are still needed

Avoid using them when the result becomes harder for a beginner to understand than the duplicated configuration.

A short Compose file with a little repetition can be clearer than a heavily abstracted one.

Note

Reusability is useful only when it improves readability and maintenance. Don’t turn every repeated line into an abstraction.

Cleanup

For the example in this lesson, stop and remove the Compose application:

docker compose down

Then return to the parent directory:

cd ..

If you no longer need the example directory:

rm -rf compose-extensions

What You Should Remember

The x- prefix gives you a way to create user-defined top-level extension fields.

YAML anchors and aliases then let you reuse those fields:

define once
    │
    ▼
&common-configuration
    │
    ├── reuse
    ├── reuse
    └── reuse

The key pieces are:

x-common-environment: &common-environment
  APP_MODE: development

and:

environment:
  <<: *common-environment

The mental model is:

x- elements hold reusable configuration; YAML anchors give that configuration a reusable name; services consume it where needed.

You now have the major Compose building blocks: services, lifecycle, networks, volumes, environment variables, secrets, and reusable configuration.

The final lesson before the larger zero-to-hero example is about putting these pieces together responsibly: Compose Best Practices.

Last updated on