Docker "No Space Left on Device": Find Which Disk Is Full First

Sanchez Kim
Sanchez Kim
AI Engineer · · 16 min read

I reproduced the mount, inode and build cases on Docker Desktop and Engine 29, measured where the VM and Mac disks fill up, and found a 966 MB log that docker system df never counted.

#docker#docker-desktop#disk-space#troubleshooting#buildkit#logging
Docker "No Space Left on Device": Find Which Disk Is Full First

No space left on device is the same message for at least four different problems. A small mount inside a container can be full. A filesystem can run out of inodes while it still has free bytes. Docker's own disk can be full, which on a Mac means the Linux VM's disk. Or the disk under that can be full. The cleanup command that fixes one of these does nothing for the others, and the most aggressive one deletes data you may want.

So I reproduced the container-mount, inode and build cases on my own machine, and measured where the VM and Mac disks fill up, before rewriting this guide. I did not fill the VM disk or the Mac's SSD. Everything in a terminal block below is real output from an Apple M5 MacBook Pro running macOS 26.6.2, Docker Desktop 4.93.0, Docker Engine 29.8.1 and BuildKit v0.33.0. The Linux-host and settings sections are from Docker's documentation, and I've marked them that way.

Which disk is full?

What you see Which disk is full First thing to try
Error inside a running container, but df -h / in that container shows plenty of space A mount inside the container (tmpfs, a volume, a bind mount) df -h on the path the app writes to, then make that mount bigger (tmpfs size counts against the container's memory)
df -h shows free space, df -i shows IUse% at 100% Inodes, not bytes Delete many small files. Freeing big files won't help
Error during docker build in a RUN step The build's scratch space or the engine's disk Read which path the failing command wrote to, then docker system df
docker pull, build or run fail everywhere, and docker system df shows large RECLAIMABLE numbers The engine's disk (/var/lib/docker and /var/lib/containerd on Linux, the VM disk on Docker Desktop) The cleanup ladder below, starting at the top
docker system df looks small but the disk is still full Container logs, which docker system df does not count Check log sizes, then cap them with max-size
Docker Desktop on a Mac, and the Mac itself is low on space Your Mac's SSD, where the VM disk image lives df -h on the Mac. Docker cleanup frees VM space first, and the Mac may not get it back right away

Diagram of three nested layers on a Mac: a mount inside a container, the Docker Desktop Linux VM disk holding images, build cache, volumes and logs, and the Mac SSD holding the Docker.raw disk image file. Each layer is labelled with the command that shows its usage: df -h /path, docker run --rm alpine df -h /, and df -h on macOS

A full mount inside the container

The simplest version is a container writing more than its mount can hold. I gave a container a 20 MB tmpfs at /data and wrote 50 MB into it:

$ docker run --rm --tmpfs /data:size=20m alpine sh -c 'df -h /data; dd if=/dev/zero of=/data/f bs=1M count=50; echo dd_exit=$?; df -h /data /'
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    20.0M         0     20.0M   0% /data
dd: error writing '/data/f': No space left on device
21+0 records in
20+0 records out
20971520 bytes (20.0MB) copied, 0.005162 seconds, 3.8GB/s
dd_exit=1
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    20.0M     20.0M         0 100% /data
overlay                 910.7G     19.4G    844.9G   2% /

Look at the last line. The container's root filesystem had 844.9 GB free at the moment the write failed. If you only run df -h /, this case looks impossible. Run df -h on the directory the application writes to. That might be a database's data directory, /tmp, or an upload folder.

The same thing happens with a named volume that has a size limit. I created one with the local driver backed by tmpfs:

docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=20m smallvol

I got the same dd: error writing '/var/lib/data/big': No space left on device at 20 MB. One thing I noticed: when I mounted that volume in a second container, it was empty again. A tmpfs-backed volume lives in memory, so it doesn't keep data after the last container using it stops. It's useful for reproducing this error, and you shouldn't put a real database on one.

Out of inodes, with free bytes left

Every file uses one inode, and some filesystems have a fixed number of them. Here, the tmpfs had 20 MB of space and 200 inodes:

$ docker run --rm --tmpfs /data:size=20m,nr_inodes=200 alpine sh -c 'i=0; while touch /data/f$i 2>/tmp/err; do i=$((i+1)); done; echo created=$i; cat /tmp/err; df -h /data; df -i /data'
created=199
touch: /data/f199: No space left on device
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    20.0M         0     20.0M   0% /data
Filesystem              Inodes      Used Available Use% Mounted on
tmpfs                      200       200         0 100% /data

The error text is identical, and the disk is 0% used. df -i is what tells this case apart. Removing one large file frees one inode. To fix it you need to remove a lot of small files: caches, session files, or old image layers on the engine's disk.

For comparison, the Docker Desktop VM's disk on my machine reported 60,710,912 inodes with 1% in use, so I was nowhere near this limit in practice.

During docker build

A RUN step can fail the same way. This Dockerfile gives one step a 16 MB scratch mount and writes 64 MB into it:

FROM alpine
RUN --mount=type=tmpfs,target=/scratch,size=16m \
    df -h /scratch && \
    dd if=/dev/zero of=/scratch/blob bs=1M count=64
#7 …
#7 0.057 tmpfs                    16.0M         0     16.0M   0% /scratch
#7 0.060 dd: error writing '/scratch/blob': No space left on device
#7 …
#7 ERROR: process "/bin/sh -c df -h /scratch &&     dd if=/dev/zero of=/scratch/blob bs=1M count=64" did not complete successfully: exit code: 1
…
ERROR: failed to build: failed to solve: process "/bin/sh -c df -h /scratch && ..." did not complete successfully: exit code: 1

In a real build the scratch space is usually the engine's own disk, not a small tmpfs. Either way, the error names the path that failed, here /scratch/blob. Read it before you delete anything. A failed write under /tmp during npm install and a failed layer extraction during docker pull point at different places.

Finding out which disk is full

Start with docker system df. This is what my machine showed before I created any test objects:

$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          11        5         11.67GB   2.834GB (24%)
Containers      6         4         167.4MB   40.96kB (0%)
Local Volumes   15        5         2.423GB   1.073GB (44%)
Build Cache     85        0         6.133GB   2.54GB

RECLAIMABLE is what a prune of that type could free. For volumes, that column is data you may still want, so treat it differently from the others.

Then check the engine's disk directly. On Docker Desktop that disk is inside a Linux VM, so the Mac's df doesn't show it. A throwaway container does:

$ docker run --rm alpine df -h /
Filesystem                Size      Used Available Use% Mounted on
overlay                 910.7G     20.1G    844.3G   2% /

Then check the file on the Mac that holds that disk. It's a sparse file, so ls and du disagree:

$ ls -lh ~/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw
-rw-r--r--@ 1 you      staff   926G Oct  4 20:27 ~/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw
$ du -h ~/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw
 20G	~/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw
$ df -h /System/Volumes/Data
Filesystem      Size    Used   Avail Capacity iused ifree %iused  Mounted on
/dev/disk3s5   926Gi   615Gi   279Gi    69%    5.3M  2.9G    0%   /System/Volumes/Data

926 GB is the maximum the image may grow to. 20 GB is what it actually takes on the SSD. Docker's Mac FAQ warns that some tools report the maximum size instead of the real one. I never changed Docker Desktop's disk setting ("Disk usage limit" in Settings, called "Disk image size" in the Mac FAQ), and on this install the maximum equals the size of the Mac's whole SSD. With a limit that large, the Mac's own disk will fill before the VM's does, so df -h on the Mac belongs in the first round of checks.

Where the bytes actually are. On this install Docker uses the containerd image store, the default for new installs of Engine 29. Inside the VM, I measured the directories (read-only) with a privileged helper container:

$ du -xsh /var/lib/docker/* /var/lib/desktop-containerd | sort -h    # inside the VM, trimmed
1.9M	/var/lib/docker/buildkit
995M	/var/lib/docker/containers
2.5G	/var/lib/docker/volumes
16G	/var/lib/desktop-containerd

Images and their layers sit in the containerd directory, and volumes and container logs stay under /var/lib/docker. That matches the daemon docs. The containerd image store page adds that this store keeps images both compressed and uncompressed, so it uses more disk than the old drivers. My 60 MB test image showed that: docker image ls listed it at 134 MB of disk usage against 64.2 MB of content.

The logs that docker system df doesn't count

Look at that 995M /var/lib/docker/containers again (including about 28 MB of my test logs). docker system df put all containers at 167.4 MB. The difference is logs. With the default json-file driver, everything a container prints to stdout is appended to a file whose size defaults to unlimited.

I checked my own containers and found that 966 MB of the 995 MB came from one service that was stuck in a crash loop. It had restarted more than 4,000 times, appending to the same log file each time, with no size limit. No prune command would have found it.

To reproduce it on purpose, I ran a container that prints 200,000 lines:

$ docker inspect --format '{{.LogPath}}' lab-logger
/var/lib/docker/containers/eca917cb…/eca917cb…-json.log
$ ls -lh <that path>          # inside the VM
-rw-r----- 1 root root 27M Oct  4 11:27 eca917cb…-json.log
$ docker ps -a --filter name=lab-logger --format '{{.Names}} {{.Size}}'
lab-logger 4.1kB (virtual 9.33MB)

27 MB on disk, and the container's SIZE column reports 4.1 kB. The same container with a size cap:

$ docker run -d --log-opt max-size=1m --log-opt max-file=2 --name lab-logger-capped alpine sh -c '…same loop…'
$ ls -lh <its log directory>   # inside the VM
-rw-r----- 1 root root 441K Oct  4 11:27 5270eded…-json.log
-rw-r----- 1 root root 977K Oct  4 11:27 5270eded…-json.log.1

It wrote the same 200,000 lines and stayed under 1.5 MB on disk. To make that the default, set it in daemon.json (Docker Desktop: Settings → Docker Engine):

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

Changing daemon.json needs a daemon restart. On Docker Desktop that means restarting the Docker engine, which stops your running containers, so check that they are back up afterwards. According to the docs, the new default applies only to containers created after the change. Existing containers keep their old logging setup until you recreate them, which with Compose means docker compose up -d --force-recreate.

The cleanup ladder, with real numbers

Go from least destructive to most, and run docker system df after each step. I ran each step on test objects I'd made for this (labelled, and filtered with --filter label=… so nothing else on the machine was touched). For your own cleanup, drop the filter. The numbers are the space reclaimed, as reported by each prune command:

Step Command What it deletes What happened here
1 docker container prune Stopped containers and their writable layers 2 stopped containers, 100 MB reclaimed
2 docker image prune Dangling (untagged) images 0 B, see below
3 docker image prune -a Every image no container uses. You'll pull or build them again 2 images, 120 MB reclaimed, but see the note on build cache below
4 docker builder prune Build cache. -a also removes internal and frontend records 418 MB across 10 records
5 docker volume prune Unused anonymous volumes only (Engine 23.0 and later; older engines also removed unused named volumes) 0 B. My test volumes were named
6 docker volume prune -a Unused named volumes too. This is where databases get deleted See below

docker system prune runs steps 1, 2 and 4 together and also removes unused networks. Its reference says "Build cache is always eligible for pruning with this command". Adding -a includes step 3, and --volumes adds anonymous volumes.

Four things did not go the way older guides suggest:

Rebuilding over a tag left no dangling image. I built lab-big:latest twice. With the classic storage driver the first build would become <none>:<none> (older guides describe this; I did not test the classic overlay2 store). Here it disappeared from docker image ls completely, and step 2 found nothing. On the containerd image store, look at build cache instead.

Deleting images increased reclaimable build cache. After step 3, docker system df showed Images SIZE down about 240 MB (11.91 to 11.67 GB) while Build Cache RECLAIMABLE rose by about the same (2.714 to 2.954 GB). The layers were still held by the cache, so little real disk likely came back until step 4. I didn't measure that. If you prune images and the disk doesn't change, this is a likely reason.

docker builder prune has no label filter. It filters by id, description, type, until and a few others (reference). I removed my own records one by one with --filter id=…. Two of them, the Dockerfile frontend and a shared base layer, were only removed with -a. For a build server, docker builder prune --max-used-space 10gb is a better habit than wiping the cache: it deletes the least recently used records until the cache fits.

docker volume prune -a skipped one of my named volumes. It removed an ordinary named volume, and it left the tmpfs-backed one alone even though no container used it. I created a second volume with mount options to check, and it was skipped too. Moby's source explains it: prune "intentionally skips volumes with mount options", because deleting them reclaims nothing. The same goes for volumes that docker run -v name:/path creates automatically: they don't get the container's labels, so a label filter can't see them. I deleted the three by name with docker volume rm (lab-datavol, lab-smallvol and lab-tmpfsprobe).

Logs are the step that isn't on the ladder. Removing a container deletes its log file. A capped log won't grow back.

On a Linux host (from the docs, not reproduced here)

I didn't test this on a Linux server, so this section follows Docker's documentation. On Linux, the daemon stores data in /var/lib/docker. With the containerd image store, image contents and container snapshots go to /var/lib/containerd instead. To see which directory is growing:

sudo du -xsh /var/lib/docker/* /var/lib/containerd 2>/dev/null | sort -h
df -h /var/lib/docker /var/lib/containerd
df -i /var/lib/docker

If the partition is simply too small, the data-root option in /etc/docker/daemon.json moves Docker's data directory. Setting { "data-root": "/mnt/docker-data" } and restarting the daemon is the documented part. The surrounding steps are general practice, not from that page: stop Docker, copy the directory with rsync -aHAX so ownership and extended attributes are preserved, start Docker with the new setting, and check that your containers come back before deleting the old copy. The same page warns that data-root does not move /var/lib/containerd, so on the containerd store moving data-root alone may free much less than you expect. Moving the snapshotter data takes containerd's own configuration. The storage driver docs explain how image layers and writable layers are laid out.

Docker Desktop's disk limit (from the docs, not changed here)

Docker Desktop has a Disk usage limit under Settings → Resources → Advanced (the Mac FAQ calls the same control "Disk image size"; settings reference). If the VM disk itself is full, raising it is the fix. Be careful lowering it: the Mac FAQ says that "when you reduce the maximum size, the current disk image file is deleted, and therefore, all containers and images are lost." I didn't change mine.

What I'd do next time

  1. Read the path in the error message.
  2. df -h and df -i on that path, inside the container.
  3. docker system df, then docker run --rm alpine df -h /, then df -h on the host.
  4. If the numbers don't add up, look at /var/lib/docker/containers for logs.
  5. Climb the cleanup ladder and stop as soon as you have enough space. Before step 6, run docker volume ls and check what each volume holds.

Methodology

  • Machine: Apple M5 MacBook Pro, 32 GB, macOS 26.6.2. Docker Desktop 4.93.0, Engine 29.8.1 (API 1.56), buildx v0.37.1, BuildKit v0.33.0, containerd image store (overlayfs snapshotter), default json-file logging. Run on 2026-10-04.
  • Reproduced: ENOSPC on a size-limited tmpfs mount and on a tmpfs-backed named volume, inode exhaustion (nr_inodes=200), ENOSPC in a RUN --mount=type=tmpfs build step, json-file log growth with and without max-size, and the cleanup ladder on test objects (2 stopped containers with 50 MB each, three 60 MB image builds, one 30 MB volume, two log containers). Measured, not reproduced: where the VM disk and the Mac's SSD fill up (Docker.raw size, df inside the VM and on the Mac). I did not fill the VM disk or the Mac's SSD.
  • Safety: this is my working Docker install. Every test object had the automatone.lab=1 label or a lab- name. Prunes used that label filter, build cache was pruned by record ID, and unlabelled test volumes were removed by name. After cleanup, docker system df matched the starting numbers on every row, and my own containers' status was unchanged. Directory sizes inside the VM were read with du from a privileged helper container. Nothing was written there.
  • Output editing: commands are shown without the test label and name flags. Long container IDs and one build error line are shortened with …, some context lines in the build log were trimmed (marked with …), home-directory paths are written as ~, and my macOS username in one ls listing is replaced with you. Nothing else was changed.
  • From documentation only: the Linux host layout, moving data-root (the copy steps are general practice), and Docker Desktop's disk limit setting. Windows, WSL2 and the classic overlay2 image store were not tested.
  • Anonymised: names of my own containers, images and volumes are left out. The 966 MB log figure is from one of my own services.

References

Related Posts