Docker & ContainersXXXIV Β· Capacity PlanningCapacity
Disk capacity β modelling how /var/lib/docker grows
What you'll learn
- Attribute /var/lib/docker consumption to images, volumes, logs and build cache
- Model each of the four growth rates and size the filesystem from them
- Set retention that bounds growth instead of reacting to a full disk
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
Docker hosts do not run out of disk suddenly. They run out of disk on a schedule that was set months earlier, by four independent processes that grow at four different rates, none of which anyone measured.
This lesson replaces βadd a monitor at 85%β with a model that tells you when the disk will fill and how large it should have been.
The four growth processes
| What | Lives in | Grows with | Bounded by |
|---|---|---|---|
| Image layers | overlay2/ | Deploy frequency x image delta | An image retention policy |
| Volumes | volumes/ | Application data | The application, or nothing |
| Container logs | containers/ | Request rate x log verbosity | max-size x max-file, if set |
| Build cache | buildkit/ | Builds on this host | --reserved-space, if set |
Three of the four are bounded only if you bound them. The default for each is βgrow foreverβ.
Reading the actual split
docker system dfTYPE 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.124GBThat host is carrying 28.3 GB of Docker state, of which 21.3 GB is reclaimable. Ten images are in use and twenty-four are not; eight volumes are attached and thirty-nine are orphaned. Neither number is a crisis today and both are a crisis in four months.
docker system df -v | sed -n '/Images space usage/,/Containers space usage/p' | head -8Images space usage:
REPOSITORY TAG IMAGE ID SIZE SHARED SIZE UNIQUE SIZE CONTAINERS
app-web dev 0ef9d88b4b73 1.05GB 384.8MB 669.4MB 0
debian bookworm-slim abd67ffcfa54 116MB 0B 115.8MB 0
app-web ci bea532850da8 964MB 424.8MB 539.3MB 0The model
Size the filesystem from the four rates plus a floor:
required = base_images
+ deploys_per_day x image_delta x image_retention_days
+ volume_growth_per_day x volume_retention_days
+ containers x log_max_size x log_max_files
+ build_cache_ceiling
+ headroom
headroom is not decoration. OverlayFS is a copy-on-write filesystem
and every write to an existing file in a container copies the whole
file up into the container layer first. At high utilisation the
allocator fragments and copy-up slows down measurably. Budget 20% and
treat 80% as the number your alert fires at, not 90%.
Worked example
A host runs 12 containers of one application, deployed 4 times a day.
The image is 400 MB with a 60 MB delta per build. Postgres grows
1.5 GB a month. Logging is json-file with max-size=50m and
max-file=3. Builds run on this host with a 10 GB cache ceiling.
base_images = 400 MB
image churn = 4 deploys/day x 60 MB x 14 days retention = 3.4 GB
volume growth = 1.5 GB/month x 12 months = 18.0 GB
logs = 12 containers x 50 MB x 3 files = 1.8 GB
build cache = 10.0 GB ceiling = 10.0 GB
--------
subtotal 33.6 GB
headroom (subtotal / 0.8) 42.0 GB
A 64 GB filesystem for /var/lib/docker gives 18 months of runway.
A 40 GB one, which looks generous next to the 28 GB the host uses on
day one, fills in under a year and does so during a deploy.
Bounding each term
Logs
Unbounded json-file logging is the most common way a Docker host
fills, because it is the only term that scales with traffic rather
than with your deploy cadence. Set the bound in the daemon so it
applies to every container, including the ones nobody reviewed:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
docker ps -aq | while read -r c; do
P=$(docker inspect --format '{{.LogPath}}' "$c")
[ -n "$P" ] && [ -f "$P" ] && printf '%10s %s\n' "$(du -h "$P" | cut -f1)" "$(docker inspect --format '{{.Name}}' "$c")"
done | sort -rh | head 2.1G /legacy-batch
50M /api
50M /webIllustrative output
The daemon-level log-opts apply to containers created after the
setting, so a long-lived container started before the change keeps
growing. Finding the 2.1 GB file is the point of the check.
Images
# Remove images not referenced by a container and older than 14 days.
docker image prune -a --force --filter 'until=336h'
until takes a duration or a timestamp. Prefer a duration in hours
in automation: 336h is unambiguous, 14d is not accepted by every
filter.
Build cache
# Trim the BuildKit cache to a 10 GB ceiling.
docker builder prune --force --reserved-space 10GB
--reserved-space is the current spelling. Older material and older
CI scripts use --keep-storage, which still works but prints
Flag --keep-storage has been deprecated. There are two siblings
worth knowing: --max-used-space caps the cache from above, and
--min-free-space prunes until the filesystem has that much free,
which is the one you want on a shared host.
docker buildx du | tail -3Private: 5.124GB
Reclaimable: 7.446GB
Total: 7.446GBVolumes
Volume pruning is the one to be careful with. docker volume prune
removes anonymous volumes not attached to a container, and an
anonymous volume is exactly how a database that was started without
-v keeps its data. Always prune volumes with --filter on a label
you control, never bare.
# Only volumes this project created and labelled as ephemeral.
docker volume prune --force --filter 'label=lifecycle=ephemeral'
Inodes: the other way to run out
overlay2 stores every file of every layer as a real file. An image
with a large node_modules or a Python site-packages tree can carry
hundreds of thousands of small files, and each one is an inode.
df -h /var/lib/docker
df -i /var/lib/dockerFilesystem Size Used Avail Use% Mounted on
/dev/sda2 126G 57G 65G 47% /
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda2 8388608 1151104 7237504 14% /No space left on device with 60 GB free is inode exhaustion. Budget
roughly 1 million inodes per 100 GB for a Docker filesystem, and check
df -i in the same alert as df -h β most monitoring templates ship
with only the byte check.
- Measure the four rates over a fortnight.
docker system dfdaily into a file is enough; you need the slope, not precision. - Bound logs at the daemon, then find and recreate containers created before the setting.
- Set an image retention filter and run it from a timer, not by hand.
- Cap the build cache with
--reserved-spaceon any host that builds. - Alert on 80% bytes and 80% inodes, both, on
/var/lib/dockerspecifically rather than on/. - Re-run the model quarterly, because the deploy cadence is the term that changes most and nobody announces it.
Sanity check
Knowledge check Β· 4 questions
Q1. Two images show 1.05 GB and 964 MB in `docker system df -v`, with about 400 MB of SHARED SIZE. How much does deleting the first free?
Q2. A host reports `No space left on device` while `df -h` shows 60 GB free. What should you check next?
Q3. Which growth terms are unbounded unless you explicitly configure a limit? Select all that apply.
Q4. `docker system prune -a --volumes` is safe to schedule nightly on a production host because it only removes unused objects.
Passing score: 75%. Answers are checked in this browser.