Most compose.yaml files slowly turn into long, copy-pasted blocks with fragile startup dependencies. Docker Compose includes several built-in features that can keep your stack cleaner, more reusable, and predictable.

Imagine a small web stack with a Python app, a worker, PostgreSQL, and an Nginx proxy defined in a single compose.yaml file. As the stack grows, you end up repeating the same settings across services.

Worse, after a reboot, the app may start before PostgreSQL is actually ready, causing failures until everything settles.

A common workaround is adding sleep commands or maintaining multiple nearly identical Compose files. Both approaches quickly become harder to maintain than the stack itself.

Docker Compose v2 already provides better solutions. Features such as anchors, healthchecks, profiles, include, and watch can reduce duplication, handle service dependencies properly, and simplify development workflows.

Many of these features were introduced or expanded between Compose 2.17 and 2.24, so they are easy to overlook.


TecMint Weekly Newsletter

Get the Learn Linux 7 Days Crash Course free when you join 34,000+ Linux professionals reading every Thursday.

Check your email for a magic link to get started.

Something went wrong. Please try again.

How We Picked These Docker Compose Features

All seven features work through Docker Compose’s project model, where Compose combines and resolves your configuration before creating the containers.

They are built into the Compose Specification, require no third-party plugins, and are demonstrated using a single example stack in /home/tecmint/webapp on our test server at 192.168.122.248.

Check Your Docker Compose Version Before You Start

Since several of these features were introduced or expanded in recent Compose releases, first verify that you are using the Docker Compose v2 plugin rather than the legacy docker-compose command:

docker compose version

You should see output similar to:

Docker Compose version v2.39.2

Your version may differ. For the examples in this guide, we recommend Docker Compose v2.24 or newer to ensure the required features are available.

1. Reuse Service Settings with YAML Anchors and x- Extension Fields

With your Compose version confirmed, the most common problem is repeated configuration because if the web and worker services use the same restart policy and logging limits, maintaining those settings separately means every change has to be made twice.

On Ubuntu and Debian, open the Compose file with nano:

cd ~/webapp && nano compose.yaml

On Rocky Linux, AlmaLinux, and RHEL, minimal installations may not include nano, so use vi instead:

cd ~/webapp && vi compose.yaml

Add the following configuration:

yaml
# /home/tecmint/webapp/compose.yaml
x-common: &common
  restart: unless-stopped
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"

services:
  web:
    <<: *common
    build: .
    ports:
      - "8080:8000"
  worker:
    <<: *common
    build: .
    command: python worker.py

Here is what the syntax does:

  • x-common is an extension field that Compose ignores when creating services, making it useful for storing reusable configuration.
  • &common creates a YAML anchor, giving the shared configuration a reusable name.
  • <<: *common merges the anchored settings into each service. Values defined directly under a service override the shared values.
  • max-size and max-file limit each log file to 10 MB and retain up to three files, helping prevent container logs from consuming excessive disk space.

Save the file and exit the editor. Then ask Compose to render the final configuration:

docker compose config

The output shows restart: unless-stopped and the same logging configuration under both services, even though you defined those settings only once.

2. Start Services in Order with depends_on condition: service_healthy

Now that the services share common settings, the next problem is startup order. A plain depends_on only ensures that the database container starts before the application.

It does not wait for PostgreSQL to be ready to accept connections, so the app can still fail with a connection refused error during startup.

Open compose.yaml again, add a db service with a healthcheck, and make web depend on its healthy state:

# /home/tecmint/webapp/compose.yaml
services:
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 10
      start_period: 10s
  web:
    <<: *common
    build: .
    ports:
      - "8080:8000"
    depends_on:
      db:
        condition: service_healthy
        restart: true

The important settings are:

  • test runs pg_isready, which reports success when PostgreSQL is ready to accept connections.
  • interval checks the database every 5 seconds.
  • retries allows up to 10 consecutive failed checks before the container is considered unhealthy.
  • start_period gives PostgreSQL 10 seconds to initialise before healthcheck failures count toward the retry limit.
  • condition: service_healthy tells Compose to start web only after the database healthcheck succeeds.
  • restart: true tells Compose to restart the dependent web service when Compose explicitly restarts the db service.

