A server that’s been running containers for a few months has a way of quietly filling its disk. Then a deploy fails with no space left on device, a database refuses to write, and you’re staring at a 90%-full /var/lib/docker wondering what’s safe to delete.
The good news is that most of what Docker accumulates is genuinely disposable: old image versions, stopped containers, dead build cache, and unbounded log files. The catch is that one of the cleanup commands also deletes volumes, which is where your real data lives. Get the order right and you reclaim tens of gigabytes with zero data loss.
This guide walks through finding what’s eating the disk, pruning the safe stuff first, capping logs so it doesn’t happen again, and understanding what the giant overlay2 directory actually is.
First, see what’s actually using space
Don’t prune blindly. Start with a summary so you know whether the problem is images, volumes, build cache, or logs:
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 42 6 18.3GB 14.1GB (77%)
Containers 11 4 210MB 180MB (85%)
Local Volumes 9 5 6.2GB 2.1GB (34%)
Build Cache 310 0 9.8GB 9.8GB (100%)
The RECLAIMABLE column is what you can free without touching anything in use. In the example above, images and build cache alone hold ~24 GB of reclaimable space. Add -v for the per-item detail when you need to find the single biggest offender:
docker system df -v
That lists every image, container, and volume with its size, so you can spot the 6 GB image you forgot about or the volume that’s grown out of control.
Prune the safe things first
Work from lowest risk to highest. The first two commands almost never cause problems.
Remove dangling images — layers with no tag, left behind by rebuilds:
docker image prune
Remove the build cache, which is often the biggest single consumer on a build server:
docker builder prune
Remove stopped containers:
docker container prune
Each prompts for confirmation and reports how much it freed. If you want to clear all unused images, not just dangling ones, add -a — but be aware that pulls everything back next time you need it:
# Removes every image not used by a container — more aggressive
docker image prune -a
Prune commands by risk
| docker image prune | Low risk. Removes dangling (untagged) images only. |
|---|---|
| docker builder prune | Low risk. Clears build cache. Slower next build, no data loss. |
| docker container prune | Low risk. Removes stopped containers. Running ones are untouched. |
| docker image prune -a | Medium. Removes all unused images; they re-pull when needed. |
| docker volume prune | High. Deletes unused volumes — this is real data. |
| docker system prune --volumes | Highest. Everything above plus volumes, in one shot. |
The one command that deletes data
docker system prune is the convenient catch-all. By itself it removes stopped containers, unused networks, dangling images, and build cache — and it leaves volumes alone:
docker system prune
The dangerous variant adds --volumes:
# This DELETES unused volumes — and their data
docker system prune --volumes
The word “unused” is the trap. A volume is “unused” if no running or stopped container is currently attached to it. A named volume from a database container you removed last week counts as unused — and pruning it deletes the database. People run --volumes to free space, not realizing their data volumes qualify.
Cap your container logs
If docker system df shows modest image/volume usage but the disk is still full, the culprit is often logs. The default json-file logging driver stores everything a container writes to stdout/stderr, with no size limit. A container that logs a line per request can produce gigabytes over weeks.
Find the offenders. On Linux, log files live next to each container’s data:
sudo du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail
The fix is to enable log rotation. Set it globally in the daemon config so every new container inherits a cap:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Then restart Docker to apply it:
sudo systemctl restart docker
That keeps at most three 10 MB files per container — 30 MB max instead of unbounded growth. You can also set it per container at run time:
docker run -d --log-opt max-size=10m --log-opt max-file=3 nginx
Or in Compose:
services:
app:
image: myapp:latest
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
What overlay2 actually is
If you’ve poked around /var/lib/docker, you’ve seen overlay2 and probably panicked at its size. It’s not junk — it’s where Docker stores the layered filesystems.
Every image is a stack of read-only layers. Every running container adds a thin writable layer on top. The overlay2 storage driver assembles these into the filesystem each container sees. So overlay2 growing reflects three things:
- Images you’ve pulled or built (each layer stored once, shared across images).
- Container writable layers — anything a container writes that isn’t in a volume.
- Stopped containers still holding their writable layer.
That second point is a frequent surprise. If an app writes large files to a normal path inside the container instead of to a mounted volume, that data lands in the container’s writable layer inside overlay2, and it grows without showing up as a “volume.” The fix is to mount a volume for data that grows — see bind mounts vs named volumes for where data should actually live.
Docker Desktop is a bit different
On Docker Desktop (Mac/Windows), containers run inside a Linux VM, and all of /var/lib/docker lives in a single virtual disk image on your host. Pruning inside Docker frees space in the VM, but the host-side disk image file doesn’t always shrink automatically.
If the VM disk has grown and won’t release space to your host after pruning, use the Docker Desktop UI — Settings → Resources → Disk — or its purge/reclaim option to compact the disk image. On Windows with WSL2, you may need to compact the WSL virtual disk to return the freed space to Windows.
Set up a maintenance routine
Keep Docker disk usage in check
- Run docker system df monthly to spot growth early
- Enable log rotation in daemon.json (max-size + max-file)
- Schedule docker image prune and docker builder prune on build hosts
- Store all persistent data in named volumes, not container layers
- Never run --volumes prune without checking docker volume ls first
- Back up data volumes before any aggressive cleanup
A simple safe cron job for a build server clears images and cache without ever touching volumes:
#!/usr/bin/env bash
docker container prune -f
docker image prune -af
docker builder prune -af
Note the absence of any --volumes flag — that’s deliberate. This reclaims the disposable space on a schedule while leaving your data alone.
Wrapping up
When Docker fills the disk, resist the urge to delete files by hand. Run docker system df to see where the space went, prune images and build cache first because they’re safe and usually the biggest win, and treat anything with --volumes as a data-destroying command that needs a backup and a second look. Then cap your logs in daemon.json so the disk doesn’t silently refill.
Most “out of disk” incidents trace back to two habits: uncapped logs and data written into container layers instead of volumes. Fix those two and the problem rarely comes back. For the deeper cleanup workflow, see cleaning Docker images, containers, and volumes, and browse more in the Docker & Containers guides.