Skip to content

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 configuration

Putting 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: alpine

That structure doesn’t necessarily change between environments.

But development might need additional configuration:

development
    ├── debugging
    └── development-specific settings

while production might need different configuration:

production
    ├── production-specific settings
    └── production-specific resources

Instead 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   production

The 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.yaml

The 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: development

You can explicitly provide both files:

docker compose -f compose.yaml -f compose.dev.yaml up -d

Compose reads the files in the order you provide them.

Conceptually:

compose.yaml
     │
     │ base configuration
     ▼
compose.dev.yaml
     │
     │ additional/overridden configuration
     ▼
combined Compose configuration
     │
     ▼
application

The 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: production

And compose.dev.yaml contains:

services:
  app:
    environment:
      APP_MODE: development

When you run:

docker compose -f compose.yaml -f compose.dev.yaml up -d

the resulting configuration uses:

APP_MODE=development

The 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.yaml

If every file contains the complete application definition, you may end up with:

same service definition
same networks
same volumes
same configuration
same changes

copied 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     production

Common 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 changes

The 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 -d

you can describe included Compose files from the main Compose file.

For example:

include:
  - compose.database.yaml

services:
  app:
    image: alpine

Here the main Compose file says:

include compose.database.yaml

Conceptually:

              compose.yaml
                    │
                    │ include
                    ▼
          compose.database.yaml
                    │
                    ▼
          combined application

The 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 -d

the 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 configuration

This is useful when you have environment-specific variants:

  • base configuration
  • development overrides
  • production overrides
  • testing overrides

With include:

include:
  - compose.database.yaml

the 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 model

This 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.yaml

The main file (compose.yaml) could include the separate components:

compose.yml
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.monitoring

This 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 volume

The 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-network

This lets the database component keep its related configuration together.

Conceptually:

compose.database.yaml
        │
        ├── database service
        ├── backend-network
        └── database-data volume

The 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.yaml

may be perfectly clear.

Splitting it into:

compose.yaml
compose.a.yaml
compose.b.yaml
compose.c.yaml

could make it harder to understand.

Use multiple files when the separation represents a real boundary.

For example:

common application
        │
        ├── development differences
        └── testing differences

or:

main application
        │
        ├── database component
        ├── worker component
        └── monitoring component

Those are meaningful boundaries.

A Simple Comparison

ApproachMain IdeaUseful When
One compose.yamlEverything in one fileSmall applications
Multiple -f filesCombine files, with later files overriding earlier configurationEnvironment-specific configuration
includeOrganize Compose configuration into separate filesLarger 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.yaml

compose.yaml:

services:
  app:
    image: alpine
    command: ["sh", "-c", "echo mode=$APP_MODE; sleep infinity"]
    environment:
      APP_MODE: production

compose.dev.yaml:

services:
  app:
    environment:
      APP_MODE: development

Start the base application:

docker compose -f compose.yaml up

It uses:

mode=production

Now start it using the development configuration:

docker compose -f compose.yaml -f compose.dev.yaml up

It uses:

mode=development

The 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.yaml

compose.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 -d

The main file includes the database configuration, so the application contains both:

app
database

The organization is:

compose.yaml
   │
   ├── app
   │
   └── include
          │
          ▼
 compose.database.yaml
          │
          └── database

This 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 differences

versus:

compose.yaml
     │
     ├── main application
     │
     ├── compose.database.yaml
     └── compose.worker.yaml

The 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/worker

Multiple 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 -d

you explicitly combine Compose files, with later files able to override or extend the earlier configuration.

With include:

include:
  - compose.database.yaml

you 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 configuration

And 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.

Last updated on