Docker Compose v2: Advanced Features You're Probably Missing

1. File Merging and Override Chains

Compose merges multiple files in order. Each subsequent file deep-merges onto the previous, with maps merged key-by-key and sequences replaced outright unless you use the !reset or !override YAML tags.

Explicit merge with -f

# merge three files in order
docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.prod.yml up

Automatic override discovery

Without -f, Compose automatically loads docker-compose.yml then docker-compose.override.yml if it exists. Commit the base file; gitignore the override for local dev customisation.

Merge semantics: maps vs. sequences

YAML typeMerge behaviorExample key
MapDeep merge — child keys in overlay win, others keptenvironment, labels
SequenceReplaced entirely by overlay valuecommand, ports
ScalarOverlay wins unconditionallyimage, restart
Watch out Sequences like ports and volumes are replaced, not appended. If your base file defines three port mappings and the override defines one, the result has one. Use extends or merge carefully.

The extends keyword

extends lets a service inherit from another file's service definition without merging entire files:

# docker-compose.yml
services:
  web:
    extends:
      file: common-services.yml
      service: app-base
    ports:
      - "8080:80"
    environment:
      NODE_ENV: production
Note extends does not support circular references. depends_on is not inherited; you must redeclare it in the child service.

2. Service Profiles

Profiles allow you to group services that are only started on demand. Services with no profile assigned are always started. Services with a profile are started only when that profile is active.

services:
  app:
    image: myapp:latest

  db:
    image: postgres:16

  debug-tools:
    image: nicolaka/netshoot
    profiles: [debug]

  mailpit:
    image: axllent/mailpit
    profiles: [dev]

  prometheus:
    image: prom/prometheus
    profiles: [monitoring, dev]

Activating profiles

# activate single profile
docker compose --profile dev up

# activate multiple profiles
docker compose --profile dev --profile monitoring up

# via environment variable
COMPOSE_PROFILES=dev,monitoring docker compose up
Tip A service depended on by a profiled service must itself be profiled or always active. If debug-tools depends on app, and app has no profile, that's fine. If debug-tools depends on another profiled service, that profile must also be active.

3. Dependency Conditions

depends_on in v2 accepts a condition alongside the service name, controlling when a dependent service is considered ready. Without conditions, Compose just waits for the container to start — not for the process inside to be ready.

services:
  app:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy
        restart: true
      migrations:
        condition: service_completed_successfully
      cache:
        condition: service_started
        required: false
ConditionMeaning
service_startedContainer has started (default, same as old-style depends_on)
service_healthyContainer healthcheck is passing; requires a healthcheck block on the dependency
service_completed_successfullyContainer exited with code 0; useful for one-shot init containers

restart and required flags

restart: true causes the dependent service to restart automatically if its dependency restarts. required: false makes the dependency optional — Compose won't fail if the dependency service is not present (e.g., excluded by profile).

One-shot migration container pattern

services:
  migrations:
    image: myapp:latest
    command: ["python", "manage.py", "migrate"]
    depends_on:
      db:
        condition: service_healthy
    restart: on-failure

  app:
    image: myapp:latest
    depends_on:
      migrations:
        condition: service_completed_successfully

4. Compose Include New in 2.20

include imports another Compose file as a self-contained unit. Unlike file merging with -f, each included file is processed in its own project namespace and its services do not collide with the parent's services of the same name.

# docker-compose.yml
include:
  - ./infra/observability.yml
  - path: ./infra/database.yml
    project_directory: ./infra
    env_file: .env.db

services:
  app:
    image: myapp:latest
    networks:
      - default
      - infra_db_net   # network exported by database.yml
Note Included files do not inherit the parent project's variables or environment. Each included file gets its own variable scope. If you need to share config, use a common .env file referenced explicitly in both.

This is the right tool for monorepos with independent service clusters: include the auth stack, include the metrics stack, compose them at the top level.

5. Secrets and Configs

Both secrets and configs mount data into containers at runtime without baking it into images or environment variables. The difference: secrets are mounted to an in-memory tmpfs under /run/secrets/; configs are standard files (not guaranteed in-memory) and commonly used for non-sensitive config files.

