Ch. 13 · Docker

Docker 'no space left on device': Safe Cleanup for Builds and Pulls

Fix Docker 'no space left on device' during build or pull: measure with docker system df, prune in a safe order, and cap logs and build cache.

~8 min readintermediateupdated Oct 4, 2026

A build or pull dies part-way through with a message that ends the same way every time:

ERROR: failed to solve: ... : no space left on device
failed to register layer: ... : no space left on device
failed to extract layer sha256:...: ... : no space left on device
Text

The first line is BuildKit during docker build; the second is docker pull with the classic overlay2 image store; the third is a pull with the containerd image store (the default on Docker Desktop). A process inside a RUN step may report it its own way, such as npm’s ENOSPC. All of them are the kernel’s ENOSPC: the filesystem Docker writes to has run out of free blocks or free inodes.

Gotcha

Several commands below delete data. Read what each one removes before running it, and never run a volume prune on a machine whose volumes you have not inspected.

Quick fix checklist

  • Find Docker’s data root: docker info --format '{{.DockerRootDir}}', then df -h and df -i on it.
  • See what uses the space: docker system df (add -v for detail).
  • Reclaim build cache first: docker builder prune (only costs rebuild time).
  • Then stopped containers and unused images: docker container prune, docker image prune (add -a only if you can re-pull or rebuild).
  • Look for huge container logs under <data-root>/containers/*/*-json.log.
  • Touch volumes last, by name, after a backup.
  • On Docker Desktop, check Settings > Resources > Advanced > Disk usage limit.

Before you start

This guide assumes Docker Engine 27/28 on Linux or Docker Desktop, and that you can run sudo on the host. It explains commands rather than reporting runs: no prune was executed while writing it, and the sample outputs follow the formats in Docker’s documentation. Know the difference between an image, a container and a volume; images versus running containers covers that.

Why it happens

Everything Docker stores lives under one directory, the data root (/var/lib/docker by default on Linux; with the containerd image store, image content lives under /var/lib/containerd too). Usually that is on the root filesystem, so Docker competes with the OS for space. Five things grow there:

  1. Images. Every tag you pulled plus the <none> “dangling” images left behind each time a rebuild moves a tag to a new image.
  2. Build cache. BuildKit keeps layer results and cache mounts (RUN --mount=type=cache) so the next build is fast. On a CI runner this is often the biggest consumer.
  3. Containers. Each container’s writable layer stays on disk after it stops, until the container is removed.
  4. Volumes. Database files and anything else written to named or anonymous volumes. This is the data you usually cannot recreate.
  5. Logs. The default json-file log driver writes stdout/stderr to <data-root>/containers/<id>/<id>-json.log and, by default, never rotates it (max-size defaults to unlimited).

Two variations catch people out. On Docker Desktop, the data root is inside a Linux VM whose virtual disk has its own size limit, so builds fail even when macOS or Windows shows plenty of free space. And overlay2 with many tiny files (think node_modules copied into dozens of image versions) can exhaust inodes while df -h still shows free gigabytes; df -i reveals it.

Step-by-step walkthrough

Step 1: Confirm which filesystem is full

docker info --format '{{.DockerRootDir}}'
df -h /var/lib/docker
df -i /var/lib/docker
Terminal

If Use% is at or near 100 in either output, you have found the problem. If the data root looks fine, the failing write may be elsewhere: a bind-mounted host directory or /tmp on the host. On Docker Desktop, skip df and look at the disk usage in Settings and the output of the next step.

Step 2: Break the usage down

docker system df
Terminal
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          41        6         18.2GB    14.9GB (81%)
Containers      9         3         1.1GB     0.9GB (81%)
Local Volumes   12        4         6.4GB     2.3GB (35%)
Build Cache     310       0         27.5GB    27.5GB
Text

ACTIVE counts objects in use by a container (running or stopped). RECLAIMABLE is what pruning unused objects of that type could free. docker system df -v lists individual images, containers, volumes and cache records with sizes, which is how you spot one 9 GB volume or a single fat image.

Logs are not in that table. Check them separately:

sudo sh -c 'du -h /var/lib/docker/containers/*/*-json.log' | sort -h | tail -n 5
docker ps -a --format '{{.ID}} {{.Names}}'     # match the ID prefix to a name
Terminal

Step 3: Reclaim space in order of risk

Work down this list and re-check df -h after each step; often the first one is enough.

# 1. Build cache: only costs a slower next build.
docker builder prune                      # dangling cache only
docker builder prune --filter until=72h   # cache not used in the last 3 days
docker builder prune -a                   # all unused build cache

# 2. Stopped containers: their writable layers are deleted.
docker ps -a --filter status=exited       # review first
docker container prune

# 3. Images: dangling first, then anything not used by a container.
docker image prune                        # <none> images only
docker image prune -a --filter until=168h # unused images older than 7 days
Terminal

