Using Multiple Docker Compose Files
A single Compose file is often enough for a small project. As the application grows, however, you may want to split configuration into multiple files.
For example, you might have:
base application
development configuration
testing configuration
production configurationPutting everything into one huge compose.yaml can make the file difficult to understand.
Compose supports multiple files so you can organize configuration without treating every environment as a completely separate application.
Why Use Multiple Compose Files?
Imagine an application with a backend and database.
The common structure might be:
services:
backend:
image: alpine
database:
image: alpineThat structure doesn’t necessarily change between environments.
But development might need additional configuration:
development
├── debugging
└── development-specific settingswhile production might need different configuration:
production
├── production-specific settings
└── production-specific resourcesInstead of duplicating the whole Compose configuration, you can separate the common configuration from environment-specific configuration.
Think of it as:
Compose Application
│
┌──────────┴──────────┐
│ │
common environment-specific
configuration configuration
│ │
├──────────┬──────────┤
│ │ │
development testing productionThe goal is not simply to create more files.
The goal is to avoid unnecessary duplication while keeping each configuration understandable.
How Does Compose Use Multiple Files?
Compose can work with multiple Compose files.
For example:
compose.yaml
compose.dev.yamlThe first file can contain the common application definition:
services:
app:
image: alpine
command: ["sleep", "infinity"]The second file can contain development-specific configuration:
services:
app:
environment:
APP_MODE: developmentYou can explicitly provide both files:
docker compose -f compose.yaml -f compose.dev.yaml up -dCompose reads the files in the order you provide them.
Conceptually:
compose.yaml
│
│ base configuration
▼
compose.dev.yaml
│
│ additional/overridden configuration
▼
combined Compose configuration
│
▼
applicationThe later file is applied on top of the earlier configuration.
This means the second file can modify or extend parts of the first file.
What Does The Override Look Like?
Suppose compose.yaml contains:
services:
app:
image: alpine
command: ["sleep", "infinity"]
environment:
APP_MODE: productionAnd compose.dev.yaml contains:
services:
app:
environment:
APP_MODE: developmentWhen you run:
docker compose -f compose.yaml -f compose.dev.yaml up -dthe resulting configuration uses:
APP_MODE=developmentThe development file changes the value defined by the base file.
The important idea is:
The files are combined into one Compose configuration before the application is managed.
Why Not Just Copy The Whole File?
Suppose you have:
compose.yaml
compose-dev.yaml
compose-test.yaml
compose-prod.yamlIf every file contains the complete application definition, you may end up with:
same service definition
same networks
same volumes
same configuration
same changescopied across several files.
That creates maintenance problems.
If you change the database configuration, you may have to remember to update several copies.
A base file avoids this:
compose.yaml
common setup
│
┌────────────┼────────────┐
│ │ │
compose.dev compose.test compose.prod
│ │ │
▼ ▼ ▼
development testing productionCommon configuration stays in one place.
Environment-specific differences stay in their own files.
When Are Multiple Files Useful?
Multiple Compose files are particularly useful when the same application needs different configurations.
For example:
compose.yaml
common application structure
compose.dev.yaml
development-specific changes
compose.test.yaml
testing-specific changesThe base file can describe what the application is.
The additional files can describe how that application should behave in a particular environment.
This is another form of separation of concerns.
What Is The include Element?
There is another Compose feature for working with multiple Compose files: the top-level include element.
Instead of explicitly listing files every time with:
docker compose -f compose.yaml -f compose.database.yaml up -dyou can describe included Compose files from the main Compose file.
For example:
include:
- compose.database.yaml
services:
app:
image: alpineHere the main Compose file says:
include compose.database.yamlConceptually:
compose.yaml
│
│ include
▼
compose.database.yaml
│
▼
combined applicationThe included file can contain its own Compose configuration.
For example, compose.database.yaml:
services:
database:
image: alpine
command: ["sleep", "infinity"]
volumes:
database-data:And the main compose.yaml:
include:
- compose.database.yaml
services:
app:
image: alpine
command: ["sleep", "infinity"]Now the application consists of configuration from both files.
How Is include Different From -f?
Both include and multiple -f files can contribute to the final merged Compose configuration. The difference is how the files are composed.
With multiple -f options:
docker compose -f compose.yaml -f compose.dev.yaml up -dthe files are treated as layers of the same Compose configuration. Later files can override or extend values from earlier files.
Think:
compose.yaml
+
compose.dev.yaml
+
compose.test.yaml
│
▼
one merged configurationThis is useful when you have environment-specific variants:
- base configuration
- development overrides
- production overrides
- testing overrides
With include:
include:
- compose.database.yamlthe included file is treated as a separate Compose configuration that is incorporated into the application. Its own project context, such as relative paths, is preserved rather than treating it simply as another override layer.
Think:
main Compose configuration
│
├── database configuration
├── monitoring configuration
└── other component configurations
│
▼
one application modelThis makes include useful for modularizing Compose configurations that can be maintained independently.
Rule of thumb
-f→ “Combine these files as layers of the same configuration.”include→ “Bring these independently organized Compose configurations into this application.”
So the distinction is not merge vs. no merge. This doesn’t mean merge doesn’t happen with include — it does happen and both ultimately contribute to the final Compose model; the important difference is override layering vs. modular composition.
When Is include Useful?
Imagine a larger project:
project/
├── compose.yaml
├── compose.database.yaml
├── compose.monitoring.yaml
└── compose.worker.yamlThe main file (compose.yaml) could include the separate components:
include:
- compose.database.yaml
- compose.worker.yaml
- compose.monitoring.yaml
services:
frontend:
image: alpine
command: ["sleep", "infinity"]Now the main file doesn’t need to contain every service definition.
You can organize the application into logical pieces:
compose.yaml
│
┌──────────────┼──────────────┐
│ │ │
database worker monitoring
│ │ │
▼ ▼ ▼
compose.database compose.worker compose.monitoringThis becomes valuable when different parts of an application have substantial configuration of their own.
Is include The Same As Copying Text?
No.
It is better to think of an included Compose file as another Compose configuration that becomes part of the application.
For example:
main Compose file
│
├── app service
│
└── includes
│
└── database Compose file
│
├── database service
└── database volumeThe included file can contain its own Compose elements.
This is different from simply pasting text into the main file.
Can An Included File Contain Networks And Volumes?
Yes.
An included Compose file can contain the resources needed for the component it describes.
For example:
services:
database:
image: alpine
command: ["sleep", "infinity"]
networks:
- backend-network
volumes:
- database-data:/data
networks:
backend-network:
volumes:
database-data:Then the main file can include it:
include:
- compose.database.yaml
services:
backend:
image: alpine
command: ["sleep", "infinity"]
networks:
- backend-networkThis lets the database component keep its related configuration together.
Conceptually:
compose.database.yaml
│
├── database service
├── backend-network
└── database-data volumeThe main Compose file can then use that component.
Multiple Files Are About Organization
It’s tempting to think:
“More Compose files must mean a better Compose project.”
Not necessarily.
For a small application:
compose.yamlmay be perfectly clear.
Splitting it into:
compose.yaml
compose.a.yaml
compose.b.yaml
compose.c.yamlcould make it harder to understand.
Use multiple files when the separation represents a real boundary.
For example:
common application
│
├── development differences
└── testing differencesor:
main application
│
├── database component
├── worker component
└── monitoring componentThose are meaningful boundaries.
A Simple Comparison
| Approach | Main Idea | Useful When |
|---|---|---|
One compose.yaml | Everything in one file | Small applications |
Multiple -f files | Combine files, with later files overriding earlier configuration | Environment-specific configuration |
include | Organize Compose configuration into separate files | Larger applications or reusable components |
The important thing is not to choose a feature because it exists.
Choose the structure that makes the application easier to understand and maintain.
A Practical Example
Let’s create a small project with a common application and a development override.
Directory:
compose-multiple-files/
├── compose.yaml
└── compose.dev.yamlcompose.yaml:
services:
app:
image: alpine
command: ["sh", "-c", "echo mode=$APP_MODE; sleep infinity"]
environment:
APP_MODE: productioncompose.dev.yaml:
services:
app:
environment:
APP_MODE: developmentStart the base application:
docker compose -f compose.yaml upIt uses:
mode=productionNow start it using the development configuration:
docker compose -f compose.yaml -f compose.dev.yaml upIt uses:
mode=developmentThe common service definition remains in the base file.
The development file contains only the difference.
This is a good pattern because the second file is small and communicates exactly what changes.
A Practical include Example
Now consider a component-based structure:
compose-include/
├── compose.yaml
└── compose.database.yamlcompose.database.yaml:
services:
database:
image: alpine
command: ["sleep", "infinity"]compose.yaml:
include:
- compose.database.yaml
services:
app:
image: alpine
command: ["sleep", "infinity"]Start the application:
docker compose up -dThe main file includes the database configuration, so the application contains both:
app
databaseThe organization is:
compose.yaml
│
├── app
│
└── include
│
▼
compose.database.yaml
│
└── databaseThis is a simple example, but the same idea can be useful when a component has a larger amount of Compose configuration.
Don’t Confuse Environment Overrides With Component Includes
These two patterns can look similar, but they answer different questions.
Environment override:
"What changes between development and production?"Component include:
"How can I organize this application's Compose configuration into logical pieces?"For example:
compose.yaml
│
├── common application
│
└── compose.dev.yaml
│
└── development differencesversus:
compose.yaml
│
├── main application
│
├── compose.database.yaml
└── compose.worker.yamlThe first is primarily about configuration variation.
The second is primarily about configuration organization.
A Useful Mental Model
You can now extend the Compose mental model one more step:
Compose Application
│
▼
Compose configuration
│
┌──────────────┼──────────────┐
│ │ │
services networks volumes
│
│
└──────── configuration files ────────┐
│
┌─────────────┴─────────────┐
│ │
environment components
overrides / includes
│ │
development/test database/workerMultiple files don’t change what a Compose application is.
They change how you organize its configuration.
That is the important concept.
What You Should Remember
You now have two useful ways to work with multiple Compose files.
With multiple -f options:
docker compose -f compose.yaml -f compose.dev.yaml up -dyou explicitly combine Compose files, with later files able to override or extend the earlier configuration.
With include:
include:
- compose.database.yamlyou organize Compose configuration into separate files that become part of the application.
The mental model is:
Multiple -f files
│
└── combine/override configuration
include
│
└── organize application configurationAnd remember:
Use multiple Compose files when they represent a meaningful configuration or organizational boundary. Don’t split files simply because Compose allows you to.
You will see all these concepts in Zero-to-Hero Example in the later chapters as part of practical demonstration.