Secrets

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    environment: MY_API_KEY   # read from host env var

services:
  app:
    image: myapp:latest
    secrets:
      - db_password
      - source: api_key
        target: /run/secrets/api_key
        uid: "1000"
        gid: "1000"
        mode: 0400

Configs

configs:
  nginx_conf:
    file: ./nginx/nginx.conf
  feature_flags:
    content: |
      FEATURE_X=true
      FEATURE_Y=false

services:
  proxy:
    image: nginx:alpine
    configs:
      - source: nginx_conf
        target: /etc/nginx/nginx.conf
Featuresecretsconfigs
Default mount path/run/secrets/<name>/<name>
In-memory (tmpfs)YesNo
Inline contentNoYes (content:)
From env varYes (environment:)No
Custom permissionsuid, gid, modeuid, gid, mode

6. Extension Fields (x-)

Any top-level key prefixed with x- is ignored by Compose's schema validation. Combined with YAML anchors and aliases, this is the most effective way to deduplicate configuration.

# Define reusable blocks at top level
x-logging: &logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

x-common-env: &common-env
  TZ: UTC
  LOG_LEVEL: info

x-healthcheck: &healthcheck
  test: ["CMD", "curl", "-f", "http://localhost/health"]
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 10s

services:
  api:
    image: myapp/api:latest
    logging: *logging
    healthcheck: *healthcheck
    environment:
      <<: *common-env
      PORT: "8080"

  worker:
    image: myapp/worker:latest
    logging: *logging
    environment:
      <<: *common-env
      WORKER_CONCURRENCY: "4"

The <<: syntax is a YAML merge key — it inlines the referenced map's keys into the current map. Keys defined after <<: override any conflicting keys from the anchor.

7. Advanced Build Configuration

Build arguments, secrets, and SSH forwarding

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
      args:
        NODE_VERSION: "20"
        BUILD_DATE: ${BUILD_DATE}
      secrets:
        - npm_token        # mounts as /run/secrets/npm_token at build time
      ssh:
        - default          # forward host SSH agent
      cache_from:
        - type=registry,ref=myregistry/myapp:buildcache
      cache_to:
        - type=registry,ref=myregistry/myapp:buildcache,mode=max
      platforms:
        - linux/amd64
        - linux/arm64
      labels:
        org.opencontainers.image.revision: ${GIT_SHA}

secrets:
  npm_token:
    environment: NPM_TOKEN

Multi-stage target selection

The target field selects a named stage from a multi-stage Dockerfile. This lets you run a development stage locally (with dev tools) and a production stage in CI, sharing cache layers between them.

additional_contexts

build:
  context: .
  additional_contexts:
    shared-lib: ../shared
    base-image: docker-image://myregistry/base:latest

In your Dockerfile, reference these as FROM shared-lib AS lib or COPY --from=shared-lib . /shared.

8. Advanced Networking

Custom driver options and IPAM

networks:
  frontend:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: br-frontend
      com.docker.network.driver.mtu: "1450"
    ipam:
      driver: default
      config:
        - subnet: 172.28.0.0/16
          ip_range: 172.28.5.0/24
          gateway: 172.28.5.254

  backend:
    internal: true       # no external connectivity
    attachable: true    # standalone containers can join

Per-service network configuration

services:
  app:
    networks:
      frontend:
        ipv4_address: 172.28.5.10
        aliases:
          - app
          - myapp
      backend:
        priority: 100      # higher = preferred interface

Connecting to external networks

networks:
  shared:
    external: true
    name: myproject_shared   # exact Docker network name
Tip Use external: true to share a network between two independent Compose projects. Services in both projects discover each other by service name if they use the same network and alias.

9. Environment Variable Interpolation

Compose supports extended shell-style variable syntax beyond simple ${VAR} substitution.

