Chapter 16Lesson 02~135 minutes

Advanced Compose: Profiles, depends_on, Health Conditions, Overrides, Includes, Watch, and Multi-File Design: Guided Hands-On Workflow and Core Operations

Build a disposable advanced Compose project with a base model, development override, profile-gated service, health dependency, included sub-model, and Compose Watch while inspecting normalized state first.

Multi-file Composeincludeservice_healthyprofileswatch

Learning objectives

  • Create a disposable advanced Compose project with explicit project identity and bounded local-only resources.
  • Split baseline and development concerns across ordered files and prove the merged result before starting containers.
  • Import a reusable tools model with include and activate its optional service through a profile.
  • Gate an application on a dependency healthcheck and inspect the difference between created, running, and healthy.
  • Exercise Compose Watch on synthetic source and compare sync/rebuild behavior with ordinary project reconciliation.
Lab boundary. All resources are disposable and local. No production endpoint, real secret, Docker socket mount, privileged container, daemon mutation, or cloud registry is used. The lab binds one host port to loopback only and labels the project explicitly.

1. Preflight: prove feature availability before writing runtime state

Create a clean workspace. POSIX commands work in Git Bash/WSL/Linux; on PowerShell create equivalent files with an editor. Record the installed versions because include and Watch have explicit minimum-version histories.

mkdir -p da-compose16/{app,tools,overlays,evidence}
cd da-compose16

docker version > evidence/docker-version.txt
docker info > evidence/docker-info.txt
docker context show > evidence/docker-context.txt
docker compose version > evidence/compose-version.txt
docker buildx version > evidence/buildx-version.txt

# Prove no old lab project is active before reuse:
docker ps -a --filter label=com.docker.compose.project=da-compose16
docker network ls --filter label=com.docker.compose.project=da-compose16

2. Build a tiny writable, non-root development service

Create app/index.html:

<!doctype html>
<html><body><h1>Compose 16 baseline</h1><p>watch target: /site</p></body></html>

Create app/Dockerfile:

# syntax=docker/dockerfile:1
FROM busybox:1.36.1
WORKDIR /site
COPY --chown=65534:65534 index.html /site/index.html
USER 65534:65534
EXPOSE 8080
CMD ["httpd", "-f", "-p", "8080", "-h", "/site"]

This image is intentionally tiny but still includes the BusyBox utilities Watch needs. The sync target is owned by the runtime user; the lab does not solve write failures by adding broad privileges.

3. Create an included tools model with an optional profile

Create tools/compose.yaml:

services:
  toolbox:
    image: busybox:1.36.1
    profiles: [debug]
    command: ["sh", "-c", "echo toolbox-ready; exec sleep 3600"]
    labels:
      devops-academy.lab: compose16

The file is a complete Compose model, not a fragment. Its path context belongs to tools/. The service is profile-gated so normal application startup does not include it.

4. Base model: include tools, health-check dependency, and built app

Create compose.yaml:

name: da-compose16
include:
  - ./tools/compose.yaml

services:
  readiness:
    image: busybox:1.36.1
    command: ["httpd", "-f", "-p", "8090", "-h", "/www"]
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8090/"]
      interval: 2s
      timeout: 1s
      retries: 10
    volumes:
      - ./readiness:/www:ro
    labels:
      devops-academy.lab: compose16

  app:
    build:
      context: ./app
    image: da-compose16-app:lab
    depends_on:
      readiness:
        condition: service_healthy
    ports:
      - "127.0.0.1:18016:8080"
    labels:
      devops-academy.lab: compose16

Create the readiness document:

mkdir -p readiness
printf '%s
' 'ready' > readiness/index.html

5. Development override: add Watch without changing the baseline model

Create overlays/compose.dev.yaml:

services:
  app:
    environment:
      APP_MODE: development
    develop:
      watch:
        - action: sync
          path: ./app
          target: /site
          initial_sync: true
          ignore:
            - Dockerfile
        - action: rebuild
          path: ./app/Dockerfile
Path rule alert. Because this file is used as an override with -f, its relative paths are interpreted relative to the base file (compose.yaml), not relative to the overlays/ directory. That is why ./app is correct here.

6. Normalize before up

docker compose \
  -f compose.yaml \
  -f overlays/compose.dev.yaml \
  config --quiet

docker compose \
  -f compose.yaml \
  -f overlays/compose.dev.yaml \
  config > evidence/dev.normalized.yaml

docker compose \
  -f compose.yaml \
  -f overlays/compose.dev.yaml \
  config --services

docker compose \
  -f compose.yaml \
  -f overlays/compose.dev.yaml \
  config --profiles

Expected baseline services are readiness and app. toolbox is declared but profile-gated. Save the normalized file before execution so later reconciliation can be compared with intended input.

