Docker Overlay2 Cleanup: Reclaim Disk Space Safely (2026)
Docker overlay2 cleanup: reclaim disk space with docker system prune, log limits, and containerd store checks. Stop /var/lib/docker/overlay2 filling your disk.
25 min read

Do not delete overlay2 folders manually
Never delete anything inside /var/lib/docker/overlay2 or /var/lib/containerd by hand. Those hash-named folders are Docker’s live metadata. Manual deletion corrupts images and breaks running containers. If you are asking “is it safe to delete /var/lib/docker/overlay2”, the answer is no. Use the supported Docker cleanup commands in this guide, and only take “reset the Docker data dir” actions if you fully understand the blast radius.
docker overlay2 cleanup is what you need when /var/lib/docker/overlay2 eats the disk and Docker starts failing writes. This guide shows how to reclaim disk space the safe way: the docker system prune family, container log limits, and one storage-backend checkpoint you should not skip. Since Docker Engine 29, fresh installs store images in /var/lib/containerd instead, so the directory filling your disk may not be overlay2 at all. Below is the full path: identify the backend, measure, prune, fix logs, diagnose leftovers, prevent the regrowth.
Step 0: Which Storage Backend? (overlay2 vs containerd image store)
Before any cleanup, find out where Docker actually keeps its data. This is a read-only check:
docker info -f '{{ .DriverStatus }}'Two possible outcomes:
- Output contains
[[driver-type io.containerd.snapshotter.v1]]→ you are on the containerd image store. Your data lives under/var/lib/containerd. - Output shows a plain overlay2 driver status → you are on the legacy overlay2 store. Your data lives under
/var/lib/docker/overlay2.
Docker Engine 29 changed the default storage
Docker Engine 29.0 (October 2025) made the containerd image store the default on fresh installs. Upgraded daemons keep the legacy overlay2 graphdriver until you switch explicitly. Hosts with userns-remap are excluded from the new default. Latest as of writing: Engine v29.9.0 (2026-10-08), buildx v0.38.0 (2026-10-07).
If you are about to set up a new box and want the current defaults documented end to end, install Docker and Docker Compose on Ubuntu ARM first, then come back here.
Legacy overlay2 store
# what to watch
du -sh /var/lib/docker/overlay2
# what Docker reports
docker system dfMost of this guide targets this store. The prune commands below work the same, but the directory names in overlay2 are what you will map later.
containerd image store
# what to watch
du -sh /var/lib/containerd
# what Docker reports (still authoritative)
docker system dfdocker system df remains the source of truth for categories. containerd keeps both compressed blobs and unpacked snapshots, so the same images cost more disk than on the legacy store. Docker’s docs say “uses more disk space” without a multiplier, and I will not invent one.
Verify: paste the real docker info -f '{{ .DriverStatus }}' output into your notes before you prune. You want a baseline of which store is active.
Failure mode: “I pruned overlay2 and nothing was reclaimed.” On a containerd-store host you were looking at the inactive store. Active-store prune commands do not touch the other backend’s data (see the hidden-store section below).
Version context, short: the old world is gone. AuFS and the legacy overlay driver were removed in Engine v24, devicemapper in v25, graphdriver plugins in v28 (official replacement advice: use the containerd image store). Engine v29 requires API v1.44 or newer. If your cleanup notes predate 2023, re-read them before running anything.
What’s Inside /var/lib/docker/overlay2
overlay2 is Docker’s copy-on-write filesystem for Linux. The directory holds:
- Layer directories: content-addressed folders with the actual filesystem diffs
- Container “upperdir” data: the writable layers where containers write files. This is usually what gets big
- Metadata and linkage: structures Docker uses to track which layers belong to which images
The folder names are random hashes, not readable identifiers. You cannot look at a folder name and know what image it belongs to. Use Docker commands instead of guessing from directory names. Docker deliberately keeps this layout undocumented.
On containerd-store hosts the equivalent internals live under /var/lib/containerd as a content store plus overlayfs snapshots. Same rule: measure with Docker commands and du, not by browsing hash folders.
That map is worth keeping in your head. The rest of this guide walks each box: measure it, clean it, then stop it from coming back.
Why /var/lib/docker/overlay2 Grows So Large
The directory grows for a handful of predictable reasons:
- Unused images and containers: stopped containers and old images still occupy space if you do not remove them.
- Build cache: Docker caches layers to make builds faster. On a box that builds often, this cache is the top offender.
- Large images and containers: some images ship big binaries or data files. Four copies of a 1.5 GB base image add up.
- Inefficient image layers: images with too many layers or leftover build junk waste space. If you control the Dockerfile, copy multiple files in one layer with a Docker COPY instead of stacking one
COPYper file. - Unbounded container logs: this is the sneaky one. The json-file log driver grows without limit by default, and the files are not in overlay2. They live in
/var/lib/docker/containers/. There is a dedicated section below.
On containerd-store hosts, add one more: blobs plus unpacked snapshots make the same image set cost more disk than the legacy store.
If you want to see which category is actually growing over time, monitor server and Docker resources (CPU, memory, disk) rather than running du by hand every week.
Ops note: a full filesystem does not fail only Docker pulls. Database containers lose writes too, and some of those failures are silent until the app errors. Target 15-20% free space on any host that runs stateful containers.
Check Docker Disk Usage First
Do this before touching anything. It is read-only.
docker system df
docker system df -vdocker system df shows space usage for images, containers, volumes, and build cache. The -v flag adds per-object detail. For monitoring scripts, add --format json (or a Go template) so you can parse it instead of scraping tables.
Then check the real filesystem:
df -h /var/lib/docker
du -sh /var/lib/docker/overlay2 /var/lib/docker/containers /var/lib/containerd 2>/dev/nullTo see which overlay2 subdirectories are largest, just for investigation, never for deletion:
du -sh /var/lib/docker/overlay2/* 2>/dev/null | sort -hr | head -10How to read the output:
- Large
overlay2usage usually means container writes or build cache, not just unused images. - Focus on the growing category in
docker system df -v(images, containers, volumes, build cache), not on specific IDs. - Save the
docker system df -voutput before cleanup. That is your baseline; you will compare against it after.
Symptom → likely cause → first command
| Symptom | Likely cause | First command |
|---|---|---|
| overlay2 huge on a Docker 29 host | hidden legacy store after a backend switch | docker info -f '{{ .DriverStatus }}', then assess or move the old store |
*-json.log files huge |
unlimited json-file logging | du -sh /var/lib/docker/containers/* | sort -hr | head |
docker system df shows 0% reclaimable |
active writable layers and in-use images | docker ps -s: scale the disk out, do not prune harder |
| prune ran, little reclaimed | named volumes; cache living in buildx | docker system df -v, then target the volumes and build cache rows |
builder prune: unknown flag --keep-storage |
old tutorials vs new buildx | docker builder prune --help, then use --max-used-space |
Docker Overlay2 Cleanup: The Prune Playbook
This is the ordered, safest-first cleanup. Each command states what it removes. Run one, check docker system df, then decide if you need the next. That is slower than firing everything at once and much easier to explain when something you wanted disappears.
Quick Win: docker system prune (and -a)
Start with the read-only report again so the numbers are fresh:
docker system df -vRemove stopped containers, unused networks, dangling images, and build cache:
docker system prune -fWhat this does: clears objects nothing references. What it will not do: touch named volumes, and it keeps images that some container (even a stopped one) still points at.
If you also want to remove all images not used by any container, which is the more aggressive step:
docker system prune -a -fFailure mode: -a removes images that a stopped-but-wanted container needs on its next start. Nothing is corrupted, but you pay the re-pull time at the worst moment. On hosts with containers you keep around for rollback, skip -a or run it inside a maintenance window.
For scheduled housekeeping on build boxes, filters keep the prune honest:
docker system prune -af --filter until=168huntil= accepts RFC3339 timestamps, dates, or Go durations like 10m and 24h. label= selects by label. Multiple filters across different keys are ANDed. A weekly cron with until=168h is a boring, reliable default for CI runners.
If you want the full teardown playbook for images, volumes, networks, and disks, see how to clean all Docker images with disks and everything.
Verify: docker system df after each command. On containerd-store hosts, note that older Engine 29 builds under-reported reclaimed space because they ignored unpacked image data (moby#52905, fixed in a later v29 patch). Confusing numbers there were a bug, not your mistake.
Volumes: Anonymous vs Named (docker volume prune -a)
This is the part older guides get wrong, and it is why people search for “docker volume prune not reclaiming space”.
Since Docker Engine 23.0 / API 1.42, docker volume prune removes anonymous volumes only. Named volumes need the -a flag:
docker volume prune -f # anonymous volumes only
docker volume prune -a -f # ALSO unused named volumes, check first!Same caveat for docker system prune --volumes: the official warning says it removes “all anonymous volumes not used by at least one container”. Named volumes survive.
Check before you delete anything:
docker volume ls
docker system df -vNamed volumes hold real data
docker volume prune -a deletes unused named volumes. Databases, uploads, and state live there. If a compose project is stopped and you prune named volumes, that data is gone. Back up the volume or confirm it is throwaway before running -a. This is the classic “we deleted prod data” story.
Verify: docker volume ls before, docker system df after. If the volumes row barely moved after docker volume prune -f, that is expected: your space is in named volumes, and only -a will reclaim them.
Build Cache: docker builder prune with Capacity Caps
If you build images on the host, BuildKit cache is often the biggest single category. Prune it directly:
docker builder prune -f # dangling cache
docker builder prune -af # all unused cacheNewer buildx (v0.38.0 as of 2026-10) adds capacity caps, which are better than waiting for disk-full alarms:
docker builder prune -f --max-used-space 10gb # LRU-evict down to 10 GB
docker builder prune -f --min-free-space 20gb # keep 20 GB free on the filesystem
docker builder prune -f --reserved-space 5gb # never evict the last 5 GB of cache
docker builder prune -f --filter until=168h # cache older than 7 days
docker buildx prune -a --max-used-space 10gb # also internal/frontend imagesSizes are human-readable and case-insensitive (10gb, 500MB). Filters include type=, type!=exec.cachemount (keep cache mounts, usually what you want), until=, description=, and parent=. Multiple --filter flags are ANDed.
If you self-host CI, self-host your Git and CI/CD with Forgejo and Woodpecker CI and put the --max-used-space prune in a weekly timer on the runner. That is where build cache goes feral.
Failure mode: unknown flag: --keep-storage. Older tutorials use it; the current buildx prune options table does not list it. Run docker builder prune --help on your host and use whatever capacity flag your version actually has (--max-used-space on recent buildx). Reclaiming nothing on a fresh box is normal, not a bug.
Verify:
docker builder prune --help | grep -E 'max-used|keep-storage'Then re-run docker system df and compare the Build Cache row against your baseline.
The Real Disk Killer: Docker Container Logs
Container logs are the disk problem that no overlay2 cleanup will fix. The default json-file log driver writes to /var/lib/docker/containers/<id>/<id>-json.log and grows without limit (max-size defaults to -1, max-file to 1). A chatty app on a small VPS can fill the disk in days while docker system df looks almost clean.
Find the offenders:
du -sh /var/lib/docker/containers/* 2>/dev/null | sort -hr | head
docker ps -sdocker ps -s shows each container’s writable layer size, which tells you whether the bloat is logs or app writes.
Emergency (now)
Truncate, do not delete. Deleting the file breaks the file handle and the space is not freed until the container restarts. Truncating is common practice in the field (it is not in the official docs, so test on your host first):
truncate -s 0 /var/lib/docker/containers/*/*-json.logReclaim happens immediately. The container keeps writing to the same handle.
Permanent (daemon.json)
Set log rotation in /etc/docker/daemon.json. Values must be strings:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}Then restart Docker:
sudo systemctl restart dockerEach container keeps at most 3 files of 10 MB. Change the numbers to taste; just cap them at something.
log-opts apply to new containers only
Restarting the daemon applies the new log options to newly created containers only. Existing containers keep unlimited logging until you recreate them. For Compose apps, update a container with Docker Compose (docker compose up -d --force-recreate) so the new log-opts take effect.
If you would rather ship logs somewhere central than rotate them locally, redirect Docker logs to a single file with a different log driver.
Verify:
du -sh /var/lib/docker/containers
docker inspect <container> --format '{{ json .HostConfig.LogConfig }}'A new container should show "max-size":"10m" and "max-file":"3".
Failure mode: you edited daemon.json, restarted Docker, and logs keep growing. The old containers were never recreated. Check docker inspect on one of them; if the log config is still empty, the file was not re-read for that container.
Map overlay2 Directories to Docker Objects
Sometimes you need to know which container owns a giant writable layer. Use Docker’s own output before any scripting:
docker ps -s
docker inspect -f '{{ .Name }} {{ .GraphDriver.Data.UpperDir }}' $(docker ps -aq)GraphDriver.Data.UpperDir is the writable-layer path straight from docker inspect. That is a supported mapping, and it is the fastest way to match a huge directory to a container name.
For images versus layer directories, I have a helper script that visualizes overlay2 disk usage:
curl -sSL https://utils.bitdoze.com/scripts/docker-overlay2-view.sh | bashAll scripts are at utils.bitdoze.com.
Guidance, not a delete list
This mapping is best-effort. overlay2 is an internal structure, and not every directory has a clean “this belongs to image X” relationship. The script covers the legacy overlay2 store only. On a containerd-store host docker inspect does not return overlay2 paths, so empty output there is expected, not broken. Use the output as guidance for what to investigate via Docker commands, never as a list of directories to delete.
Why overlay2 Subfolders Don’t Match Image IDs
You pruned everything and there are still hash directories that match no image. Normal. Here is why:
- Container writable layers: running or stopped containers each have a writable layer in
overlay2. If you have containers, you have overlay data. - Build cache: modern builds (BuildKit) create cache data.
docker system prune -adoes not always remove all of it. Target it withdocker builder prune. - Shared or referenced layers: images share layers, and Docker will not remove anything still referenced. The directory name will not match what you see in
docker image ls. - Init layers and internal metadata: the
*-initdirectories and metadata structures are part of how Docker sets up containers. - Leaked or orphaned data (rare): power loss, daemon crashes, or storage corruption can leave leftovers. Check
docker system df -vand the daemon logs before assuming this is your case. - Hidden legacy store after a backend switch: this one is new. If you moved to the containerd image store (or the reverse), the inactive store’s data stays on disk but is invisible to the active store’s prune commands. Prune reports nothing to reclaim while
dfshows a full disk. Detect it withdocker info -f '{{ .DriverStatus }}'and compare againstdu -sh /var/lib/docker/overlay2 /var/lib/containerd.
Even if a directory looks unused, deleting overlay2 directories by hand can corrupt Docker’s state. If you genuinely need a fresh start, do the controlled reset below with Docker stopped and your data backed up.
Prevent /var/lib/docker/overlay2 From Filling Up Again
Cleanup is a one-time win. These five habits keep the disk healthy:
- Cap log size. The
daemon.jsonlog-optsfrom the logs section are the highest-leverage change on any host that runs chatty containers. - Schedule housekeeping on build boxes. A weekly timer beats a disk-full alarm:
docker system prune -af --filter until=168h
docker builder prune -af --max-used-space 10gb- Move Docker’s data dir to a bigger volume. Small root disks are the usual reason Docker fills the OS partition. In
/etc/docker/daemon.json:
{
"data-root": "/mnt/volume/docker"
}The flag is --data-root on the dockerd command line too (the old -g/--graph flags were removed in Engine v23). If the box needs the hardware, add a new drive to Ubuntu LVM and mount it permanently first. On Hetzner-style VPS boxes where the root disk is small and a separate volume is cheap, Hetzner’s cost-optimized cloud plans pair well with this setup, and a Hetzner Cloud VPS with a mounted volume solves the “Docker ate the OS disk” problem without moving data around later. A Hostinger VPS with NVMe storage is the alternative if you want one big local disk instead. Storage headroom is cheaper than downtime.
Warning: data-root does not migrate existing data. Plan a copy or move with the daemon stopped, then verify with docker info | grep 'Docker Root Dir' before you delete anything from the old path.
- Alert on filesystem usage. Keep 15-20% free and get paged before 80%, not after writes fail. Set up Beszel and Uptime Kuma for server monitoring and disk alerts if you want a self-hosted path.
- Stay patched. Engine 29.x patches have included both the prune under-reporting fix and a BuildKit cache-poisoning fix (GHSA-p3rc-w3hc-pqvv, February 2026): a malicious cache image or remote cache can inject false build cache into your builds. That one hits anyone importing build cache, not just containerd-store users. Security fixes land in patch releases; run them.
- json-file
log-optsset withmax-sizeandmax-file - Weekly prune scheduled on build boxes and CI runners
data-rooton a filesystem sized for your image churn- Disk usage alerts firing before 80%
- 15-20% free headroom on the Docker filesystem
Switching to the containerd image store
Enabling "features": { "containerd-snapshotter": true } in daemon.json switches the active store, but it does not clean up. Expect disk usage to grow for a while: containerd keeps compressed blobs and unpacked snapshots, and the legacy overlay2 data stays frozen on disk. userns-remap is not available with the containerd image store (moby#47377). Docker documents an experimental auto-migration (containerd-migration plus a snapshotter threshold), and Docker itself calls it unreliable. Prefer a fresh start on a host you can rebuild.
Verify after any data-root change:
systemctl status docker
docker info | grep 'Docker Root Dir'Failure mode: a typo in daemon.json (a number where a string is required, a missing comma) keeps dockerd from starting. journalctl -u docker shows the parse error. Keep a copy of the last working file before editing.
Last Resort: Controlled Docker Reset (Both Stores)
Last resort
This is disruptive. Every image is re-pulled, every stopped container is effectively gone. Safer than deleting random hash directories, but only do it after backups and only when the prune playbook has nothing left to reclaim.
Legacy overlay2 host
- Stop or remove containers you do not need.
- Back up anything important: named volumes and bind-mounted data especially.
- Stop the Docker daemon:
sudo systemctl stop docker. - Move the data dir:
sudo mv /var/lib/docker /var/lib/docker.bak. - Start Docker again:
sudo systemctl start docker. It recreates/var/lib/dockerempty.
Then re-pull images and restore only what you need. Once everything runs and you have reclaimed the space, delete /var/lib/docker.bak in a second, deliberate step. Not in the same command session.
containerd image store host
- Stop or remove containers you do not need.
- Back up named volumes and bind-mounted data.
- Stop the Docker daemon:
sudo systemctl stop docker(and containerd if it runs as a separate unit you manage). - Move both data dirs:
sudo mv /var/lib/docker /var/lib/docker.bakandsudo mv /var/lib/containerd /var/lib/containerd.bak. - Start Docker and confirm it is clean before touching the
.bakdirectories.
If you move only /var/lib/docker on a containerd host, the reset appears to do nothing: the visible data was in /var/lib/containerd all along. If you previously switched backends, the inactive store’s leftovers also live in whichever directory is not active. That is why both moves are in the steps.
Verify: df -h before and after, docker info shows a fresh root dir, and a test container starts (docker run --rm alpine echo ok). Only then schedule the .bak cleanup.
FAQ: Docker Overlay2 Cleanup Questions
Is it safe to delete /var/lib/docker/overlay2 manually?
No. The hash directories are live layer and metadata structures. Deleting them corrupts Docker’s graph and can break running containers and images even if the directory “looks” unused. Use docker system prune, docker builder prune, and the other supported commands in this guide. The only sanctioned bulk removal is a controlled reset of the whole data dir with the daemon stopped.
Why is docker volume prune not reclaiming space?
Because since Docker Engine 23.0 / API 1.42, docker volume prune removes anonymous volumes only. Your space is probably in named volumes (databases, uploads), which need docker volume prune -a. Check with docker volume ls and docker system df -v first. Named volumes hold real data, so back up before -a.
Why is /var/lib/docker/overlay2 so big on a Docker 29 server?
Two likely reasons. First, Engine 29 enables the containerd image store on fresh installs, so you may be looking at leftover legacy-store data that prune commands will never touch. Detect with docker info -f '{{ .DriverStatus }}'. Second, overlay2 holds container writable layers and build cache, which grow even when every image is in use. Read the category breakdown in docker system df -v before assuming unused images are the problem.
Do Docker container logs really take up disk space?
Yes. The json-file log driver defaults to unlimited size (max-size is -1) and the files live in /var/lib/docker/containers/, not in overlay2. Truncate them for an immediate fix (truncate -s 0 on the *-json.log files, do not delete), then set log-opts with max-size and max-file in /etc/docker/daemon.json and recreate the containers so the rotation applies.
What's the fastest way to reclaim Docker disk space tonight?
In order: docker system df -v to see the biggest category, docker system prune -f for the safe win, docker builder prune -af --max-used-space 10gb if build cache is large, then check du -sh /var/lib/docker/containers/* for runaway logs and truncate them. Save the docker system df -v baseline first so you can prove what you reclaimed. For the deep-dive version, see how to clean all Docker images with disks and everything.
Summary
A growing /var/lib/docker/overlay2 (or /var/lib/containerd on Engine 29 hosts) comes down to four things: unused objects, build cache, writable-layer writes, and unbounded container logs. Do the docker overlay2 cleanup in that order of blame: run docker system df -v, prune with the supported commands (remember named volumes need docker volume prune -a), cap your logs with log-opts, and schedule housekeeping instead of waiting for the disk-full alarm.
Do not delete anything inside those directories by hand. If you reach the point where prune has nothing left to give and the disk is still full, do the controlled reset with the daemon stopped and backups in place.
All Docker cleanup commands

