Docker no space left on device: the Complete Cleanup Ladder, Safe to Aggressive
Symptom: build or pull dies halfway — out of space
# During build
failed to solve: ... write /var/lib/docker/tmp/...: no space left on device
# During pull
failed to register layer: Error processing tar file(no space left on device)
# A running container writing logs
Error response from daemon: ... no space left on device
One-line diagnosis: the host partition holding /var/lib/docker is full. Docker stacks images with copy-on-write layers — every build, pull, and run piles more into the same directory, and most of that pile is regenerable garbage.
The same failure can masquerade as other symptoms: a container that OOMs and never comes back, a compose service stuck at Creating, a CI box that was green yesterday and red today. Do not buy a larger disk first — measure who ate the space.
First, see who ate the space
df -h /var/lib/docker # how much is left on the partition
docker system df # Docker's own view of four categories
docker system df -v # per-image / container / volume breakdown
The docker system df output maps to four cleanup dimensions:
| Category | What it is | Cleanability |
|---|---|---|
| Images | images (incl. dangling layers) | mostly regenerable |
| Containers | writable layers of stopped containers | business call |
| Local Cache | build cache | regenerable |
| Volumes | persistent data | ⚠️ data — leave alone by default |
Adding -v shows per-image and per-container sizes. Locate before you delete — don't start with prune -a.
When two engineers disagree about what filled the disk, these commands end the debate:
# inodes vs bytes — "disk full" with free bytes is often inode exhaustion
df -i /var/lib/docker
# honest directory map (-x stays on one filesystem)
du -xh --max-depth=1 /var/lib/docker | sort -rh | head
The cleanup ladder: safe to aggressive, one rung at a time
# Rung 1: stopped containers only (the non-Up ones in docker ps -a)
docker container prune
# Rung 2: dangling images (<none>:<none> layers left by failed builds)
docker image prune
# Rung 3: every image "not referenced by any container" — including old tags
docker image prune -a
# Rung 4: build cache
docker builder prune
# The combo: all of the above (volumes excluded)
docker system prune
Most "disk full" incidents resolve by rung two or three. Each command lists what it will delete and how much it reclaims — read it before pressing y.
When you need a larger reclaim and you are sure old tags are disposable:
docker system prune -a # every image not used by a container
docker builder prune -af # force-clear BuildKit cache
Pure CI builders can schedule -af daily. Laptops and production nodes should not default to -a, or yesterday's base image becomes today's re-download.
The most dangerous switch: --volumes
docker system prune --volumes # ⚠️ deletes "unreferenced" volumes too
A database container was recreated and its old volume no longer referenced? --volumes deletes it with the data inside. Unless you know exactly what you're doing, leave this flag alone. On production, back up before deleting any volume, or at least:
docker volume ls
docker volume inspect <name> # check Mountpoint before you prune
Also never rm -rf /var/lib/docker/* — bypassing Docker's metadata desynchronizes the registry from the actual files, and every subsequent image operation fails in bizarre ways. Always clean up through docker's own commands.
Three root causes of "cleaned, then full again"
1. Unbounded container logs
The json-file log driver has no size limit by default — one log-spamming container can fill the disk:
du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head -5
The lasting fix: log rotation in /etc/docker/daemon.json (applies to new containers; old ones need recreation):
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "3" }
}
Then sudo systemctl restart docker. A single container can override with docker run --log-opt max-size=50m. For logs that already exploded, and only when the content is disposable:
truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log
2. The disk isn't full — the inodes are
df -h shows free space but the error persists? Check inodes:
df -i /var/lib/docker
# IUse% 100% ← tiny files (overlay layers, cache) exhausted the inodes
The remedy is the same prune — one leaking source inside an overlay filesystem can generate oceans of tiny files. If prune does not free inodes, use du -xh to find the outlier directory (bad hostPath mounts and leftover temp layers are common).
3. BuildKit cache and multi-arch layers
With BuildKit on, docker builder prune and docker image prune do not clear the same pile. Multi-arch manifests (--platform linux/amd64,linux/arm64) leave multiple layer sets on disk; weeks of buildx build without pruning often shows "Images look fine, Cache ate tens of GB". Ending each CI job with docker builder prune -af is cheaper than buying another disk.
overlay2 and rootfs: do not stare only at the Images line
The Images row in docker system df is not the whole of /var/lib/docker. Writable layers (containers that wrote huge temp trees), leftover tmp, and storage-driver metadata show up in du but not always as RECLAIMABLE Images. Keep a fixed order:
df -h+df -i— bytes or inodesdocker system df -v— what Docker itself accounts fordu -xh --max-depth=1 /var/lib/docker— drill intooverlay2/containers/buildkitwhen the books disagree- Only then pick a prune rung — never prune first and guess later
The space-forensics one-liners that settle arguments
docker system df -v— per-object breakdown; the summary view hides the offending volumedf -ialongsidedf -h— inodes can exhaust while bytes still show 40% freedu -xh --max-depth=1 /var/lib/docker | sort -rh | head— the honest size map across one filesystem
The inode case deserves emphasis because it presents identically to byte exhaustion, survives careless cleanups that never delete the small-file source, and is fixed by removing that source — not by random prune -a alone.
Prevention
- On CI/build machines, add a daily
docker system prune -af(pure build machines have no volumes to worry about); BuildKit hosts should also rundocker builder prune -af; - Configure log rotation in daemon.json (see above) — the item most people miss;
- Monitor the partition holding
/var/lib/docker, alert at 80%; monitor inode use too; - On production, mount
/var/lib/dockeron a dedicated partition so a full disk cannot take down system logs or databases; - On laptops, prune unused tags regularly instead of waiting for a once-a-quarter emergency
prune -a.
When writing Dockerfiles, keep layers and build context lean — unrelated files in the context inflate cache. Use the Dockerfile Generator to sanity-check base images and multi-stage layout, then lock runtime flags with Run to Compose. When estimating sizes, the Byte Converter avoids mixing GiB and GB.
Checklist
df -h /var/lib/docker+docker system df— space and breakdown at a glance- Climb the ladder: container prune → image prune → builder prune
- No
--volumesunless desperate; neverrm -rf /var/lib/docker - Full again soon after? Check container logs with
du -sh .../*-json.log - Space available but still failing?
df -ifor inodes - Is log rotation configured in daemon.json? Was BuildKit cache cleared?
- When
duandsystem dfdisagree, drill intooverlay2/buildkit
Triage notes
The cleanup commands form a ladder rather than jumping straight to system prune -a, because most full disks resolve by rung two or three (stopped containers, dangling images), and each rung lists exactly what it would delete — locate before you delete. The "cleaned but fills up again" section exists for the inode-exhaustion case where df -h still shows space, and for the overlay2 writable layer that makes du and system df disagree — system df only counts what Docker itself accounts for. --volumes gets its own section marked most dangerous because after a database container is recreated, the old volume is no longer referenced and gets deleted along with its data.
Provided by ToolVault. Related tools: Dockerfile Generator, Run to Compose, Byte Converter, Linux Cheatsheet. Related reading: Docker daemon unreachable, Port already in use (EADDRINUSE), executable file not found. See the homepage for more developer tools.
Related Tools
Related Articles
Linux Crontab in Practice: Commands, Logs, and Debugging
Crontab beyond syntax: three must-know commands, /etc/cron.d vs user crontabs, five-step debugging, PATH/timezone/% escaping, MAILTO, and what to do when you need second-level scheduling.
docker run to docker-compose: The Complete Migration Guide (with Flag Map)
Migrate docker run commands to docker-compose.yml: five advantages of compose over raw run, a flag-by-flag mapping table (ports, volumes, env, restart, networks), the classic pitfalls (bare -e passthrough, anonymous volumes, --rm semantics), and an online converter that runs locally in your browser.
Permission denied (publickey): 6 Reasons Git Push Fails Over SSH (and the Fix for Each)
git clone or push rejected with Permission denied (publickey) fatal: Could not read from remote repository? Covers missing keygen, key not loaded in the agent, public key not added to GitHub/GitLab, multi-account key routing with ~/.ssh/config, deploy key limits, and wrong remote URLs — with ssh -v diagnostics.