Docker & ContainersXXVIII Β· MaintenanceReclaiming disk
Build cache maintenance β the quiet largest consumer
What you'll learn
- Measure build cache with `docker buildx du` and read the shared/private split
- Prune build cache with a bound rather than wholesale
- Configure BuildKit garbage collection in `daemon.json`
- Distinguish cache loss from cache invalidation when builds get slow
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
On a host that builds images, build cache is usually the largest single
consumer of disk and the last one anyone looks at. It does not appear in
docker images, it has no tags, and its entries are opaque hashes. The
7.4 GB in the Build Cache row of the previous lesson was larger than every
container writable layer on the host by four orders of magnitude.
It is also the only Docker object with a real garbage collector, which makes it the easiest of the four categories to stop worrying about.
Measuring it
docker system df gives you the total. docker buildx du gives you the
records.
$ docker buildx duID RECLAIMABLE SIZE LAST ACCESSED
j5j2jprjqbdgiobhyqqukak41 true 248B* 10 days ago
50w5fx23527c0ex1kai5m4nss true 405B* 6 days ago
7zxq95ogag1jtq3tc40zfqgcz true 957B* 6 days ago
j66nkr4hb6t12vmyoz92ple0c true 553.1MB 6 days ago
Shared: 2.322GB
Private: 5.124GB
Reclaimable: 7.446GB
Total: 7.446GBIllustrative output
Four numbers at the bottom, and they answer different questions:
- Shared β cache blobs that are also referenced by an image on this host. Pruning the cache record does not free these, because the image still holds the layer.
- Private β blobs held only by the cache. This is what a prune actually frees.
- Reclaimable β the total of records marked
RECLAIMABLE true. - Total β everything, including records currently pinned by a running build.
The asterisk on some sizes marks a shared record. When you are estimating how much a prune will return, Private is the number, not Reclaimable.
docker buildx du --verboseThe verbose form adds the Dockerfile step or mount that produced each record.
That is how you find out that 5 GB of your cache is one RUN npm ci layer
retained across forty builds of a Node image.
Why it grows without bound
Pruning with a bound
The wholesale form is the one everyone runs and the one you should almost never use:
docker builder prune -a -f-a on build cache means βinclude internal and frontend records tooβ β it is
not the same widening as -a on images, but the practical effect is the
same: the next build starts cold.
The bounded forms are better:
# Drop records not touched in a week
docker builder prune --filter 'until=168h' -f
# Keep a floor of cache and prune down to it
docker builder prune --reserved-space 10GB -f
# Prune until the cache is under a ceiling
docker builder prune --max-used-space 20GB -f
# Prune until the disk has this much free
docker builder prune --min-free-space 30GB -fConfiguring garbage collection so you never prune by hand
This is the part worth doing once. BuildKit inside dockerd reads its GC policy
from daemon.json:
{
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "20GB"
}
}
}With enabled: true and a keep-storage figure, BuildKit runs its own
collection in the background and holds the cache near that size. The default
policy behind that single number is a four-tier sweep, in order:
- Ephemeral cache (internal and frontend records) older than 48 hours.
- Any cache older than 60 days.
- Unshared cache over the size limit.
- Any cache over the size limit.
That ordering matters: BuildKit gives up the cheap-to-rebuild records first and only touches shared, recently used records when it has no other choice.
For a builder with its own buildkitd.toml β a docker-container driver
builder rather than the default in-daemon one β the equivalent keys live under
[worker.oci] and use reservedSpace, maxUsedSpace and minFreeSpace.
Cache loss versus cache invalidation
When builds suddenly take ten times longer, the cause is one of two things and the remedies are opposite.
| Symptom | Cause | Fix |
|---|---|---|
| Every step re-runs, including ones whose inputs did not change | Cache loss β the records were pruned or GC removed them | Re-warm the cache; raise the GC cap |
| Steps re-run from a specific line onwards, consistently | Cache invalidation β an input to that step changed | Reorder the Dockerfile; this is a build design problem |
| First build after a deploy is slow, subsequent ones fast | Normal cold cache | Nothing |
docker buildx du --verbose | head -40
docker system dfIf Total collapsed since yesterday, someone pruned. If it is unchanged and
builds are still slow, the Dockerfile changed. The build caching lesson in the
build part of this course covers the second case; this lesson is only about
the first.
Knowledge check
Knowledge check Β· 4 questions
Q1. In `docker buildx du` output, which figure best predicts the disk you will actually reclaim by pruning the cache?
Q2. Which `daemon.json` structure caps BuildKit cache for the default in-daemon builder?
Q3. Which of these cause build cache to grow faster than image count would suggest? Select all that apply.
Q4. Adding `builder.gc` to `daemon.json` and restarting the daemon immediately shrinks the existing cache to the configured size.
Passing score: 75%. Answers are checked in this browser.