Seven parts built the stack; this one is about living with it. A morning with the sample API and its PostgreSQL: how much disk Docker has taken and which prune gives it back without deleting another project's database, the five commands of the daily loop with their timings, what a restart policy does and does not do when a container is killed, how Docker judges health and what it does with an unhealthy dependency, what docker events shows, and a one-file pre-push check that passes in ten seconds. Every output is quoted; Docker Desktop 4.95.0, Compose v5.6.0. This run's copy of compose.yaml publishes the two ports on 127.0.0.1 only, so its port column reads 127.0.0.1:15881 where the Part 6 reader file says 15851.

Steps in this part
  1. docker system df, then docker system df -v and docker builder du; read what each prune --help would remove and run none of them
  2. docker compose up -d, ps, logs api --tail 50, exec db psql, top, restart api, stop, start, with a logs -f --since 1m running alongside
  3. Add a develop.watch block, run docker compose watch, save Program.cs, time the new answer
  4. docker inspect --format "{{json .HostConfig.RestartPolicy}}"; a container with --restart unless-stopped killed, one that exits on its own, both stopped
  5. docker inspect --format "{{.State.Health.Status}}" and {{json .Config.Healthcheck}}; a copy of the compose file whose db test is ["CMD", "false"]
  6. docker events --since 2m --until 0s --filter container=dw-p8-api-1 --format "{{.Type}} {{.Action}} {{.Actor.Attributes.name}}"
  7. check.ps1 twice, then once with a broken healthcheck

Where the disk went

PS> docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          14        3         6.645GB   4.661GB (70%)
Containers      3         0         49.15kB   49.15kB (100%)
Local Volumes   14        2         1.118GB   980.7MB (87%)
Build Cache     404       0         21.99GB   9.576GB

Before the sample was even started, the build cache was more than three times all images together, after a day of building this series. docker builder du lists its 404 records and ends with "Reclaimable: 21.99GB", where system df says 9.576GB; the two commands disagree and this part reports both. With the project running, docker system df -v showed the share that was really ours:

dw-p8-api                         latest    bb0753428961   20 minutes ago   342MB     340MB         2.215MB       1

Of the 342 MB image, 2.215 MB is unique; the rest is the aspnet:10.0 base shared with every other image built on it. The one dangling image on the PC was somebody else's publish from earlier in the day and was left alone, which is the point of the next paragraph. The prune commands were read with --help and not run, because this PC holds three other projects' stopped containers and their named volumes. The documentation puts the chain plainly: docker system prune removes "all stopped containers", and "Named volumes that aren't attached to a container are left alone unless you pass --all / -a". So a plain system prune would take the three stopped containers, and a volume prune -a after it would take their databases, which show LINKS 1 today only because those containers still exist. -a on image prune or system prune also removes every image without a container, the 1.25 GB SDK image included, to be downloaded again. The safe habit on a shared PC: docker builder prune for the cache, which is where the space is, docker compose down -v --rmi local for your own project when you are done with it, and the documentation's --filter "until=24h" before any wider prune.

The daily loop, timed

PS> docker compose ps
NAME          IMAGE         COMMAND                  SERVICE   CREATED         STATUS                   PORTS
dw-p8-api-1   dw-p8-api     "dotnet ClinicApi.dll"   api       9 seconds ago   Up 2 seconds             127.0.0.1:15881->8080/tcp
dw-p8-db-1    postgres:18   "docker-entrypoint.s…"   db        9 seconds ago   Up 8 seconds (healthy)   127.0.0.1:15880->5432/tcp
CommandTookWhat it printed
docker compose up -d7.3 sNetwork and volume Created, db Waiting then Healthy, api Started. Compose v5.6.0 printed the Created lines twice.
docker compose restart api0.9 sRestarting, Started. The api only; the database never noticed.
docker compose stop1.0 sapi first, then db. ps shows only the header afterwards; ps -a shows both Exited (0).
docker compose start6.3 sdb Starting, Started, Waiting, Healthy, then api. The six seconds are the healthcheck interval being waited for.

