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.
docker system df, thendocker system df -vanddocker builder du; read what eachprune --helpwould remove and run none of themdocker compose up -d,ps,logs api --tail 50,exec db psql,top,restart api,stop,start, with alogs -f --since 1mrunning alongside- Add a
develop.watchblock, rundocker compose watch, saveProgram.cs, time the new answer docker inspect --format "{{json .HostConfig.RestartPolicy}}"; a container with--restart unless-stoppedkilled, one that exits on its own, both stoppeddocker inspect --format "{{.State.Health.Status}}"and{{json .Config.Healthcheck}}; a copy of the compose file whose db test is["CMD", "false"]docker events --since 2m --until 0s --filter container=dw-p8-api-1 --format "{{.Type}} {{.Action}} {{.Actor.Attributes.name}}"- 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
| Command | Took | What it printed |
|---|---|---|
docker compose up -d | 7.3 s | Network and volume Created, db Waiting then Healthy, api Started. Compose v5.6.0 printed the Created lines twice. |
docker compose restart api | 0.9 s | Restarting, Started. The api only; the database never noticed. |
docker compose stop | 1.0 s | api first, then db. ps shows only the header afterwards; ps -a shows both Exited (0). |
docker compose start | 6.3 s | db 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 container name is already in use and port is already allocated, the two leftovers of a previous run
- ports are not available, forbidden by its access permissions, the Windows excluded ranges
- cannot attach stdin to a TTY-enabled container, the
-itfrom a script - Git Bash rewriting container paths
- 28P01 after changing POSTGRES_PASSWORD, superuser password is not specified, postgres:18 exits at once with the old volume path and database files are incompatible with server
- no configuration file provided, repository name must be lowercase, pull access denied and failed to resolve reference, not found
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.