7. Predict state changes before applying them

  1. Prediction A: ordinary dev up creates readiness and app, plus the project network, but not toolbox.
  2. Prediction B: Compose creates the dependency first and waits for its healthcheck before starting app.
  3. Prediction C: enabling the debug profile adds toolbox without changing the source definition of the core services.
  4. Prediction D: a Watch sync changes files in the running app container without rebuilding the image; a Watch rebuild can replace the app container/image.

8. Apply the dev model and inspect health-gated startup

docker compose \
  -f compose.yaml \
  -f overlays/compose.dev.yaml \
  up -d --build

docker compose -f compose.yaml -f overlays/compose.dev.yaml ps -a
docker inspect da-compose16-readiness-1 \
  --format 'state={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{end}}'
docker inspect da-compose16-app-1 \
  --format 'id={{.Id}} image={{.Image}} state={{.State.Status}}'

docker ps -a --filter label=com.docker.compose.project=da-compose16

Do not hard-code container names in production automation; this lab uses them only as human-readable evidence with one replica. The project labels are the stronger ownership signal.

9. Activate and observe the optional profile

docker compose \
  -f compose.yaml \
  -f overlays/compose.dev.yaml \
  --profile debug \
  up -d

docker compose -f compose.yaml -f overlays/compose.dev.yaml --profile debug ps -a

docker inspect da-compose16-toolbox-1 \
  --format '{{json .Config.Labels}}' > evidence/toolbox-labels.json

The resource appeared because the profile became active. The normalized core model did not need to be duplicated into a separate project just to add a debugging helper.

10. Exercise Watch with narrow synthetic input

Run Watch in a second terminal:

docker compose \
  -f compose.yaml \
  -f overlays/compose.dev.yaml \
  watch app

In the first terminal, change only the synthetic page:

printf '%s
' '<!doctype html><html><body><h1>Compose 16 synced</h1></body></html>' > app/index.html
sleep 2
curl http://127.0.0.1:18016/

docker inspect da-compose16-app-1 --format 'container={{.Id}} image={{.Image}}'   > evidence/app-after-sync.txt

A sync should update the target without requiring a new image. Now change the Dockerfile with a harmless label to trigger the rebuild rule:

printf '
LABEL devops-academy.watch-rebuild="1"
' >> app/Dockerfile
# Observe the watch terminal, then record the resulting object identity.
docker compose -f compose.yaml -f overlays/compose.dev.yaml ps -a
docker image inspect da-compose16-app:lab --format '{{.Id}}'   > evidence/image-after-rebuild.txt

Stop Watch with Ctrl+C. The evidence should show that sync and rebuild are different state transitions.

11. Reconciliation after a model change

Add a harmless label to the app service in the dev override, normalize again, then apply only that service:

# Edit overlays/compose.dev.yaml and add under app:
# labels:
#   devops-academy.variant: dev

docker compose -f compose.yaml -f overlays/compose.dev.yaml config   > evidence/dev.changed.normalized.yaml

docker compose -f compose.yaml -f overlays/compose.dev.yaml up -d app

docker inspect da-compose16-app-1   --format '{{json .Config.Labels}}' > evidence/app-labels-after-reconcile.json

Compose may recreate a container when configuration changes. Capture the old/new container IDs if you want to prove reconciliation rather than assuming an in-place mutation.

12. Challenge: choose the layer, not a command

The toolbox service is missing, readiness is healthy, and the app is running. Which layer should you inspect first?

  • If the toolbox is absent: inspect profile/model selection.
  • If ./app cannot be found during config: inspect path resolution and file ordering.
  • If app never starts while readiness is unhealthy: inspect the dependency health layer.
  • If the page does not update under Watch: inspect watch path/ignore/target permissions, not the Docker network first.

13. Exact cleanup

docker compose \
  -f compose.yaml \
  -f overlays/compose.dev.yaml \
  --profile debug \
  down --remove-orphans

# Verify only the named lab project is gone:
docker ps -a --filter label=com.docker.compose.project=da-compose16
docker network ls --filter label=com.docker.compose.project=da-compose16

# Optional exact local image cleanup after verifying no other container uses it:
docker image rm da-compose16-app:lab 2>/dev/null || true

The lab does not use broad prune commands. Cleanup is scoped to the Compose project and explicitly named image.

Next lesson

Next: Configuration, Design Choices, and Tradeoffs

Now compare the advanced design choices: override vs include, profiles vs separate projects, startup conditions vs resilient retry, and each Watch action against the state and trust boundaries it changes.

Knowledge check

Why did the lab run config before up?

Why is ./app correct inside overlays/compose.dev.yaml?

Why did toolbox not start initially?

What does service_healthy change in the lab?

What is the state difference between Watch sync and rebuild?

Official references and version notes

Version baseline checked 2026-09-21.

Upstream Compose is v5.5.1. include requires Compose 2.20.3+ in current Docker documentation; Compose Watch requires 2.22.0+. Because installations can lag or be vendor-packaged, every lab records docker compose version and validates features with docker compose config before changing runtime state.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.