After the stop and start, docker compose exec db psql -U clinic -d clinic -c "select count(*) from patients" still answered 3: the data lives in the volume, as Part 5 showed. docker compose logs api --tail 50 opens with a warning worth decoding once:

api-1  | warn: Microsoft.AspNetCore.Hosting.Diagnostics[15]
api-1  |       Overriding HTTP_PORTS '8080' and HTTPS_PORTS ''. Binding to values defined by URLS instead 'http://+:8080'.

The Part 7 Dockerfile sets ASPNETCORE_URLS on top of the base image's own port variable; both say 8080, so the warning is harmless, and dropping the line would silence it. docker compose top showed the API running as root inside its container and PostgreSQL as user 999, with STIME in UTC. And a docker compose logs -f --since 1m left running in the background caught the restart live: "Application is shutting down...", then api-1 exited with code 0 (restarting), then the startup lines again.

compose watch: six seconds from save to answer

The sample has no develop section, so a copy of the compose file gained one under api: a watch list with action: rebuild for Program.cs and for ClinicApi.csproj. docker compose -f compose.watch.yaml watch built, started both services and printed Watch enabled. Then Program.cs was saved with a new field in the root response:

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

dotnet publish ran inside the build, the api container was recreated, and the new JSON arrived 6.8 seconds after the save, with a new machine value because it was a new container. Putting the file back triggered a second, fully cached rebuild. Stopping the watch process left both containers running; down removed them. Not tested: the sync action, which copies files into a running container without a rebuild.

Restart policies, and what kill does

{"Name":"no","MaximumRetryCount":0}

That is both Compose services, which have no restart: key: the documented default, "Don't automatically restart the container." A throwaway container started with --restart unless-stopped reported the policy in the same shape, and three experiments followed. docker kill after 14 seconds of uptime, past the documentation's rule that the policy "only takes effect after a container starts successfully", meaning "up for at least 10 seconds": the container stayed Exited (137) and did not come back, in this test; the documentation speaks of a container being "manually stopped" and does not name kill. A second container running sh -c "sleep 12; exit 3", exiting on its own:

881be8f167ee   alpine    "sh -c 'sleep 12; ex…"   15 seconds ago   Up 2 seconds              dw-p8-crash

Restarted, RestartCount 1. Then docker stop on both, and fifteen seconds later both were still exited: "If you manually stop a container, the restart policy is ignored until the Docker daemon restarts or the container is manually restarted." An existing container's policy changes with docker update --restart. What happens at Windows sign-in was read, not rebooted into: Docker Desktop starts at sign-in only if the General setting "Start Docker Desktop when you sign in to your computer" is on, and its documented default is Disabled, which matches this PC's settings file. With it on, the engine's start is a daemon restart, so always and unless-stopped containers that were running come back, ones you stopped stay stopped, and Compose services with no policy do not start. One timing to flag rather than explain: docker stop with no -t ended a container that ignores SIGTERM after 3.4 seconds, and -t 10 after 10.4; the stop reference says the daemon default "is 10 seconds for Linux containers". The post states the measurement and not a default.

Health, as Docker sees it

{"Test":["CMD-SHELL","pg_isready -U clinic -d clinic"],"Interval":5000000000,"Timeout":3000000000,"Retries":10}

Durations in nanoseconds; status healthy; Docker keeps the last five check results, each with its exit code and output, here /var/run/postgresql:5432 - accepting connections. The api has no healthcheck, so its State.Health is null and its STATUS carries no suffix. One Windows PowerShell 5.1 trap on the way: ConvertFrom-Json | Select-Object -Last 2 printed all five entries, and wrapping the first two commands in parentheses printed two. Then the broken check, a copy of the compose file whose db test is ["CMD", "false"]:

dependency failed to start: container dw-p8-db-1 is unhealthy