image prune -a deletes images no container uses, including ones you built locally and never pushed. Make sure you can rebuild or re-pull them.

Volumes come last:

docker volume ls --filter dangling=true   # volumes not attached to any container
docker volume prune                       # anonymous unused volumes only
Terminal

Since API 1.42 (Docker 23), docker volume prune removes only anonymous volumes unless you add -a, which also deletes unused named volumes. A stopped database container’s volume counts as “used” only while that container exists, so pruning containers first can turn a database volume into a prune target. Back up anything you need, and remove specific volumes by name rather than by prune.

Step 4: Deal with an oversized log safely

Deleting <id>-json.log under a running container does not free the space: the daemon still holds the file open. Restarting the container with rotation configured is the real fix (Step 5). In an emergency, sudo truncate -s 0 <path-to>-json.log releases the space immediately at the cost of that container’s log history.

Step 5: Prevent it from coming back

Cap logs for all new containers in /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "builder": {
    "gc": {
      "enabled": true,
      "defaultKeepStorage": "20GB"
    }
  }
}
JSON

Restart the daemon (sudo systemctl restart docker) and recreate containers: existing containers keep the log settings they were created with. The builder.gc block tells BuildKit to trim its cache automatically; Docker’s garbage collection docs describe newer per-policy keys such as reservedSpace if you need finer control. On Docker Desktop, edit the same JSON under Settings > Docker Engine, and raise Settings > Resources > Advanced > Disk usage limit if your normal workload genuinely needs more room. Avoid Troubleshoot > Clean / Purge data unless you want every image, container and volume gone.

Worked scenario

A self-hosted CI runner with a 60 GB disk starts failing nightly builds:

#12 [build 5/8] RUN npm ci
#12 41.20 npm error code ENOSPC
#12 41.20 npm error nospc ENOSPC: no space left on device, write
ERROR: failed to solve: process "/bin/sh -c npm ci" did not complete successfully: exit code: 228
Text

npm’s own exit code here is just a symptom (the ENOSPC lines matter, not the 228). Diagnosis on the runner:

$ df -h /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p1   60G   59G  1.0G  99% /

$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          63        2         21.4GB    20.1GB (93%)
Containers      2         2         88MB      0B (0%)
Local Volumes   1         1         120MB     0B (0%)
Build Cache     512       0         31.7GB    31.7GB
Text

Every pipeline tagged a new image and nothing ever cleaned up. Volumes are tiny and in use, so they are not part of the fix. The team adds a scheduled job on the runner:

docker builder prune -f --filter until=48h
docker image prune -a -f --filter until=72h
Terminal

and enables builder.gc with defaultKeepStorage set to 15GB in daemon.json. After the first run, df -h shows 38 GB free and builds pass; the cache stays warm for anything built in the last two days.

Common mistake

The command people reach for is:

docker system prune -a --volumes -f
Terminal

On a CI runner with no state that may be acceptable. On a developer machine or a server it deletes every unused image, all build cache, every stopped container and, because of --volumes, unused volumes, which is where local databases live. -f removes the confirmation prompt that would have told you so.

Also wrong:

  • Deleting directories under /var/lib/docker/overlay2 by hand. Docker’s metadata still references them, and you end up with images and containers that fail to start or remove.
  • Only growing the disk. It buys time; without log rotation and cache limits the disk fills again.
  • rm on a running container’s log file. The space stays allocated until the container stops.

Verify the behavior

df -h "$(docker info --format '{{.DockerRootDir}}')"
docker system df
docker run -d --name logcheck nginx:1.27
docker inspect --format '{{.HostConfig.LogConfig}}' logcheck
Terminal

Expect free space well above your largest build’s needs, a Build Cache line within the GC limit, and for the new container:

{json-file map[max-file:3 max-size:10m]}
Text

Finally, rerun the build that failed; it should complete. Remove the test container with docker rm -f logcheck.

Interview exercise

“A production Docker host is at 97% disk. docker system df shows 40 GB of reclaimable images, 12 GB of build cache and 25 GB of volumes. What do you clean, in what order, and how do you stop it happening again?”

Answer and reasoning

Start by confirming the full filesystem really is the data root (df -h, df -i) and checking container log sizes, since logs do not appear in docker system df. Then remove what is cheapest to recreate: build cache (a production host should not be building at all), then stopped containers after a review, then dangling images and unused images old enough that no rollback needs them. Images can be pulled again; that is why they come before anything stateful.

Volumes hold data that may not exist anywhere else, so I would not prune them. I would list them with docker system df -v, map each to the service that created it, and remove specific ones only after confirming a backup. For prevention: json-file max-size/max-file (or a log shipper), BuildKit GC limits, an image retention policy on deploys, and a disk-usage alert at 80% so cleanup is planned rather than an outage.

Continue learning

More in Docker

esc