Docker 'No Space Left on Device': What Is Safe to Delete

devopsdockerpostgresqlself-hosting
Docker 'No Space Left on Device': What Is Safe to Delete

Docker's no space left on device means the disk ran out of bytes or inodes. Run read-only checks first, and don't restart Docker. Plain image prune and builder prune are safe while Docker lists every container, because they remove only untagged images and dangling cache, but only truncate freed a live log, and volume prune -a can delete a database.

I filled an ext4 disk of about 4 GiB five ways and ran every cleanup against each fill, on Docker 29.9.3 and containerd 2.4.2, in an Ubuntu 24.04 systemd container under WSL2. That isn't a real VPS, so I didn't test a provider's image or a real boot path.

How Do You Find What Filled the Disk?

Start with commands that change nothing, because these kept answering on my full disk while pulls, docker run, and docker exec failed:

df -h /var/lib/docker
df -i /var/lib/docker
docker system df
sudo du -xh --max-depth=1 /var/lib/docker /var/lib/containerd | sort -h | tail -6

This is the log fill, trimmed (/dev/loop6 is my test disk):

/dev/loop6      3.9G  3.9G     0 100% /var/lib/docker
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          7         3         1.267GB   469.9MB (37%)
Containers      4         3         33.6MB    33.56MB (99%)
Local Volumes   4         2         165.8MB   67.11MB (40%)
Build Cache     6         0         180.8MB   100.7MB
1.4G	/var/lib/containerd
2.3G	/var/lib/docker/containers

The disk is full, but docker system df accounts for about 1.6 GB, and du finds the rest in /var/lib/docker/containers. docker system df doesn't count logs, and its API returned byte-identical numbers with a 1.73 GB log and with none.

This loop names the container, and mine printed 2337MiB /chatty:

for id in $(docker ps -aq); do printf "%s %s\n" "$(sudo du -m "$(docker inspect --format "{{.LogPath}}" $id)" | cut -f1)MiB" "$(docker inspect --format "{{.Name}}" $id)"; done | sort -n

Which Command Identifies Each Cause?

I repeated those checks for each fill:

Causedocker system dfduWhat names it
Unbounded json-file logContainers 33.6 MB/var/lib/docker/containers 2.3Gdu only. docker system df misses it
Old images after redeploysImages 3.684 GB, reclaimable 2.887 GB (78%)/var/lib/containerd 3.6G, /var/lib/docker 160Mdocker system df -v
BuildKit cacheBuild Cache 2.446 GB, reclaimable 1.359 GB/var/lib/containerd 3.5G, /var/lib/docker/buildkit 536Kdocker system df
Postgres named volumeLocal Volumes 2.384 GB, reclaimable 67.11 MB (2%)/var/lib/docker/volumes 2.3Gdocker system df -v
Tiny files, inodes goneContainers 44.97 MBdu --inodes: one snapshot holds 248,378 of 259,613df -i, then du --inodes

df -h read 100 percent and df -i 6 on the first four fills, and the inode fill read 44 and 100.

On a fresh Docker 29 apt install, the containerd image store is the default (read November 7, 2026), so images, layers, and build cache land in /var/lib/containerd, and only logs and volumes stay in /var/lib/docker. An upgraded host keeps overlay2, which I ran only at small scale.

What Is Safe to Delete for Each Cause?

Every prune here prompts, so leave -f off, but the prompt names categories, not objects.

This table is the containerd image store on Docker 29.9.3, not overlay2, in decimal MB freed, measured with -f, one run per cell. The fixtures are small, so read it for which cleanup reaches the fill.

CommandLog fillRedeploysBuildKitInode fill (inodes freed)
image prune13413413417
image prune -a4362,85443756
builder prunefailed1011,76217
builder prune -afailedfailed1,906540
container prune34343431
system prune2692691,93065
system prune -a6473,0652,510628
system prune --volumes3363361,99771
system prune -a --volumes7143,1322,577634
volume prune3434343
volume prune -a6767676
truncate -s 0 on logs2,450000

The Postgres volume fill matched the log fill, except builder prune (101) and truncate (0).

Plain image prune removes untagged images only, and builder prune removes dangling build cache, or all of it with -a. container prune removes stopped containers and any log they held. system prune adds unused networks, and its -a form adds the rollback tag and all cache.

The --volumes and volume prune -a rows can delete a database, as the Postgres section shows.

That table holds only while the daemon lists every container. If Docker restarted or the host rebooted while full, compare docker ps -a with what should be running before any prune, because a short or empty list means a prune can delete those containers or their named volume.