Exit code 1 after 51 seconds, ten retries at five seconds, the db Up 51 seconds (unhealthy) while the database itself was fine. Started over a running project, the api that was already up kept serving the three patients; from a clean start, the api stayed Created and never ran, and the port refused connections. depends_on with service_healthy is the difference between an API that starts into a missing database and one that does not start.

What docker events shows

container create dw-p8-api-1
container start dw-p8-api-1
container top dw-p8-api-1
container kill dw-p8-api-1
container stop dw-p8-api-1
container die dw-p8-api-1
container start dw-p8-api-1
container restart dw-p8-api-1

That is the api's life so far through --format: created and started by up, top from compose top, then restart api as kill, stop, die, start, restart. The kill carries signal=15, SIGTERM, for a restart and for a stop alike; 137 in a STATUS column means the grace period ran out and SIGKILL followed. Two things to know: the raw, unformatted lines carry every Compose label, including the path of the compose file under your user folder, so format them before pasting anywhere; and the reference says "Only the last 256 log events are returned", so --since reaches back only that far. The db's healthcheck shows up every five seconds as an exec_create, exec_start, exec_die triple, which is most of the noise in an unfiltered stream.

A local pre-push check, in one file

check.ps1 runs docker compose build and up -d, asks Compose which host port the api publishes for 8080 so it works with the Part 6 file's 15851 as well as this run's 15881, polls /health up to thirty times two seconds apart, reads /patients and counts the rows, prints PASS or FAIL with the elapsed time, and runs docker compose down in a finally block so the stack never stays up. It calls curl.exe by name because curl alone is an alias in Windows PowerShell 5.1. First run, with a changed line in Program.cs and no volume:

== api published at 127.0.0.1:15881
   try 1 : {"status":"ok","database":"reachable"}
   3 patients, first: Maria Garcia
PASS (3 patients, 14.8 s)

Second run, everything cached: PASS (3 patients, 10.4 s), and most of those ten seconds are Compose waiting for the first healthy check. With COMPOSE_FILE pointed at the broken file, the failing path: FAIL: docker compose up -d failed (56.6 s), exit code 1, and the down block ran anyway. A CI service would run the same three Compose commands around the same two requests; no CI run was made for this part. Not tested either: a downloaded copy under this PC's RemoteSigned execution policy, which normally needs Unblock-File first.

When something breaks

Every Docker error this site shows was reproduced on a Windows PC and fixed before it was written up. The ones this series' readers meet most:

The whole list is in the Fixes category, Docker group, and the error lookup takes the message as you see it.

The morning routine: docker compose up -d, work, docker compose stop at the end of the day; docker compose down -v --rmi local when a project is finished; docker builder prune when the disk fills; check.ps1 before every push. Nothing with -a on a PC that holds other people's volumes.

Frequently asked

How do I free disk space from Docker Desktop on Windows without losing data?
Start with docker system df: on this PC the build cache was 21.99 GB against 6.6 GB of images. docker builder prune removes only the cache. Avoid docker system prune -a and docker volume prune -a on a PC with other projects: the first removes every image without a container, the second removes named volumes whose containers are gone, which is where their databases live. For your own project, docker compose down -v --rmi local.
Does restart: unless-stopped bring a container back after docker kill or a reboot?
After docker kill, not in this test: the container stayed exited, like after docker stop. A container that exits on its own is restarted. After a Windows sign-in, the engine restarts only if Docker Desktop is set to start at sign-in, which is off by default; then always and unless-stopped containers that were running come back, and Compose services without a restart key do not.
What happens in Docker Compose when a healthcheck fails?
With depends_on condition service_healthy, docker compose up -d waits for the retries, here 10 at 5 seconds, then exits 1 with dependency failed to start: container is unhealthy. From a clean start the dependent service is created but never started; a service that was already running keeps running. docker inspect --format {{.State.Health.Status}} shows the state, and the last five check results are kept.

That closes the series. The same PC's setup, from SDKs to the firewall to the developer certificate, is the Set Up a Windows PC for .NET Development series, and the clinic schema the sample's table comes from grows into twenty tables in Database Design for Developers.