Reported symptoms
The deploy pipeline fails on one host in the fleet. The error names a full disk:
failed to register layer: write /var/lib/docker/overlay2/9f3c.../diff/usr/
lib/node_modules/.package-lock.json: no space left on device
Everyone opens the disk dashboard, sees 41% used, and concludes the error message is wrong or that Docker is broken.
Diagnosis
The error message is not wrong. It is reporting the wrong resource because the kernel only has one errno for both.
df -h /var/lib/docker
df -i /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg0-docker 550G 221G 302G 43% /var/lib/docker
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/mapper/vg0-docker 36M 36M 0 100% /var/lib/docker
IUse% is the whole finding. Blocks are the resource everyone
watches; inodes are the resource that ran out.
Find which layers own the inodes. This walks the entire overlay2
tree and takes several minutes on a large host — it is read-only, but
run it with nice on a host that is still serving:
cd /var/lib/docker/overlay2
for d in */diff; do
printf '%s\t%s\n' "$(find "$d" | wc -l)" "${d%/diff}"
done | sort -rn | head -10
The top of that list is almost always the same shape: an application
image built FROM node or FROM python that ships its dependency
tree unpruned, rebuilt on every commit, with every historical tag
still resident on the host.
Confirm the ceiling is structural rather than transient:
DEV=$(findmnt -no SOURCE --target /var/lib/docker)
sudo tune2fs -l "$DEV" | grep -iE 'inode count|free inodes|block count'
An ext4 filesystem cannot grow its inode table. Whatever count it was created with is the count it has for life.
Resolution path
- Reclaim inodes so the host can deploy again.
docker image prune -a --filter until=168hremoves images not used by a container in the last week.docker builder prune --filter until=168hremoves the BuildKit cache, which on a build host is usually the larger half.- Re-check
df -i /var/lib/dockerafter each step; stop when you have working headroom. - **Do not reach for
docker system prune -a --volumesunder pressure.** - It removes named volumes that no running container references, which on a host with a stopped database container is the database.
- Record the file count that caused this.
- The prune bought time proportional to the images you deleted, not to the problem. Put the inode count and the top layers in the ticket.
- Size the durable fix.
- Target inode count = peak resident image file count x 3. Most build hosts need 4-8 KiB per inode, not the ext4 default of 16 KiB.
- Rebuild the filesystem, or move to XFS.
systemctl stop docker docker.socket, copy /var/lib/docker to a staging path withcp -aorrsync -aHAX(hardlinks and xattrs matter),mkfs.xfsormkfs.ext4 -i 8192on the device, copy back,systemctl start docker.- XFS allocates inodes dynamically, which removes the ceiling rather than raising it.
- Verify before handing the host back.
- Pull the image that failed. Start a container. Write a file inside it.
Verification
- **
df -i /var/lib/dockershows IUse% below the alert threshold.** - The failing pull completes.
docker pullon the image named in the original error. - A new container starts and writes.
docker run --rm alpine sh -c "touch /tmp/probe && echo ok" - **Both
df -handdf -iare recorded in the ticket** so the next responder has a baseline.
Prevention
- Alert on inode usage, not just block usage.
node_exporterexposesnode_filesystem_files_free; most teams graphnode_filesystem_avail_bytesand nothing else, which is exactly why this failure gets three hours of misdirected investigation. - Prune on a schedule with an age filter, so the job is bounded and predictable rather than an emergency action.
- Attack the file count at source. A multi-stage build that copies a built artefact into a distroless base ships hundreds of files where the single-stage image shipped hundreds of thousands.
- Provision /var/lib/docker on XFS on new hosts and stop having this conversation.