The introductory Compose lesson showed a working stack. This lesson explains the service keys you will actually configure in every project, then focuses on the problem that breaks most first Compose files: the API starts before the database is ready. After it you will use depends_on with conditions, write healthchecks that mean "ready" rather than "running", and know the day-to-day command set.
Compose reads compose.yaml (the older docker-compose.yml still works) from the current directory. The top-level version: key is obsolete and can be removed; name: sets the project name that prefixes containers, networks and volumes. The important service keys are:
| Key | Purpose |
|-----|---------|
| image / build | Use an existing image, or build one (context, dockerfile, args, target) |
| ports | Publish "host:container"; long form adds protocol and host_ip |
| environment / env_file | Variables as a map, or loaded from files |
| volumes | Named volumes, bind mounts (./src:/app/src:ro) and anonymous volumes |
| networks | Attach to named networks; omit to use the project default |
| command / entrypoint | Override CMD and ENTRYPOINT |
| restart | no, always, on-failure, unless-stopped |
| deploy.resources.limits | cpus and memory caps, honoured by docker compose too |
Compose creates a user-defined network for the project, so services reach each other by service name; db:5432 in the sample resolves to the database container.
The short form of depends_on only controls start order: Compose starts db before api, but "started" means the container process exists, not that PostgreSQL is accepting connections. The long form adds a condition:
depends_on:
db:
condition: service_healthy # wait for the healthcheck to pass
migrate:
condition: service_completed_successfully # wait for a one-off job to exit 0
cache:
condition: service_started # the short-form behaviour
required: false # do not fail if cache is missing from this runservice_completed_successfully is the clean way to run database migrations: a migrate service runs once, exits 0, and only then does api start. When you docker compose up api, Compose also starts everything api depends on, and docker compose down stops them in reverse order.
A healthcheck block overrides or adds to the image's HEALTHCHECK. The test must exist in the container's image:
services:
db:
image: postgres:17
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d shop"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
api:
build: ./api
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 15s
start_period: 20sCMD runs the command directly; CMD-SHELL runs it through sh -c, which allows pipes and ||. Set start_period generously for services that need time to initialise, and keep interval short in development so service_healthy resolves quickly. docker compose ps shows healthy or unhealthy per service.
Even with a passing healthcheck, applications should still retry their first connection: a database can restart later, and a resilient client handles that on its own.
docker compose up -d --build # build if needed, start in the background
docker compose ps # status and health of every service
docker compose logs -f api # follow one service's logs
docker compose exec api sh # shell into a running service
docker compose run --rm api npm test # one-off container with the service config
docker compose restart api
docker compose down # stop and remove containers and networks
docker compose down -v # also remove named volumes (data loss)
docker compose config # print the fully resolved fileup recreates only services whose configuration or image changed; add --force-recreate to recreate everything. docker compose config is the fastest way to debug interpolation and merge problems because it prints exactly what Compose will run.
depends_on and adding sleep 10 to the API's command instead of a healthcheck condition.test: "curl ..." for an image that does not contain curl; the check fails forever.docker compose down -v casually and deleting the database volume.What does short-form `depends_on: [db]` guarantee?
compose.yaml needs no version:; name: sets the project prefix and services share a default network.depends_on orders start-up only; the long form with condition: waits for health or completion.start_period prevents false failures during boot.service_completed_successfully runs migrations before the API starts.up -d --build, ps, logs -f, exec, run --rm, down and config cover daily work; down -v deletes data.Next lesson: Compose Profiles, Override Files and Multiple Environments — run the same stack differently in development, testing and production.