A cleanup can work while df -h stays at 100 percent, because ext4 holds back blocks for root: image prune freed 134 MB and Avail didn't move.

A Container Log

truncate is the only cleanup that reached the log, freeing all 2,450 MB at once. Replace chatty with your container, and note that this destroys that log's history:

sudo truncate -s 0 "$(docker inspect --format "{{.LogPath}}" chatty)"

It breaks readers: docker logs -f stayed attached with no output and no error, and docker logs --tail 1 printed nothing until the container restarted.

Docker's json-file docs (read November 7, 2026) say to avoid outside access to these files, so treat this as an emergency move, then set limits and recreate the container.

Don't rm the file. That freed 0 bytes, because dockerd keeps the deleted file open, and a live writer took another 431 MB in 20 seconds where du could no longer see it. If you already did, docker restart chatty released the space.

Old Images After Redeploys

Plain docker image prune keeps your previous tag. docker image prune -a freed 2,854 MB by taking every image without a container, including the one you would roll back to. With my registry stopped, the rollback then failed with No such image.

Deleting old tags by name kept the rollback working, and this keeps the newest three of the repository you name:

docker image ls registry.lab:5000/shop --format '{{.CreatedAt}} {{.Repository}}:{{.Tag}}' | sort -r | tail -n +4 | awk '{print $NF}' | xargs -r docker image rm

It sorts on build time, so a moving tag such as edge counts as one. In a separate run where -a freed 2.719 GB, it freed 2.417 GB, and v19 still ran with the registry down.

Build Cache

The failed cells are docker builder prune hitting the true last block, where it can't write its own metadata and exits with ResourceExhausted. That hit 7 of 15 attempts on the other fills, and freeing a little first with docker container prune made the retry work 3 of 3 times.

A Postgres Volume

No prune reaches a live database, and docker system df already said only 2% was reclaimable. My Postgres disk full guide covers what the database does here and how to recover it.

Two prunes can delete a database, though. docker system prune --volumes removes anonymous volumes, and docker run postgres:18 with no -v creates one, so a stopped Postgres started that way lost its data. docker volume prune -a takes named volumes too, and unused means no container the daemon can list. docker container prune and system prune also removed an exited Postgres container, 6 of 6, while its named volume survived.

While db was listed, pgdata stayed. After systemctl restart docker at full, the daemon lost db in two of three rounds, and this command deleted the volume of a database that was running before the restart:

Deleted Volumes:
pgdata

Total reclaimed space: 48.2MB

With room back, db returned as Exited (1) with no volume, and the prompt gave no hint. I didn't run this after a reboot at full, but expect the same risk.

Free Space but Writes Fail (Inodes)

The inode count on ext4 is fixed at format time, so tiny files can use them all while df -h reads 44 percent. The best prune freed 634 of about 281,000, so find the directory instead, as root:

du --inodes -x --max-depth=1 /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots | sort -n | tail -4
for c in $(docker ps -q); do echo "$(docker inspect --format "{{.Name}}" $c) $(grep -o "upperdir=[^,]*" /proc/$(docker inspect --format "{{.State.Pid}}" $c)/mountinfo | head -1)"; done

The loop maps a snapshot to a container, and mine pointed at sessions, full of files in /sessions. I didn't run this hunt on overlay2.

The next commands delete data, so confirm the directory is disposable first. docker exec failed with no inodes left, so prune first, one command at a time, answering y:

docker image prune
docker exec sessions sh -c "find /sessions -type f -delete"

The prune freed enough to run the find when an untagged image existed. With none it freed 0B and docker exec still failed. docker rm -f sessions worked with no inodes free and removes the whole writable layer.

Why Not Restart Docker or Reboot on a Full Disk?

The daemon writes a file for each container as it registers them, and on a full disk that write fails with failed to register container.

systemctl restart docker returned 0 in all 11 rounds, and docker ps -a was then empty in 6 of them, with containers missing or stopped in the rest. Restarting the systemd container as a stand-in for a reboot listed nothing in 8 of 8 boots from one snapshot, and other runs kept some. docker system prune in that window deleted containers that looked stopped, including two with unless-stopped.

Docker can also refuse to start, as it did in 10 of 11 boots with inodes full.

Key login kept working in my rig, even with /etc and /usr on the full disk, but I could not test a real VPS boot, so keep your provider's console within reach.

How Do You Bring Docker Back After a Restart at Full?

Free room first. The truncate above worked by name with the list empty, but with Docker down it fails with cannot open '', so pick the file by size and run sudo truncate -s 0 on its path:

