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. Close-up of server equipment in a modern data center highlighting technology infrastructure.

Errorno space left on device
Where it happensAny 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 meansA 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

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.

A bearded man intensely focuses on his laptop screen with hands on chin, in a dimly lit room, representing late-night work. ## None of Those? Narrow It Down
  1. Run df -h <path> and df -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.
  2. On macOS or Windows, compare host and guest: df -h / in your terminal versus docker run --rm alpine df -h /. Guest full + host free = VM disk limit (cause 3). Host full + guest free = sparse image cannot grow (cause 4).
  3. Run docker system df -v. The single largest RECLAIMABLE bucket 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.
  4. Walk the storage dir: sudo du -xh --max-depth=1 /var/lib/docker | sort -h. overlay2 dominating means layers and cache; containers dominating means unrotated logs (cause 6); volumes dominating means cause 5.
  5. Compare df -h / against sudo du -sh -x /. A gap larger than a few hundred MB means blocks are held by deleted-but-open files — confirm with sudo lsof +L1 (cause 8).
  6. 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).
  7. 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.
  8. If the host is a CI runner or Kubernetes node, read the orchestrator's view: kubectl describe node <node> | grep -i pressure, or print df -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. Closeup of an external hard drive connected to a laptop with a USB cable on a desk.

Stop It From Coming Back

  • Cap container logs globally in /etc/docker/daemon.json with "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"}} in daemon.json so 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=168h weekly keeps a week of cache and drops the rest (leave --volumes out of anything automated).
  • Alert on all three numbers, not just one: df -h, df -i, and docker system df. Node Exporter's node_filesystem_avail_bytes plus node_filesystem_files_free will 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 .dockerignore excluding .git, node_modules and local build output, so each image is gigabytes smaller to begin with.
  • Put data-root on a dedicated volume when you provision a build or Docker host, so Docker filling up degrades Docker instead of taking /var/log, sshd and 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 device
  • ENOSPC: 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.

댓글

이 블로그의 인기 게시물

TypeError: Cannot read properties of undefined (reading 'map') - 11 Causes and Fixes

psql: FATAL: password authentication failed for user - 10 Causes and Fixes

Error: ERR_MODULE_NOT_FOUND: Cannot find package - 10 Causes and Fixes