Docker No Space Left on Device: Find What Fills the Disk
Fix Docker's no space left on device: read docker system df, prune images and build cache by age, keep the newest N images, cap logs and the journal.
By Matthews WongReviewed on
Key takeaway
No space left on device on a Docker host means something grows without a limit: tagged images from every deploy, build cache, container logs, the systemd journal, or volumes. Measure first with df, docker system df and journalctl --disk-usage. Then prune by age or count, cap the logs and the journal, and leave volumes alone.
You will be able to
- Find which of images, build cache, logs, the journal or volumes is using the disk, before deleting anything.
- Choose the right prune command, and say which ones destroy data.
- Cap what grows by itself: keep the newest N images, rotate container logs and vacuum the journal.
Step through the diagram
Press Play, or step with the arrows. Every step is written out below the diagram.
What fills a Docker host's disk, and what reclaims it
Five things fill the disk: images, build cache, container logs, the journal and volumes. Each has its own command to measure it or to cap it, and volumes are the one nothing should prune automatically.
Keys: ← → step · Home / End · K play or pause
Left, the five things that grow on a Docker host. Right, the command that measures or caps each one. Measure first, then pick the smallest cleanup that works.
docker system df summarises space used by images, containers and local volumes. Add -v for each item. Logs and the journal are measured elsewhere, with du and journalctl --disk-usage.
docker system df -vdocker image prune -a removes images no container uses. With until=72h it only takes those older than 72 hours. docker builder prune accepts the same until filter for build cache.
docker image prune -af --filter "until=72h"Predict the next step A busy day brings 19 deploys in 40 hours. What does the 72-hour prune remove? (Every old release · Nothing: every image is under 72 hours old · Only dangling images)So the deploy also caps by count: list the app's images newest first, skip the first two, remove the rest. docker image rm refuses an image a container uses.
docker image ls | sort -r | tail -n +3 | xargs -r docker image rmThe json-file driver defaults to unlimited max-size and one file, and max-file only works with max-size. The compose file sets 10m and 3, so each log rotates.
max-size: "10m" max-file: "3"journalctl --vacuum-size deletes the oldest archived journal files until they fit the limit. Active files stay, so --disk-usage can still read higher than the target.
journalctl --disk-usage sudo journalctl --vacuum-size=200MPredict the next step Does docker system prune with no flags remove volumes? (Yes, every unused volume · No, volumes are not pruned by default · Only named volumes)Docker never removes volumes on its own, because that could destroy data. docker volume prune takes unused anonymous volumes; named ones need --all. The deploy never runs it.
What fills the disk of a Docker host?
Five things grow on a Docker host, and only some of them grow quietly. Images pile up because every deploy pulls a new tag, and a tagged image is not dangling: Docker defines a dangling image as one that is untagged and not referenced by any container. Build cache grows with every build. Container logs have no limit until you set one. The systemd journal and volumes hold the rest.
Read more (1)Show less
The error that tells you is no space left on device, usually in the middle of an image pull or a build, not at a quiet moment. That timing is why this site prints df before and after its cleanup steps: the log then shows how much headroom each step actually bought.
How do you find what is using the space?
Start with df -h on the filesystem that is full, so you know how bad it is. Then docker system df, which Docker describes as the space used by the daemon, in rows for images, containers and local volumes. Add -v and each image, container and volume gets its own line, with the unique size an image alone uses and the size it shares with others.
Read more (1)Show less
Docker's own numbers do not cover the journal. For that, journalctl --disk-usage reports the total of all archived and active journal files. Read both before deleting anything: the biggest line is usually obvious, and it tells you which section below to use.
What does each prune command remove?
The prune commands differ in scope, and the table below sets them side by side. Two flags change the answer: -a widens image prune from dangling images to every image no container uses, and --filter until=24h style filters limit any prune to items older than the age you give. Each command asks for confirmation unless you pass -f, so reading the prompt is your last chance.
How do you keep only the last N images?
An age filter assumes a steady pace of deploys. This site's deploy had one, until 19 deploys landed in 40 hours: every image was younger than 72 hours, the prune removed nothing, and the disk reached 93%. The fix is a count cap beside the age filter: list the app's images newest first, skip the first two, and remove the rest with docker image rm.
Read more (1)Show less
Two properties make that safe. docker image rm refuses to remove the image of a running container unless you force it, so the live release survives. And removing an image from the host does not remove it from the registry, so an older release can be pulled again if you need it.
How do you cap logs and the journal?
For container logs, the fix is a setting, not a cleanup. The json-file driver's max-size defaults to -1, unlimited, and max-file defaults to 1 and only takes effect when max-size is set. This site's compose file sets 10m and 3, so a container keeps at most three log files of up to 10 MB each.
Read more (1)Show less
The journal has two tools. journalctl --vacuum-size, --vacuum-time and --vacuum-files clean it once: they delete archived journal files by total size, by age, or down to a number of files. SystemMaxUse= in journald.conf caps it for good, and by default journald uses up to 10% of the filesystem, capped at 4G. Use the vacuum to recover now and the setting to stop it coming back.
The real config behind this lesson
Excerpts from the repository that builds and deploys this website, quoted as they are.
# The 72h window assumes a few deploys a day. On 2026-10-13/14 there
# were 19 in 40 hours, every image was younger than 72h, so the
# prune above removed nothing and the disk reached 93%. So the
# portfolio's own images are also capped by count: the newest two
# are the running release and the one a rollback needs. Older
# releases stay in GHCR. `docker image rm` refuses an image a
# container still uses, so a rolled-back release is never removed.
KEEP_PORTFOLIO_IMAGES=2
PORTFOLIO_REPO="${{ needs.build-and-push.outputs.image }}"
PORTFOLIO_REPO="${PORTFOLIO_REPO%:*}"
docker image ls "$PORTFOLIO_REPO" --format '{{.CreatedAt}}\t{{.Repository}}:{{.Tag}}' \
| sort -r | tail -n +$((KEEP_PORTFOLIO_IMAGES + 1)) | cut -f2 \
| xargs -r docker image rm || true
df -h / | tail -1The count cap that sits after the age filter. docker image ls prints newest-first once sorted in reverse, tail -n +3 skips the two to keep, and xargs -r docker image rm removes the rest. The trailing || true means a refused removal never fails the deploy, and the last line prints the disk so the log shows what it bought.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"The cap on container logs. Without max-size the json-file driver never rotates, because its default is unlimited. With it, Docker rolls the file at 10 MB and keeps three, so a noisy container can no longer be the thing that fills the disk.
Mistakes that cost real time
Destructive: volume prune and system prune --volumes delete data
docker volume prune removes unused anonymous volumes, docker volume prune --all also removes unused named ones, and docker system prune --volumes adds the anonymous ones to a system prune. Once nothing references a volume, it is fair game. Docker never removes volumes by itself because of this, so do not put them in a cleanup script.
Destructive: prune -a with no filter removes your rollback image
docker image prune -a removes every image no container uses, tagged or not, and docker system prune -a does the same plus the rest. The previous release is not in use, so it goes too, and a rollback has to pull it again. Add an until filter or a count cap, as the deploy does.
A time filter alone fails on a busy day
until=72h removes only images older than 72 hours. When many deploys land inside that window, nothing qualifies and the prune frees nothing while the log still reads as success. Cap by count as well, and print df after the cleanup so a no-op shows up.
Logs with no max-size, or max-file on its own
A container without max-size keeps one log file that grows until the disk is full, and no prune command touches it. Setting only max-file does nothing, because it is only effective when max-size is also set. Set both, and remember the journal is capped separately.
The vacuum ran, but the journal is still over the limit
--vacuum-size only deletes archived journal files, while --disk-usage counts archived and active ones. The active file can keep the total above your target. That is expected, not a failed command; a SystemMaxUse= limit is what keeps the journal bounded over time.
| Command | Removes | Data loss? |
|---|---|---|
| docker image prune | Dangling images: untagged and unused | No, images can be pulled again |
| docker image prune -a | Every image no container uses, tagged too | No, but loses local rollback images |
| docker container prune | All stopped containers | Yes, stopped containers are deleted |
| docker builder prune | Dangling build cache; -a removes all unused cache | No, only slower next build |
| docker system prune | Stopped containers, unused networks, dangling images, unused build cache | Yes, stopped containers go |
| docker volume prune | Unused anonymous volumes; --all adds named ones | Yes, this is stored data |
Do it on your own machine
Use a practice machine, not a production host. You need Docker and a Linux box with systemd. Look first, delete second.
Check how full the disk is and what Docker thinks it uses.
df -h / docker system dfGet the per-item list. Note which images show 0 containers: those are the ones a prune can reach.
docker system df -vCheck the journal, which docker system df does not show.
journalctl --disk-usagePrune images older than 72 hours. Read the confirmation prompt before you answer, and do not add --volumes anywhere.
docker image prune -a --filter "until=72h"Vacuum the journal to a size, then run the usage command again and compare.
sudo journalctl --vacuum-size=200M journalctl --disk-usage
Three questions before you move on
Pick an answer to see why it is right or wrong. Nothing is saved or graded, so guess freely.
Answer in your head, then check
1.Why is a tagged image from last week not removed by a plain docker image prune?Show the answer
Answer:A plain image prune removes only dangling images, meaning untagged and unreferenced. Every deploy pulls a new tag, so old releases are tagged and survive until you add -a or remove them by count.
2.What two limits does the compose file set on a container's log, and what happens without the first?Show the answer
Answer:max-size 10m and max-file 3. Without max-size the json-file driver is unlimited, and max-file is ignored, so the log grows until the disk is full.
3.Why is it safe for the cleanup to run docker image rm on the older releases?Show the answer
Answer:The command refuses an image a running container uses, and removing it from the host leaves the registry copy alone. The newest two images are kept, and any other release can be pulled again.
Related deep dives on this site
Sources
Primary sources checked on the research date. Follow them for the current version.