Compose: depends_on, Healthchecks & Profiles
The notice board from Docker Compose Basics had a small flaw: for the first 20 seconds the PHP page showed a connection error, because depends_on only waited for the MySQL container to start, not for MySQL to be ready. In development that is annoying. In production, an app that crashes on start-up because its database is still booting is a real outage.
This lesson fixes that with healthchecks and depends_on conditions, then adds the features that make Compose files ready for real projects: restart policies, optional services with profiles, override files and watch mode for development.
Healthchecks in Compose
A healthcheck is a command Docker runs inside the container again and again to check that the service works. You met the HEALTHCHECK instruction in Dockerfiles; Compose has the same feature with a healthcheck key, which also works for images you did not build, such as mysql:8.4:
services:
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: notices
MYSQL_USER: meera
MYSQL_PASSWORD: Meera-Change-Me
MYSQL_ROOT_PASSWORD: Root-Change-Me
volumes:
- dbdata:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 5s
timeout: 3s
retries: 10
start_period: 30stestis the command.CMDruns it directly;CMD-SHELLruns a string through the shell.intervalis how often it runs, andtimeoutis how long one check may take.- After
retriesfailures in a row, the container is markedunhealthy. - During
start_period, failures do not count, which gives slow services time to boot.
mysqladmin ping succeeds once the MySQL server accepts connections. Using -h 127.0.0.1 forces a network connection, which matters for the MySQL image: during its first-start setup it runs a temporary server without networking, and the check must not pass until the real server is up.
Waiting for healthy dependencies
The long form of depends_on lets you say what to wait for:
services:
web:
build: .
ports:
- "8080:80"
environment:
DB_HOST: db
DB_NAME: notices
DB_USER: meera
DB_PASSWORD: Meera-Change-Me
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfully
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/"]
interval: 30s
timeout: 3s
retries: 3
migrate:
build: .
command: ["php", "/var/www/html/migrate.php"]
environment:
DB_HOST: db
DB_NAME: notices
DB_USER: meera
DB_PASSWORD: Meera-Change-Me
depends_on:
db:
condition: service_healthyThe three conditions are:
| Condition | Compose waits until the dependency... |
|---|---|
service_started | has started (the short form's behaviour) |
service_healthy | reports healthy from its healthcheck |
service_completed_successfully | has run to the end with exit code 0 |
Here migrate is a one-off job that creates the tables and exits. web starts only after the database is healthy and the migration has finished successfully. restart: true under db tells Compose to restart web when db is restarted by a Compose command, so it reconnects cleanly.
Now up waits for health before starting the app:
docker compose up -d
docker compose ps --format 'table {{.Service}}\t{{.Status}}'[+] Running 4/4 ✔ Network notice-board_default Created 0.1s ✔ Volume "notice-board_dbdata" Created 0.0s ✔ Container notice-board-db-1 Healthy 21.4s ✔ Container notice-board-web-1 Started 21.7s SERVICE STATUS db Up 2 minutes (healthy) web Up 2 minutes (healthy)
(The migrate lines are left out for space.) The app never sees a missing database. In scripts and CI, docker compose up -d --wait goes one step further and only returns when every service is running and healthy, or fails if one is not.
Restart policies
restart takes the same values as docker run --restart: no, always, on-failure and unless-stopped. For long-running services on a server, unless-stopped is the usual choice. Do not put a restart policy on one-off jobs such as migrate, or Compose will run them again and again.
Optional services with profiles
Some services are only needed sometimes: a database admin tool, a mail catcher for testing emails, a load-testing script. Give them a profile, and Compose leaves them out unless that profile is switched on:
adminer:
image: adminer
ports:
- "127.0.0.1:8081:8080"
profiles: [tools]docker compose up -d # web, db and migrate only
docker compose --profile tools up -d # also starts adminer
COMPOSE_PROFILES=tools docker compose up -dServices without profiles always start. Naming a profiled service directly, such as docker compose up -d adminer, also starts it. Profiles let one file serve developers, testers and production without copies.
Override files
When Compose finds compose.override.yaml next to compose.yaml, it merges the two automatically. Teams use this to keep production settings in compose.yaml and development extras in the override:
# compose.override.yaml (development only)
services:
web:
volumes:
- ./src:/var/www/html
environment:
APP_DEBUG: "1"For production, name the files explicitly so the override is not used: docker compose -f compose.yaml -f compose.prod.yaml up -d. Later files override earlier ones, and docker compose config shows the merged result.
Watch mode for development
Instead of a bind mount, Compose can watch your files and update containers when they change. Add a develop section to a service:
develop:
watch:
- action: sync
path: ./src
target: /var/www/html
- action: rebuild
path: DockerfileRun docker compose watch (or docker compose up --watch). Editing a PHP file copies it into the running container at once (sync); editing the Dockerfile rebuilds the image and replaces the container (rebuild). A third action, sync+restart, copies the files and then restarts the container, which suits apps that read their configuration only at start-up. Watch avoids the slow file sharing and hidden node_modules problems that bind mounts can cause on Windows and macOS.
Common mistakes
- Using short
depends_onand expecting the database to be ready. It only orders container start-up. Add a healthcheck andcondition: service_healthy. - A healthcheck command that does not exist in the image.
curlis missing from many slim and Alpine images, so the check fails forever. Use a tool the image has. - Restart policies on one-off jobs. A migration with
restart: alwaysruns in a loop. - Forgetting that
compose.override.yamlloads automatically. Development bind mounts can end up on a server by accident. Use explicit-ffiles in production.
Key points
- Add a
healthcheckwithtest,interval,timeout,retriesandstart_periodso Docker knows when a service is really working. depends_onwithcondition: service_healthywaits for readiness;service_completed_successfullywaits for one-off jobs.docker compose up -d --waitreturns only when everything is healthy, which is ideal for scripts and CI.profileskeep optional services out of a normalup; enable them with--profileorCOMPOSE_PROFILES.compose.override.yamlmerges automatically;develop.watchsyncs or rebuilds on file changes.
Exercise
Add a cache service using the redis:8-alpine image to the notice board. Give it a healthcheck that runs redis-cli ping every 5 seconds, and make web wait until it is healthy. Then add a mailpit service (image axllent/mailpit, web interface on container port 8025) that only starts with a profile called mail.
Show solution
cache:
image: redis:8-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
mailpit:
image: axllent/mailpit
ports:
- "127.0.0.1:8025:8025"
profiles: [mail]web:
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
cache:
condition: service_healthyredis-cli ping exits with 0 when Redis answers PONG, so the container becomes healthy. The web block shows only the changed depends_on part. docker compose --profile mail up -d starts Mailpit as well.
Quick Quiz
1. What does the short form depends_on: [db] guarantee?
Starting is not the same as ready; use condition: service_healthy for readiness.
2. Which condition waits for a one-off job such as a migration to finish without errors?
It waits until the dependency exits with code 0.
3. A service has profiles: [tools]. When does docker compose up -d start it?
Services without profiles always start; profiled services are opt-in.
4. Why is docker compose up -d --wait useful in CI pipelines?
The next CI step can safely run tests against the started services.
Learn Docker & DevOps with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
