no space left on device - 12 Causes and Fixes for Docker, Linux and macOS
Docker just died with no space left on device, and df -h on your laptop probably still shows tens of gigabytes free. That mismatch is the problem: the message is the kernel's ENOSPC errno stringified by whatever tool happened to be writing, and it can mean out of data blocks, out of inodes, or out of room inside Docker's own virtual disk — three unrelated fixes.
The tool printing the error almost never tells you which filesystem ran out. docker build reports it from inside a VM, Postgres reports it from /dev/shm, a CI runner reports it from a 14 GB ephemeral volume.
This page gives you the one-command fix first, then 12 distinct causes with a reproduction and a fix for each, then a checklist for the case where none of them match.
| Error | no space left on device |
|---|---|
| Where it happens | Any Linux filesystem write (ENOSPC, errno 28) - surfaced by Docker Engine / BuildKit, Docker Desktop on macOS and Windows, Node.js (ENOSPC), Postgres, apt/yum, Kubernetes kubelet, CI runners |
| What it means | A write(2) call could not allocate storage: either the filesystem is out of blocks, out of inodes, or the write is happening inside a container, tmpfs, or VM disk that is smaller than you think. |
The Fast Fix
Find out which bucket is full, then reclaim it. For a Docker machine this clears it ~80% of the time:
docker system df # which bucket: images / containers / volumes / build cache
docker system prune -a --volumes # unused images, containers, networks, build cache AND volumes
docker builder prune -a # BuildKit cache, if the line above left any
--volumes deletes data in any volume not currently attached to a container — drop it if you have databases in volumes you care about.
On macOS or Windows with Docker Desktop, if the host has space but Docker still fails, you hit the VM's disk cap, not your SSD: raise Settings -> Resources -> Advanced -> Virtual disk limit, then Apply & Restart.
What Is Actually Causing It
Jump to your case
- 1. Dangling images and stopped containers filled Docker's storage
- 2. BuildKit build cache grew unbounded
- 3. Docker Desktop VM hit its virtual disk limit (Mac/Windows)
- 4. The macOS host disk is full, so Docker's sparse disk image cannot grow
- 5. Unused named and anonymous volumes hold most of the data
- 6. Container logs with no rotation ate the disk
- 7. Inodes are exhausted while blocks are free
- 8. Deleted files are still held open by a running process
- 9. /dev/shm inside the container is the default 64 MB
- 10. Writing to a size-limited tmpfs or read-only-ish mount in the container
- 11. /var/lib/docker sits on a small root partition while a big disk idles
- 12. CI runner or Kubernetes node ran out of ephemeral storage
1. Dangling images and stopped containers filled Docker's storage
Reproduce it
$ docker build -t app .
...
failed to solve: write /var/lib/docker/tmp/buildkit-mount123/layer.tar: no space left on device
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 214 6 98.4GB 87.1GB (88%)
Containers 61 3 4.2GB 4.1GB (97%)
Why it happens — Every rebuild of the same tag leaves the previous image untagged but still on disk, and every stopped container keeps its writable layer. Docker garbage-collects none of this on its own, so a machine that builds daily grows without bound.
The fix
docker container prune -f
docker image prune -a -f # -a also removes unused *tagged* images, not just dangling ones
Changed: image prune without -a only drops untagged layers; -a is what removes the old tagged images nothing is running.
Confirm it worked — Re-run docker system df — RECLAIMABLE for Images and Containers should be near 0B — then re-run the build that failed.
2. BuildKit build cache grew unbounded
Reproduce it
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 12 5 8.1GB 2.0GB (24%)
Build Cache 1043 0 61.7GB 61.7GB
$ docker buildx du | tail -1
Total: 61.7GB
Why it happens — BuildKit keeps every intermediate layer and every RUN --mount=type=cache directory so later builds are fast. docker image prune does not touch it — it is a separate store, and it is frequently the single largest consumer on a build host.
The fix
docker builder prune -a -f
# or keep the last week only, instead of nuking everything
docker buildx prune --all --force --filter until=168h
Then cap it permanently in /etc/docker/daemon.json so it never gets here again:
{
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "20GB"
}
}
}
Confirm it worked — docker buildx du | tail -1 shows a Total under your cap, and docker system df reports Build Cache close to 0B right after the prune.
3. Docker Desktop VM hit its virtual disk limit (Mac/Windows)
Reproduce it
# host, in macOS Terminal - plenty of room
$ df -h /
Filesystem Size Used Avail Capacity Mounted on
/dev/disk3s1s1 926Gi 410Gi 480Gi 47% /
# inside the Docker VM - completely full
$ docker run --rm alpine df -h /
Filesystem Size Used Avail Use% Mounted on
overlay 59.0G 59.0G 0 100% /
Why it happens — On macOS and Windows, Docker runs in a Linux VM with a fixed-size virtual disk. Your Mac's free space is irrelevant once that disk is full — this is the single most common form of docker no space left on device mac.
The fix
Reclaim inside the VM first, then raise the ceiling:
docker system prune -a --volumes
docker builder prune -a
If it is still tight: Settings -> Resources -> Advanced -> Virtual disk limit, drag it up, Apply & Restart. The guest filesystem is grown on restart.
Confirm it worked — docker run --rm alpine df -h / shows the new, larger Size and a non-zero Avail.
4. The macOS host disk is full, so Docker's sparse disk image cannot grow
Reproduce it
$ df -h /
/dev/disk3s1s1 460Gi 459Gi 380Mi 100% /
$ docker pull postgres:16
...
failed to register layer: write /var/lib/docker/overlay2/.../layer.tar: no space left on device
Why it happens — Docker Desktop's disk image is a sparse file: it only occupies what the VM has actually written, and it grows on demand. When the Mac itself has no blocks left, that growth fails and the guest reports ENOSPC — the opposite direction from the previous cause.
The fix
# see what Docker Desktop is holding on the host side
du -sh ~/Library/Containers/com.docker.docker/Data
# shrink it by deleting data inside Docker, not with rm
docker system prune -a --volumes
Pruning inside the VM is what makes the image shrinkable; if the host file stays huge afterwards, Settings -> Troubleshoot -> Reset disk image recreates it at minimum size (this destroys all images, containers and volumes).
Confirm it worked — df -h / on the host shows gigabytes in Avail, and du -sh ~/Library/Containers/com.docker.docker/Data is smaller than before.
5. Unused named and anonymous volumes hold most of the data
Reproduce it
$ docker system prune -a -f
Total reclaimed space: 1.2GB
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Local Volumes 87 2 143.2GB 141.8GB (99%)
Why it happens — docker system prune -a skips volumes unless you pass --volumes — deliberately, because volumes are where persistent data lives. Meanwhile docker compose down leaves named volumes behind, and every docker run of an image with a VOLUME instruction (Postgres, MySQL, Elasticsearch) creates a fresh anonymous volume that nothing ever deletes.
The fix
docker volume ls -qf dangling=true # inspect the list BEFORE deleting
docker volume prune -f # removes volumes not attached to any container
Changed: target volumes explicitly instead of relying on system prune, and read the dangling=true list first so you do not throw away a database you still need.
Confirm it worked — docker system df shows Local Volumes size dropping by roughly the RECLAIMABLE figure you started with.
6. Container logs with no rotation ate the disk
Reproduce it
$ sudo du -sh /var/lib/docker/containers/* | sort -h | tail -2
2.1G /var/lib/docker/containers/41ab...
48G /var/lib/docker/containers/9f2c...
$ sudo ls -lh /var/lib/docker/containers/9f2c*/9f2c*-json.log
-rw-r----- 1 root root 48G Oct 12 01:04 9f2c...-json.log
Why it happens — The default json-file log driver has no size limit. One chatty container in a crash loop writes stack traces forever, and the log file sits on the same filesystem as every image layer.
The fix
Truncate now, then cap it in /etc/docker/daemon.json:
sudo truncate -s 0 /var/lib/docker/containers/*/*-json.log
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
Then sudo systemctl restart docker. The cap applies to newly created containers, so recreate the long-running ones (docker compose up -d --force-recreate).
Confirm it worked — docker inspect -f '{{.HostConfig.LogConfig}}' <container> shows max-size:10m, and sudo du -sh /var/lib/docker/containers stays flat over a day.
More causes (6 remaining)
7. Inodes are exhausted while blocks are free
Reproduce it
$ df -h /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 100G 62G 38G 63% /
$ touch /var/lib/docker/probe
touch: cannot touch '/var/lib/docker/probe': No space left on device
$ df -i /var/lib/docker
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 6553600 6553600 0 100% /
Why it happens — An ext4 filesystem has a fixed inode count set at format time. The overlay2 driver creates an enormous number of tiny files, so an image-heavy host can run out of inodes with a third of its bytes unused — and the kernel returns the same ENOSPC either way.
The fix
# free inodes by deleting file *count*, not just bytes
docker system prune -a -f
docker builder prune -a -f
# find the directory holding the files
sudo find /var/lib/docker -xdev -printf '%h\n' | sort | uniq -c | sort -n | tail -5
If it recurs, reformat the volume with more inodes (mkfs.ext4 -i 8192 /dev/sdX) or move Docker to XFS, which allocates inodes dynamically.
Confirm it worked — df -i /var/lib/docker shows IUse% well below 100, and touch /var/lib/docker/probe now succeeds (then rm it).
8. Deleted files are still held open by a running process
Reproduce it
$ df -h /
/dev/sda1 50G 50G 0 100% /
$ sudo du -sh --exclude=/proc --exclude=/sys -x / 2>/dev/null
19G /
$ sudo lsof +L1 | head -3
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
dockerd 1184 root 7w REG 8,1 31.2G 0 918 /var/log/docker.log (deleted)
Why it happens — df counts blocks the filesystem considers allocated; du counts blocks reachable through a directory entry. Unlinking a file does not free its blocks until the last file descriptor closes, so a 31 GB log you already rm-ed is still charged to the disk.
The fix
# option 1: let the holder reopen its file
sudo systemctl restart docker
# option 2: reclaim without restarting - truncate through the open fd
sudo sh -c ': > /proc/1184/fd/7'
Changed: you act on the process holding the descriptor, not on the filesystem — rm again does nothing here.
Confirm it worked — df -h / and du -sh -x / now agree within a few hundred MB, and sudo lsof +L1 returns no multi-gigabyte entries.
9. /dev/shm inside the container is the default 64 MB
Reproduce it
$ docker run --rm postgres:16 df -h /dev/shm
Filesystem Size Used Avail Use% Mounted on
shm 64M 0 64M 0% /dev/shm
ERROR: could not resize shared memory segment "/PostgreSQL.1803366862" to 4194304 bytes: No space left on device
Why it happens — Docker mounts /dev/shm as a 64 MB tmpfs by default. Postgres parallel queries, headless Chrome and many ML libraries use POSIX shared memory and blow past that instantly — the host disk is irrelevant.
The fix
docker run --rm --shm-size=1g postgres:16 df -h /dev/shm
services:
db:
image: postgres:16
shm_size: 1gb
Changed: one flag (--shm-size) / one key (shm_size). Nothing inside the image needs editing.
Confirm it worked — docker exec <container> df -h /dev/shm reports the new Size, and the query or browser launch that failed now completes.
10. Writing to a size-limited tmpfs or read-only-ish mount in the container
Reproduce it
$ docker run --rm --tmpfs /tmp:rw,size=16m alpine \
sh -c 'dd if=/dev/zero of=/tmp/big bs=1M count=64'
dd: writing '/tmp/big': No space left on device
17+0 records in
16+0 records out
Why it happens — A tmpfs mount has its own hard size limit independent of the host, and it lives in RAM. Anything that spools to /tmp — image processing, tar, pip, a database sort — hits the mount's ceiling, not the disk's.
The fix
# raise the limit
docker run --rm --tmpfs /tmp:rw,size=1g alpine \
sh -c 'dd if=/dev/zero of=/tmp/big bs=1M count=64'
# or spool to a real volume instead of RAM
docker run --rm -v scratch:/tmp alpine \
sh -c 'dd if=/dev/zero of=/tmp/big bs=1M count=64'
Changed: size=16m -> size=1g, or the tmpfs is replaced by a named volume so large temp files land on disk.
Confirm it worked — docker exec <container> df -h /tmp shows the expected Size, and the same dd command that failed now writes 64 records out.
11. /var/lib/docker sits on a small root partition while a big disk idles
Reproduce it
$ df -h /var/lib/docker /data
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 20G 20G 0 100% /
/dev/sdb1 2.0T 12G 1.9T 1% /data
$ docker info | grep -i 'docker root dir'
Docker Root Dir: /var/lib/docker
Why it happens — Cloud images ship a small root volume, and Docker's default data-root is /var/lib/docker on exactly that volume. Pruning buys you days; the layout is the bug.
The fix
sudo systemctl stop docker
sudo rsync -aHAX /var/lib/docker/ /data/docker/
{
"data-root": "/data/docker"
}
sudo systemctl start docker
Changed: data-root in /etc/docker/daemon.json points at the large volume. Use rsync -aHAX (not cp) so hard links and xattrs that overlay2 depends on survive the move.
Confirm it worked — docker info | grep -i 'docker root dir' prints /data/docker, docker images still lists everything, and df -h / is back under 50%.
12. CI runner or Kubernetes node ran out of ephemeral storage
Reproduce it
# GitHub Actions
#12 ERROR: failed to copy files: write /var/lib/docker/.../blob: no space left on device
# Kubernetes
$ kubectl describe pod build-7f9c4
Events:
Warning Evicted The node was low on resource: ephemeral-storage.
Container build was using 14518296Ki, which exceeds its request of 0.
Why it happens — A hosted runner gives you a fixed, small disk that already holds a preinstalled toolchain, and a Kubernetes node shares one filesystem between image layers, container logs and every emptyDir. A multi-stage build with no cleanup step exhausts it mid-job.
The fix
- name: Reclaim disk before build
run: |
docker system prune -af --volumes
df -h /
On Kubernetes, declare the budget so the scheduler and kubelet can enforce it:
resources:
requests:
ephemeral-storage: 4Gi
limits:
ephemeral-storage: 16Gi
volumes:
- name: scratch
emptyDir:
sizeLimit: 20Gi
Changed: the job reclaims space before the heavy step, and the pod stops being an unbounded consumer the kubelet has to evict blindly.
Confirm it worked — The df -h / step prints growing Avail before the build, and kubectl describe node <node> | grep DiskPressure reports False.
## None of Those? Narrow It Down
- Run
df -h <path>anddf -i <path>on the exact path that failed. Bytes at 100% points at causes 1-6 and 11; inodes at 100% with bytes free means cause 7 and nothing else. - On macOS or Windows, compare host and guest:
df -h /in your terminal versusdocker run --rm alpine df -h /. Guest full + host free = VM disk limit (cause 3). Host full + guest free = sparse image cannot grow (cause 4). - Run
docker system df -v. The single largestRECLAIMABLEbucket names your culprit: Images, Containers, Local Volumes, or Build Cache. If every bucket is small, Docker is not what filled the disk — skip to the next step. - Walk the storage dir:
sudo du -xh --max-depth=1 /var/lib/docker | sort -h.overlay2dominating means layers and cache;containersdominating means unrotated logs (cause 6);volumesdominating means cause 5. - Compare
df -h /againstsudo du -sh -x /. A gap larger than a few hundred MB means blocks are held by deleted-but-open files — confirm withsudo lsof +L1(cause 8). - Check whether it fails everywhere or at one path only:
docker exec <container> sh -c 'df -h / /tmp /dev/shm'. A single mount at 100% while/has room isolates it to tmpfs or shared memory (causes 9-10). - Probe with a clean container:
docker run --rm alpine sh -c 'dd if=/dev/zero of=/probe bs=1M count=50'. Success means the daemon's storage is fine and the problem is specific to your container's mounts or a host path you bind-mounted. - If the host is a CI runner or Kubernetes node, read the orchestrator's view:
kubectl describe node <node> | grep -i pressure, or printdf -h /as the first step of the job. A quota or eviction threshold, not a full disk, may be what you are hitting (cause 12).
Why This Error Exists At All
no space left on device is the text of a single POSIX errno: ENOSPC, error 28. The kernel returns it from write(2), mkdir(2), link(2) and friends whenever the filesystem driver cannot allocate what the call needs. There is exactly one code for "allocation failed" and no second code to say which resource ran out — blocks, inodes, directory entries, or a quota. Every layer above the syscall just calls strerror(errno) and prints the string, which is why Docker, Node.js, Postgres and apt all emit the identical sentence for genuinely different conditions. The message is accurate about what happened and silent about where, and that silence is the whole reason this error is hard.
Two design decisions turn ordinary ENOSPC into a Docker-specific hazard. First, copy-on-write layering: every image, every container's writable layer, every build cache mount and every json-file log lives in one pool under data-root. Pulling an image, running a container, and a crash-looping service writing stack traces all consume the same finite resource, so the thing that fills the disk is routinely not the thing that reports the failure. Second, the filesystem's accounting is layered too. A Docker Desktop write passes through a container's overlay mount, the guest's ext4, a sparse disk image, and finally APFS or NTFS on your actual drive. Any one of those four can be the layer that says no, and all four produce the same seven words.
One more mechanism catches people repeatedly: deallocation is deferred. POSIX frees a file's blocks only when the last link and the last open descriptor are gone, so rm on a file a process still holds reclaims nothing — which is exactly why df and du disagree on a disk you "already cleaned." Once you internalise that ENOSPC means "some allocator, somewhere in the stack, said no," you stop guessing and start asking the three questions that actually partition the space: blocks or inodes, host or guest, and whole filesystem or one mount.
Stop It From Coming Back
- Cap container logs globally in
/etc/docker/daemon.jsonwith"log-opts": {"max-size": "10m", "max-file": "3"}, then recreate existing containers so the limit applies to them too. - Enable BuildKit garbage collection via
"builder": {"gc": {"enabled": true, "defaultKeepStorage": "20GB"}}indaemon.jsonso build cache can never become the biggest thing on the box. - Schedule reclamation instead of doing it in a panic: a systemd timer or cron entry running
docker system prune -af --filter until=168hweekly keeps a week of cache and drops the rest (leave--volumesout of anything automated). - Alert on all three numbers, not just one:
df -h,df -i, anddocker system df. Node Exporter'snode_filesystem_avail_bytesplusnode_filesystem_files_freewill catch the inode case that a bytes-only dashboard misses entirely. - Shrink what you build: multi-stage Dockerfiles that copy only artefacts into the final stage, plus a real
.dockerignoreexcluding.git,node_modulesand local build output, so each image is gigabytes smaller to begin with. - Put
data-rooton a dedicated volume when you provision a build or Docker host, so Docker filling up degrades Docker instead of taking/var/log,sshdand the package manager down with it.
Related Guides
Errors You Will Probably Hit Next
failed to register layer: Error processing tar file(exit status 1): write /usr/lib/...: no space left on deviceENOSPC: no space left on device, write (Node.js fs / webpack / esbuild)could not resize shared memory segment "/PostgreSQL.123456" to 4194304 bytes: No space left on device
Three questions partition every occurrence of this error: blocks or inodes (df -h versus df -i), host or guest (your terminal's df versus docker run --rm alpine df -h /), and whole filesystem or one mount (df -h / /tmp /dev/shm inside the container). Answer them in that order before you delete anything, because docker system prune fixes exactly one branch of that tree and wastes your time on the other five. And when df insists the disk is full while du says it is half empty, stop looking at the filesystem and go find the process holding a deleted file open.
댓글
댓글 쓰기