The ${DB_PASSWORD} value is read from a .env file in the same project directory. Save the file, then start the stack with --wait:

docker compose up -d --wait

The --wait option makes Compose wait until the services are running or healthy before returning control to the shell.

If the database healthcheck never succeeds, Compose reports the dependency failure instead of allowing web to start against an unavailable database, and that gives you a clear healthcheck failure to investigate rather than an application crash loop.

If this finally fixed the “app starts before the database” problem for you, share it with someone who still has sleep 30 in their startup scripts.

3. Run Optional Services on Demand with profiles

With the core stack starting cleanly, the next problem is optional services. For example, you may need Adminer for database troubleshooting but have no reason to run its web interface all the time.

Open compose.yaml and add the following service under services:

# /home/tecmint/webapp/compose.yaml
  adminer:
    image: adminer
    ports:
      - "8081:8080"
    profiles: ["debug"]

The profiles setting assigns Adminer to the debug profile, so Compose skips services assigned to inactive profiles, while services without a profile always start.

A normal startup therefore leaves Adminer stopped:

docker compose up -d

When you need the database administration interface, enable the profile:

docker compose --profile debug up -d

You can also enable the profile through the COMPOSE_PROFILES environment variable. For example, adding the following to .env enables the debug profile without requiring the command-line flag:

COMPOSE_PROFILES=debug

This keeps occasional troubleshooting tools out of your normal stack while making them available with a single command.

4. Split a Large Compose File with include

As compose.yaml grows, keeping every service in one file becomes harder to navigate, so the include element lets you split related services into separate Compose files while keeping them part of the same project.

Create a db directory and a new Compose file. On Ubuntu and Debian:

mkdir -p db && nano db/compose.yaml

On Rocky Linux, AlmaLinux, and RHEL:

mkdir -p db && vi db/compose.yaml

Move the db service from the previous example into this file:

# /home/tecmint/webapp/db/compose.yaml
services:
  db:
    image: postgres:17
    env_file: db.env
    volumes:
      - ./data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 10

Paths inside an included Compose file are resolved relative to that file. Therefore, db.env and ./data refer to /home/tecmint/webapp/db/db.env and /home/tecmint/webapp/db/data/.

Now add the following to the main compose.yaml and remove its old db service:

# /home/tecmint/webapp/compose.yaml
include:
  - db/compose.yaml

Your existing depends_on configuration can continue to reference the included db service:

depends_on: db: condition: service_healthy

Compose combines the included configuration into the project model, so the web service can still depend on db even though the two services are defined in different files.

After making the change, validate the resulting configuration:

docker compose config

If you accidentally keep another db service with the same project-level name, Compose reports the conflict instead of silently choosing one configuration.

If your Compose file just shrank by half, share this with a teammate who scrolls through a 400-line compose.yaml every day.

5. Live-Sync Code into Containers with develop.watch

Splitting the Compose configuration files keeps the project organised, but development can still become repetitive if you have to rebuild the web image after every code change.

Compose Watch can sync local changes directly into a running container instead. Open compose.yaml and add a develop section to the web service:

# /home/tecmint/webapp/compose.yaml
    develop:
      watch:
        - action: sync
          path: ./app
          target: /app
          ignore:
            - __pycache__/
        - action: rebuild
          path: requirements.txt

The two watch actions serve different purposes:

  • action: sync copies changes from ./app into /app without rebuilding the image.
  • ignore excludes Python bytecode caches from synchronization.
  • action: rebuild rebuilds the image when requirements.txt changes, ensuring newly added dependencies are installed.

Save the file and start Compose Watch:

docker compose watch

Compose Watch is intended for development workflows, so keep this configuration on machines where you actively modify the application source code.

6. Remove Inherited Values with !reset and !override

Production deployments often layer a second Compose file over the base configuration. However, Compose normally merges values from multiple files. For example, adding another ports entry does not remove the port published by the base file.

Compose provides the !reset and !override YAML tags to explicitly control this behaviour.

