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 deviceThe 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}}', thendf -handdf -ion it. - See what uses the space:
docker system df(add-vfor 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-aonly 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:
- Images. Every tag you pulled plus the
<none>“dangling” images left behind each time a rebuild moves a tag to a new image. - 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. - Containers. Each container’s writable layer stays on disk after it stops, until the container is removed.
- Volumes. Database files and anything else written to named or anonymous volumes. This is the data you usually cannot recreate.
- Logs. The default
json-filelog driver writes stdout/stderr to<data-root>/containers/<id>/<id>-json.logand, by default, never rotates it (max-sizedefaults 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/dockerIf 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 dfTYPE 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.5GBACTIVE 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 nameStep 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 daysimage 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 onlySince 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"
}
}
}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: 228npm’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.7GBEvery 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=72hand 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 -fOn 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/overlay2by 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.
rmon 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}}' logcheckExpect 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]}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
- Docker interview questions
- Docker MCQs
- Docker BuildKit and cache mounts
- Docker logs and application diagnostics
- Docker base image maintenance and dependency updates
- Docker ‘exec format error’ in docker-entrypoint.sh
- Official: docker system df, docker builder prune, docker volume prune, JSON file logging driver, build garbage collection and Docker Desktop settings