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: infoThe configuration is repeated three times.
If you later change:
LOG_LEVEL=debugyou have to remember to change it in every service.
That creates two problems:
- The Compose file becomes unnecessarily large.
- One service can accidentally end up with different configuration.
A reusable configuration block can give you:
shared configuration
│
┌───────────┼───────────┐
│ │ │
frontend backend workerInstead 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: alpineThe 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 configurationBut 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: infoThere are two parts:
&common-environment
│
└── name given to this configurationThe complete declaration is:
x-common-environment: &common-environmentNow another part of the YAML can reuse that configuration with *:
environment:
<<: *common-environmentHere:
&common-environment
│
│ defines
▼
shared configuration
▲
│ reuses
│
*common-environmentThe <<: 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-extensionsCreate 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-environmentStart it:
docker compose up -dAll three services receive the same environment configuration.
The important structure is:
x-common-environment
│
│ anchor
▼
&common-environment
│
├──────────────┐
│ │
▼ ▼
frontend backend worker
│ │ │
└──────────────┴─────────────┘
same configurationYou defined the configuration once.
What Does <<: Mean?
The syntax can look strange at first:
environment:
<<: *common-environmentBreak it into pieces:
<<:
│
└── merge this mapping here
*common-environment
│
└── use the configuration named by the anchorSo:
environment:
<<: *common-environmentmeans roughly:
“Take the mapping stored in
common-environmentand merge it into thisenvironmentmapping.”
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: infobut 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=4The 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 configurationWhat 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: debugThe backend gets:
APP_MODE=development
LOG_LEVEL=debugwhile the frontend gets:
APP_MODE=development
LOG_LEVEL=infoThe service-specific value takes precedence over the merged value.
This gives you a useful mental model:
shared defaults
│
▼
service configuration
│
└── service-specific value can overrideCan 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-labelsThe 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 AIf configuration A changes, every copy must be updated.
With an anchor:
configuration A
│
┌────────────┼────────────┐
▼ ▼ ▼
frontend backend workerThere 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: alpineCompose does not try to start:
x-common-environmentas 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 servicesHow Does This Fit Into The Compose Mental Model?
You’ve now seen several top-level elements:
compose.yaml
│
├── services
├── networks
├── volumes
├── secrets
│
└── x-... extension fieldsThey serve different roles:
| Element | Purpose |
|---|---|
services | Defines application services |
networks | Defines communication networks |
volumes | Defines persistent storage |
secrets | Defines 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 storageThis 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
└── workerThe 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 downThen return to the parent directory:
cd ..If you no longer need the example directory:
rm -rf compose-extensionsWhat 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
└── reuseThe key pieces are:
x-common-environment: &common-environment
APP_MODE: developmentand:
environment:
<<: *common-environmentThe 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.