Create the production override file. On Ubuntu and Debian:

nano compose.prod.yaml

Or with vi on Rocky Linux, AlmaLinux, and RHEL:

vi compose.prod.yaml

Add the following configuration:

# /home/tecmint/webapp/compose.prod.yaml
services:
  web:
    ports: !override
      - "127.0.0.1:8080:8000"
  adminer:
    ports: !reset []

These tags behave differently:

  • !override completely replaces the value from the base Compose file. Here, web publishes only 127.0.0.1:8080, rather than merging the production port with the existing one.
  • !reset [] removes the inherited value. Here, Adminer’s published port is cleared, so the service is not exposed through a host port in production.

Save the file and inspect the merged configuration:

docker compose -f compose.yaml -f compose.prod.yaml config

Under web, look for host_ip: 127.0.0.1. The adminer service should no longer have a published ports entry.

Version Note: The !reset and !override tags require Docker Compose v2.24.4 or newer. If you use these features, update the earlier version requirement in this guide from v2.24 to v2.24.4+.

If you’ve ever wondered why a “removed” port kept showing up in production, share this with your ops team.

7. Embed Config Files Inline with configs content

Since web now listens only on localhost, the stack needs a reverse proxy to accept external requests. Docker Compose’s top-level configs element lets you keep the Nginx configuration directly inside compose.yaml instead of maintaining a separate configuration file.

Open compose.yaml and add the following configuration:

# /home/tecmint/webapp/compose.yaml
configs:
  nginx_conf:
    content: |
      server {
        listen 80;
        location / {
          proxy_pass http://web:8000;
          proxy_set_header Host $$host;
        }
      }

services:
  proxy:
    image: nginx:stable
    ports:
      - "80:80"
    configs:
      - source: nginx_conf
        target: /etc/nginx/conf.d/default.conf

The configuration works as follows:

  • content stores the Nginx configuration directly in the Compose file. Compose makes it available to the container when the service starts.
  • source references the named Compose config, while target specifies where it appears inside the container.
  • proxy_pass forwards incoming requests from Nginx to the web service on port 8000.

There is one important detail in the Nginx configuration. Compose performs variable interpolation inside content, so $host would be interpreted as a Compose variable. Writing it as $$host escapes the dollar sign and passes $host through to Nginx unchanged.

Without $$, Compose may warn that host is not set and substitute an empty value, resulting in an invalid or incorrect Nginx configuration.

Tips for Using These Docker Compose Features Safely

When multiple Compose files contribute to the final project configuration, run docker compose config before deploying. It validates the configuration and shows the model Compose will use, making it easier to catch unexpected merges or overrides.

Be careful with its output, however: resolved environment variables can appear in plain text. Never paste the full output into a public issue, forum post, or support ticket if it contains passwords, API keys, or other secrets.

Protect environment files that contain credentials:

chmod 600 .env db/db.env

Also add them to .gitignore so they are not accidentally committed:

.env
db/db.env

For faster builds, keep unnecessary files out of the Docker build context with .dockerignore. At minimum, consider excluding the Git metadata and PostgreSQL data directory:

.git
db/data

This reduces the amount of data Docker has to send as build context and can make rebuilds faster.

Conclusion

These seven Compose features solve common problems without requiring extra tooling:

  • YAML anchors and x- fields reduce repeated service configuration.
  • Healthchecks and service_healthy prevent services from starting before their dependencies are ready.
  • Profiles keep optional services out of the default stack.
  • include splits large Compose projects into manageable files.
  • Compose Watch syncs code changes during development.
  • !reset and !override give you precise control when layering Compose files.
  • configs.content keeps small configuration files directly inside compose.yaml.

Together, these features make Compose projects easier to maintain, safer to deploy, and faster to work with during development.

If you use other Compose features to keep your projects clean or avoid common deployment problems, share them in the comments.

If this article helped, with someone on your team.

TecMint Weekly Newsletter

Get the Learn Linux 7 Days Crash Course free when you join 34,000+ Linux professionals reading every Thursday.

Check your email for a magic link to get started.

Something went wrong. Please try again.

Share.
Leave A Reply