Root Cause Diagnosis
/var/lib/docker/overlay2/ accumulates directories named by content-addressable hash for every image layer, stopped container’s writable layer, and build-cache intermediate that Docker has ever created; the daemon does not garbage-collect them automatically. Directories become “dead” (safe to remove) when their owning image, container, or build cache entry no longer exists in Docker’s internal reference graph, but a crashed daemon, a killed docker build, or manual interference with /var/lib/docker can also leave orphaned directories with no corresponding metadata, or conversely leave metadata pointing at a directory you deleted by hand. Manually deleting anything under overlay2/ without going through the Docker API breaks the mount references Docker tracks internally (in /var/lib/docker/image/overlay2/layerdb/ and the container/image JSON stores), which can corrupt running containers, fail future container starts with “no such file or directory”, or leave the daemon unable to start at all.
Quick Fix
Never rm -rf inside overlay2/ directly. Use Docker’s own pruning, which cross-references the reference graph before deleting anything:
docker system df -v
docker system prune -a --volumes
-a removes all unused images (not just dangling ones); --volumes removes unused volumes. Review docker system df -v output first if the host has data you don’t want to lose, since this command is destructive and irreversible.
Step-by-Step Resolution
1. Inventory current usage before touching anything
docker system df -v
du -sh /var/lib/docker/overlay2/* 2>/dev/null | sort -rh | head -20
df -h /var/lib/docker
docker system df -v shows reclaimable space per image, container, and volume, mapped through Docker’s own accounting. The raw du pass tells you which individual overlay2 directories are largest, useful for identifying whether the bloat is images, container logs, or build cache before you decide what to prune.
2. Remove unused containers first, then images, then build cache
Prune in this order — containers depend on images, and both depend on overlay2 layers, so top-down avoids the daemon holding references you’re trying to clear.
docker container prune -f
docker image prune -a -f
docker builder prune -a -f
docker volume prune -f
Check space reclaimed at each step:
docker system df
3. Identify genuinely orphaned overlay2 directories
After a full prune, cross-reference remaining overlay2/ directories against what Docker still knows about:
comm -23 \
<(ls /var/lib/docker/overlay2/ | sort) \
<(docker system df -v --format '{{json .}}' 2>/dev/null; \
find /var/lib/docker/image/overlay2/layerdb/sha256 -maxdepth 1 -type d -exec basename {} \; | sort) \
> /tmp/possible_orphans.txt
A more reliable method on modern Docker is to stop the daemon and use the built-in check rather than hand-rolled diffing, since layerdb references are stored as symlinked cache-id files, not directory names directly:
sudo systemctl stop docker
sudo systemctl stop docker.socket containerd
With the daemon stopped, no new references can appear mid-comparison. List directories under overlay2/ and cross-check each against cache-id files:
for d in /var/lib/docker/overlay2/*/; do
id=$(basename "$d")
grep -rl "$id" /var/lib/docker/image/overlay2/layerdb/sha256/*/cache-id 2>/dev/null || echo "ORPHAN: $id"
done > /tmp/overlay2_orphans.txt
wc -l /tmp/overlay2_orphans.txt
Review the list manually. Entries flagged ORPHAN have no cache-id file pointing at them and are candidates for removal, but confirm none are -init suffixed companions to a layer still in use (init layers share a parent hash prefix with their container layer).
4. Remove confirmed orphans with the daemon stopped
sudo systemctl status docker # confirm it is stopped, not just the socket
sudo tar -czf /root/overlay2-backup-$(date +%F).tar.gz \
-C /var/lib/docker overlay2 image/overlay2/layerdb
while read -r orphan; do
id="${orphan#ORPHAN: }"
sudo rm -rf "/var/lib/docker/overlay2/${id:?}"
done < /tmp/overlay2_orphans.txt
The ${id:?} guard aborts the loop if a line parses to an empty string, preventing an accidental rm -rf /var/lib/docker/overlay2/. The tar backup lets you restore if a directory is removed incorrectly; delete it once the daemon starts cleanly and containers run normally.
5. Restart Docker and validate
sudo systemctl start containerd
sudo systemctl start docker
sudo systemctl status docker
docker ps -a
docker images
docker run --rm hello-world
If any existing container fails to start with an error referencing a missing merged, diff, or lower path under overlay2/, that container’s writable layer was affected. Recreate the container from its image (docker compose up -d or docker run with the original parameters); the container’s data is not recoverable if its layer was deleted, only stateless recreation is possible.
6. Prevent recurrence
Enable log rotation and periodic pruning rather than manual cleanup:
/etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2"
}
Add a scheduled prune (adjust retention to your build cadence):
sudo crontab -e
0 3 * * 0 docker system prune -af --filter "until=168h" >> /var/log/docker-prune.log 2>&1
Why This Fails for Some Users
- Daemon won’t start after manual deletion: If a directory referenced by a
cache-idfile was removed,dockerdlogserror mounting overlay: no such file or directoryorlayer does not existon startup and refuses to serve requests for affected images. Restoring from the tar backup (Step 4) or fully reinstalling affected images (docker pull/docker buildagain) is the only recovery. overlay2vsoverlay2-fuseorzfs/btrfsdrivers: This procedure applies only to theoverlay2storage driver. Confirm withdocker info --format '{{.Driver}}'before following layerdb-based orphan detection; other drivers use different metadata layouts (zfsdatasets,btrfssubvolumes) and thelayerdb/sha256/*/cache-idcross-reference does not apply.- Kernel overlay filesystem limits: Some older kernels (pre-4.x) or specific distributions impose a maximum overlay stacking depth; large images with many layers can hit
too many levels of symbolic linksindependent of disk space, which pruning does not fix. This requires reducing image layer count (multi-stage builds,--squash) rather than storage cleanup. - Containers using
--rmwith orphaned state: Containers started with--rmthat crash mid-write can leave a writable layer directory behind if the daemon itself crashes before cleanup completes;docker container prunewon’t find them since no container object exists, butdocker system prunecatches them via dangling layer detection in most cases. Reboot and re-run prune if directories persist. - Build cache from BuildKit vs legacy builder:
docker builder pruneonly clears the currently active builder’s cache. If BuildKit was enabled (DOCKER_BUILDKIT=1) after builds ran on the legacy builder, or vice versa, orphaned cache from the other builder can remain. Check both:docker buildx dufor BuildKit,docker system dffor legacy. - Overlay2 directories held open by a zombie process:
rmon a directory with an active bind mount or a process holding an open file descriptor fails silently or leaves.wh.whiteout files behind. Check withlsof +D /var/lib/docker/overlay2/<id>andmount | grep <id>before forcing removal; unmount first withumount /var/lib/docker/overlay2/<id>/mergedif it appears mounted. - Rootless Docker: Paths differ entirely — data lives under
~/.local/share/docker/overlay2/and the daemon runs as the invoking user, not root. Adjust all paths and dropsudofrom commands, or they will silently operate on the wrong (nonexistent) root-owned path. - containerd managed separately from Docker (Kubernetes nodes running both): On nodes where
containerdalso serves a Kubernetes CRI socket independent of the Docker daemon, overlay2 directories under /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/ are a separate, parallel structure from Docker’s own/var/lib/docker/overlay2/. Pruning Docker’s does not affect containerd’s CRI-managed layers, and vice versa; confirm which subsystem owns the space you’re trying to reclaim withcrictlvsdocker.