sudo find /var/lib/docker/containers -name "*-json.log" -printf "%s %p\n" | sort -n | tail -3

With inodes gone and Docker down, the du --inodes line still names the snapshot, and deleting disposable files under its fs directory worked 3 of 3 times, as root:

find /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/<id>/fs/sessions -type f -delete

This edits containerd's storage directly, and I ran it only on a lab rig against a container's writable layer, so ls that directory first.

Stop a fast writer before you restart anything, because mine refilled the disk within seconds. Running docker stop and docker update --restart=no by name worked with the list empty, and the disk stayed clear. With Docker down neither runs, and mine refilled the disk after the restart, 3 of 3.

If docker.service is active and only the list is short, systemctl restart docker as root brought the containers back in every completed round.

If Docker won't start, journalctl -u docker --no-pager -n 25 shows Unable to get the TempDir under /var/lib/docker, and once room is back, unknown service containerd.services.leases.v1.Leases. systemctl restart docker alone kept failing, 3 of 3, because containerd reports active with plugins that failed to load at boot. Restarting containerd first worked in 6 of 6, as root:

systemctl restart containerd
systemctl reset-failed docker.service docker.socket
systemctl restart docker

Then compare docker ps -a with what should be running before any prune. I didn't test this on a real server.

How Do You Stop the Disk Filling Again?

Limit logs, since json-file has no max-size by default. Merge this into /etc/docker/daemon.json rather than overwriting the file:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

The values must be strings, because a numeric max-file stops the daemon. Validate as root with dockerd --validate --config-file /etc/docker/daemon.json, then restart in a maintenance window with room on the disk, because the restart stopped containers with no restart policy.

The limits reach only containers created afterwards. Mine kept "Config":{} through a daemon restart, docker restart, and compose up -d, so recreate them with docker compose up -d --force-recreate. My VPS deployment checklist shows the per-service Compose form.

Add disk and the log loop to a regular check alongside backups, updates, and access.

A four-step maintenance routine: confirm backups are recent and restorable, check disk, CPU, and memory, review and apply updates, then verify the app and your access still work.

Summary

docker system df left out a 2.45 GB log, and system prune freed 269 MB of unrelated objects next to it, so run the read-only checks before you delete anything.

Leave Docker running until there's room. A restart or reboot at full can empty docker ps -a, and system prune then deleted containers, while volume prune -a after a daemon restart deleted a named database volume. If that already happened, free room, restart containerd and then Docker, and check the list before you prune.

My self-hosting course builds a monthly maintenance routine that covers server resources along with backups and updates.

Thanks for reading.

And stay awesome!


Frequently Asked Questions

What is safe to delete when Docker says no space left on device?

It depends on what filled the disk. Plain image and builder prunes remove only untagged images and dangling build cache, but while Docker lists the container, no prune touches its running log or live database. Find the cause with df, docker system df, and du before you delete anything.

Why does docker system df not show my container logs?

It does not count them. The disk usage API has no log field, and its numbers stayed byte-identical with a 1.73 GB log and with none. Use du on /var/lib/docker/containers, or on the log path of each container.

Why does Docker report no space left when df shows free space?

The disk can run out of inodes, the entries that track files, before it runs out of bytes. In testing, df -h showed 44 percent used while touch failed and df -i showed 100 percent. The largest prune freed only 634 inodes.

Should I reboot or restart Docker when the disk is full?

Not before freeing room. In a lab, both left docker ps -a short or empty, because the daemon could not write the files it needs to register containers. A system prune then deleted containers, and after a daemon restart a volume prune with the all flag deleted a named database volume. This ran in containers, not on a real VPS boot.

Can I delete a container json log file?

Do not delete it. Removing a live log freed 0 bytes, because Docker keeps the file open and keeps writing to it. Truncating it to zero freed the space at once, but docker logs with follow or tail then returned nothing until the container restarted, and Docker advises against touching these files.

Does docker image prune remove my rollback image?

Plain image prune does not, because it removes untagged images only. With the all flag it removes every image without a container, including the previous tag, and with the registry unreachable the rollback then failed. Delete old tags by name to keep the last few.

Browse all Self-Hosting guides →
Loading comments...

Whenever you're ready, there are 5 ways I can help you:

  1. Pragmatic Self-Hosting: Learn to deploy and run your apps on a VPS with Dokploy and Tailscale. Keep administration private, automate releases, and practice restoring backups, with source code and a runbook you can reuse.
  2. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  5. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

One practical .NET tip, every Saturday.

Join 66,000+ engineers who read it. Free, and you can unsubscribe any time.