Parts 4 and 5 needed a network, a volume, two docker run commands with eight options each and a wait between them. Compose is the file that says all of that once. This part brings up the clinic API and its PostgreSQL database from one compose.yaml, watches the healthcheck make the app wait, moves the password into an .env file and walks into the trap that sets, rebuilds on a code change, takes it all down with and without the data, adds an optional service behind a profile, and tries docker compose watch. Compose v5.6.0; every output quoted.

Steps in this part
  1. Download compose.yaml into the Part 7 project folder, next to the Dockerfile and the db folder; docker compose config
  2. docker compose up -d, docker compose ps, curl /patients on 15851, docker compose logs api
  3. docker inspect dw-clinic-db-1 --format "{{json .State.Health}}" and docker events: see the API wait for the healthy database
  4. Create .env with a new password, up -d again, curl /health, and read why it fails
  5. Edit Program.cs, up -d --build, then up -d without it
  6. Insert a row, down, up -d: still there; down -v, up -d: gone
  7. --profile tools up -d, then down without the profile and read the warning

What the file says, and what Compose adds

The file declares two services: db from postgres:18 with the three variables from Part 5, the init folder, a named volume and a healthcheck that runs pg_isready every five seconds; and api built from the Dockerfile, with the connection string Host=db;Port=5432 and depends_on with condition: service_healthy. name: dw-clinic at the top sets the project name. docker compose config prints the resolved file: the ${POSTGRES_PASSWORD:-clinic} expression became clinic in both places, the network got the name dw-clinic_default, the volume dw-clinic_clinic-data, and the short port syntax expanded to the long one. Read that output once; it is the file Compose actually runs.

up, and the order of events

 Image dw-clinic-api Built
 Network dw-clinic_default Created
 Volume dw-clinic_clinic-data Created
 Container dw-clinic-db-1 Started
 Container dw-clinic-db-1 Waiting
 Container dw-clinic-db-1 Healthy
 Container dw-clinic-api-1 Starting
 Container dw-clinic-api-1 Started

One command built the image, created the network and the volume, started the database, waited, and started the API. docker compose ps then showed the database as (healthy) on 15850 and the API on 15851, and /patients returned the three patients. The engine's events give the timing: the database container started at 14.2 seconds past the minute, PostgreSQL was ready at 15.8, the first healthcheck ran at 19.2, the healthy event came at 19.3 and the API started at 19.9. Most of the wait was the five-second probe interval, not the database. The documentation's words for the rule: a dependency marked service_healthy "is expected to be 'healthy', which is defined with healthcheck, before starting a dependent service." One correction from the run: the dependent container is created immediately and only started after the healthy event, which is what the line Waiting shows.

The API's log carries two lines readers ask about. The "Overriding HTTP_PORTS" warning comes from the Dockerfile setting ASPNETCORE_URLS on top of the base image's port, Part 7's subject, and is harmless. The other, Cannot load library libgssapi_krb5.so.2, is Npgsql looking for a Kerberos library the image does not have; the request still returned 200. And docker compose exec db psql worked without -T even from a shell with no terminal, which plain docker exec -it in Part 2 did not.

.env, and the trap it sets

An .env file next to the compose file with POSTGRES_PASSWORD=dw-p6-secret changes both places at once, as docker compose config showed:

      ConnectionStrings__Clinic: Host=db;Port=5432;Database=clinic;Username=clinic;Password=dw-p6-secret

up -d recreated both containers because their environment changed, the volume stayed, and /health answered 500. The API log said 28P01: password authentication failed for user "clinic", and the database log said why: "Database directory appears to contain a database; Skipping initialization". The password lives in the volume from the first start; a new variable does not change it, and the healthcheck stayed green throughout, because pg_isready does not log in. For a development database the fix is down -v and up -d, which the run did; changing the role's password inside psql keeps the data and was not run here. Precedence is documented and was confirmed: a variable set in the shell beats the .env file, and docker compose config --environment shows which value won.

Rebuilding on a change

Program.cs changed to return a version field, and docker compose up -d --build rebuilt the image and recreated the API:

{"app":"ClinicApi","machine":"3aac7abf2b0d","version":"2"}

A second edit followed by a plain up -d rebuilt nothing: the container reported Running and curl still returned version 2. docker compose build and then up -d brought version 3. The rule is easy to forget on a Monday: up starts what exists, and only --build or a separate build looks at your source.

down, and down -v

 Network dw-clinic_default Removed

After docker compose down the containers and the network were gone and the volume was not, and a fourth patient inserted before the down was still there after the next up. docker compose down -v printed the extra line:

 Volume dw-clinic_clinic-data Removed

and the next up ran the init script on an empty volume and showed three patients again. That is the clean-slate command, and the reason to keep real data out of a compose project's default volume.

Profiles, and the warning they can leave

A third service, tools, with profiles: ["tools"], is ignored by a plain up and started by docker compose --profile tools up -d. Two things the run found that the documentation does not spell out. Once the profile service runs, a plain docker compose ps does list it. And a plain docker compose down while it runs removes the other two services, leaves it running, and prints a line that is easy to miss, because the exit code is still 0:

 Network dw-clinic_default Resource is still in use

docker compose --profile tools down removed the service and the network.

Two errors, and watch

docker compose up -d in a folder with no compose file prints no configuration file provided: not found and exits with 1; the Fixes post covers the search order. The second error is a collision between Compose and a container you started by hand with the same name: Compose then fails with service:db:1 Error response from daemon: Conflict and the name-conflict message, and docker compose ps -a does not even show the intruder, because Compose only sees containers it labelled. docker rm -f -v on the plain container clears the way.

docker compose watch, with a develop: watch: block on the API service that rebuilds on any change under the project except bin and obj, built once, printed Watch enabled, and after Program.cs was saved:

Rebuilding service(s) ["api"] after changes were detected...

The new version answered 9.5 seconds after the save. One inference from its first build: it rebuilt the publish layer although no source had changed, because compose.yaml itself had, and the .dockerignore does not exclude it; add compose.yaml and .env to .dockerignore so that neither ends up in your image's build context.

Frequently asked

How do I make Docker Compose wait for PostgreSQL before starting my app?
Give the database service a healthcheck, for example pg_isready -U clinic -d clinic every 5 seconds, and in the app service use depends_on with condition: service_healthy. Compose creates both containers, starts the database, waits for the healthy event and only then starts the app; with a 5 second interval the wait is about five seconds, most of it the probe interval.
Why does my app fail with password authentication failed after I changed the password in .env?
The password was written into the database when its volume was first initialised, and a changed POSTGRES_PASSWORD does not change an existing database; the log says Skipping initialization. For a development database run docker compose down -v and up -d to start from an empty volume, or change the role's password inside psql to keep the data. The healthcheck stays healthy throughout, because pg_isready does not log in.
What is the difference between docker compose down and down -v?
down stops and removes the containers and the network and keeps the named volumes, so the data is there after the next up. down -v removes the volumes as well, and the next up runs the init scripts on an empty database. A plain down also leaves a running profile service behind and prints that the network is still in use.

Next: Part 7, building the .NET image: the two-stage Dockerfile that Compose has been using, the SDK's own container publisher, and the two ways to build an image that does not run.