Skip to main content
RunBook Academy

Docker & ContainersXXVIII Β· MaintenanceDisk accounting

Where the disk went β€” reading `docker system df` correctly

Intermediate⏱ ~20 mindocker

What you'll learn

  • Interpret every column of `docker system df` and `docker system df -v`
  • Explain why the sum of image sizes exceeds the disk they occupy
  • Name the disk consumers that `docker system df` does not account for
  • Locate the real consumer before running any prune command

Prerequisites

Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11

Not yet marked complete on this device.

Every Docker maintenance task begins with the same question: what is eating the disk? The wrong answer sends you to a prune command that reclaims two hundred megabytes while the actual consumer keeps growing.

This lesson is about measuring first. The commands are all read-only. Nothing here deletes anything β€” that is the next lesson, deliberately.

The four categories

Read-only / Safesystem df
$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          34        10        17.98GB   14.46GB (80%)
Containers      10        9         192.5kB   49.15kB (25%)
Local Volumes   47        8         2.901GB   1.684GB (58%)
Build Cache     87        0         7.446GB   5.124GB

Illustrative output

Four categories, and each one is stored somewhere different underneath /var/lib/docker:

CategoryWhere it livesWhat it is
Imagesoverlay2/ plus image/ metadataRead-only layers pulled or built
Containersoverlay2/ writable layersPer-container changes since start
Local Volumesvolumes/Named and anonymous volume contents
Build Cachebuildkit/BuildKit’s content-addressed cache

The host above has almost 18 GB of images and 7.4 GB of build cache against 193 kB of container writable layers. That ratio is typical and it is the first thing to internalise: on a normal production host, containers are almost never the problem. Images and build cache are.

What ACTIVE means, per category

ACTIVE does not mean β€œin use right now”. It means something slightly different in each row, and the differences are where operators go wrong.

  • Images. Active means referenced by at least one container β€” running or stopped. An image used only by a container you stopped last week still counts as active.
  • Containers. Active means running. Stopped and created containers are counted in TOTAL but not in ACTIVE.
  • Local Volumes. Active means attached to at least one container, again running or stopped.
  • Build Cache. Active is effectively always 0 in the summary; build cache records are not held by containers.

RECLAIMABLE is an upper bound with a footnote

The RECLAIMABLE figure for images is what docker image prune -a would free β€” every image not referenced by a container. The percentage in brackets is that number over SIZE.

For volumes, RECLAIMABLE counts every unattached volume, including named ones. But the default docker volume prune only removes anonymous volumes. So the volume row routinely reports more reclaimable space than the default prune will actually reclaim. That gap is a feature: it is the difference between β€œspace that is not referenced” and β€œspace it is safe to delete”.

The verbose view is the one that finds the culprit

Read-only / Safesystem df -v
docker system df -v

The images section gains three columns the summary does not have:

Read-only / Safeimages detail
$ docker system df -v
Images space usage:

REPOSITORY   TAG              IMAGE ID       SIZE      SHARED SIZE   UNIQUE SIZE   CONTAINERS
myapp        dev              0ef9d88b4b73   1.05GB    384.8MB       669.4MB       0
myapp        release-2026-07  bea532850da8   964MB     424.8MB       539.3MB       0
postgres     16.14-bookworm   92620daddcd9   622MB     0B            621.8MB       1
python       3.12-slim        6c4dd321d176   179MB     176.9MB       2.179MB       0
nginx        1.27-alpine      97d490c12ba5   93.5MB    12.92MB       80.58MB       0

Illustrative output

  • SIZE is the logical size of the image: every layer added up.
  • SHARED SIZE is the part of that made of layers other images also use.
  • UNIQUE SIZE is what deleting this image would actually free.
  • CONTAINERS is the reference count that drives ACTIVE.

Look at python:3.12-slim: 179 MB of size, 176.9 MB of it shared, 2.2 MB unique. Deleting it frees 2 MB. Meanwhile myapp:dev has 669 MB of unique layers and no containers β€” that one is worth 669 MB.

Why the numbers do not add up to du

Compare the two:

Read-only / Safecross-check
docker system df
sudo du -sh /var/lib/docker
df -h /var/lib/docker

They will disagree, in both directions, for reasons worth knowing:

What docker system df does not show

This is the part that costs people an outage.

Container logs

The Containers row counts the writable layer only. Log files live at /var/lib/docker/containers/<id>/ β€” outside the layer β€” so they are invisible to docker system df.

Read-only / Safecontainer sizes
$ docker ps -s --format 'table {{.Names}}\t{{.Size}}'
NAMES        SIZE
grafana      4.1kB (virtual 576MB)
prometheus   20.5kB (virtual 297MB)
postgres     20.5kB (virtual 461MB)

Illustrative output

Those containers have been up for weeks. Their reported size is kilobytes. The JSON log for a chatty container next to them can be gigabytes, and no Docker df column will ever mention it. Find those separately:

Read-only / Safelog sizes
sudo du -sh /var/lib/docker/containers/* | sort -h | tail -10

Rotation is the fix, and it is covered in the logging part of this course. What matters here is knowing the number is missing from your measurement.

Bind mounts

A bind mount is a host path. It is not a Docker object, so it appears in no Docker accounting anywhere. A container writing 40 GB into /srv/appdata shows up only in df -h.

The containerd content store

Pulled image content is also held by containerd under /var/lib/containerd. On a host that has been running a long time this is usually modest, but it is a separate tree with a separate lifecycle and it is not in the four categories.

A measurement routine

Run this before any prune, every time. It takes ten seconds and it is the difference between a targeted cleanup and a guess.

Read-only / Safepre-prune survey
# 1. Filesystem reality
df -h /var/lib/docker

# 2. Docker's four categories
docker system df

# 3. Per-object detail for whichever category dominates
docker system df -v

# 4. The blind spot: container logs
sudo du -sh /var/lib/docker/containers/* | sort -h | tail -5

# 5. The other blind spot: bind mount targets
docker ps --format '{{.Names}}' | xargs -r -I{} docker inspect -f '{{.Name}} {{range .Mounts}}{{.Source}} {{end}}' {}

Step 3 is the one that decides what you do next. If images dominate, you have an image retention problem. If build cache dominates, you have a build host without a cache policy. If volumes dominate, you have orphans. Each of those is a different lesson in this part, and each has a different safe procedure.

Knowledge check

Knowledge check Β· 4 questions

  1. Q1. Which column of `docker system df -v` predicts how much disk space deleting a specific image would actually free?

  2. Q2. A container has been running for three weeks and writes heavily to stdout. What does the Containers row of `docker system df` report for it?

  3. Q3. Which of these consume host disk but are NOT accounted for anywhere in `docker system df`? Select all that apply.

  4. Q4. An image referenced only by a container that is stopped still counts as ACTIVE in the Images row of `docker system df`.

Passing score: 75%. Answers are checked in this browser.