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.
- Download compose.yaml into the Part 7 project folder, next to the Dockerfile and the
dbfolder;docker compose config docker compose up -d,docker compose ps, curl/patientson 15851,docker compose logs apidocker inspect dw-clinic-db-1 --format "{{json .State.Health}}"anddocker events: see the API wait for the healthy database- Create .env with a new password,
up -dagain, curl/health, and read why it fails - Edit Program.cs,
up -d --build, thenup -dwithout it - Insert a row,
down,up -d: still there;down -v,up -d: gone --profile tools up -d, thendownwithout 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.