SyntaxBehavior
${VAR}Substitute value; empty string if unset
${VAR:-default}Use default if VAR is unset or empty
${VAR-default}Use default only if VAR is unset (not if empty)
${VAR:?error}Error with message if VAR is unset or empty
${VAR?error}Error with message only if VAR is unset
${VAR:+value}Use value if VAR is set and non-empty; else empty
$$VARLiteral $VAR — escape for passing to containers
services:
  app:
    image: myapp:${TAG:-latest}
    environment:
      DB_HOST: ${DB_HOST:?DB_HOST must be set}
      DB_PORT: ${DB_PORT:-5432}
      DOLLAR_SIGN_EXAMPLE: $$HOME    # passes literal $HOME to container

Multiple env files

The env_file key accepts a list and supports per-file settings:

services:
  app:
    env_file:
      - path: .env
        required: true
      - path: .env.local
        required: false   # silently skipped if absent

Later files in the list override earlier ones. environment: keys in the service definition override everything from env_file.

10. File Watch and Sync New in 2.22

develop.watch replaces bind mounts for live development. It syncs specific paths into running containers and can trigger rebuilds on certain changes — without mounting the entire project directory.

services:
  app:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          ignore:
            - node_modules

        - action: sync+restart
          path: ./config
          target: /app/config

        - action: rebuild
          path: package.json
ActionBehavior
syncCopy changed files into the running container; no restart
sync+restartSync files, then restart the container's entrypoint
rebuildRun docker compose build and recreate the container

Activate with:

docker compose up --watch
# or in background
docker compose watch
Tip sync uses tar-based file transfer, not bind mounts — it works correctly on Docker Desktop (macOS/Windows) where bind mount performance is poor, and avoids cross-OS file permission issues.

11. Healthcheck Configuration

Healthchecks defined in Compose override any HEALTHCHECK instruction in the Dockerfile.

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
      start_interval: 2s   # poll faster during start_period

  redis:
    image: redis:7
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3

  inherited:
    image: some-image-with-healthcheck
    healthcheck:
      disable: true   # suppress Dockerfile HEALTHCHECK

start_interval (added in Docker Engine 25) lets you poll more aggressively during the initial start_period without affecting the steady-state interval.

Test formatBehavior
["CMD", "cmd", "arg"]Exec directly; no shell — faster, no shell injection risk
["CMD-SHELL", "shell command"]Runs /bin/sh -c "shell command"; supports pipes, &&, env expansion
"shell command"String shorthand; equivalent to CMD-SHELL

12. Underused CLI Flags

Selective service operations

# restart only one service without recreating dependants
docker compose restart worker

# bring up a single service and its dependencies only
docker compose up --no-deps api

# build without using cache
docker compose build --no-cache app

# pull images and rebuild before starting
docker compose up --pull always --build

Scaling and resource control

# run multiple instances of a service
docker compose up --scale worker=4

# equivalent in compose file (preferred)
services:
  worker:
    deploy:
      replicas: 4
      resources:
        limits:
          cpus: "0.5"
          memory: 256M
        reservations:
          cpus: "0.25"
          memory: 128M

Dry-run and inspection

# show the merged config that would be applied
docker compose config

# show only service names
docker compose config --services

# validate without starting
docker compose config --quiet

# dry-run any command (shows what would happen)
docker compose --dry-run up --build

Run one-off commands

# run a command in a new container (not the running one)
docker compose run --rm app python manage.py shell

# exec into running container
docker compose exec -it app bash

# run with specific env overrides
docker compose run --rm -e DEBUG=1 app pytest tests/

Project isolation

# run multiple independent instances of the same compose file
docker compose -p project-a up -d
docker compose -p project-b up -d

# or via env variable
COMPOSE_PROJECT_NAME=staging docker compose up -d
Note The project name prefixes all resource names (containers, networks, volumes). Changing it after resources are created does not rename them — it creates a new isolated project and orphans the old resources.

Wait for services to be healthy

# block until all services with healthchecks pass
docker compose up -d --wait

# with a timeout
docker compose up -d --wait --wait-timeout 60

This is the correct replacement for external wait-for-it.sh scripts in CI pipelines when your services define healthchecks.