Docker Filling Up Your VPS Disk? Full Cleanup Guide
Your VPS was fine last month. Now df -h shows the root partition at 95% and you haven't deployed anything new. If you're running Docker, there's a good chance the daemon is quietly hoarding gigabytes in old image layers, dead containers, and log files that never get truncated. Here's where it actually goes and how to clean it up without taking a container down.
Symptom: disk fills up, but 'du' on your app folder looks fine
The giveaway is that regular disk-usage checks don't add up. You run du -sh /var/www or check your project folder and it's a few hundred MB, but df -h / says you're nearly out of space. That gap almost always means something outside your normal file tree is the culprit — and on a Docker host, that's /var/lib/docker.
You'll usually notice it one of these ways:
docker buildordocker pullfailing withno space left on device- A container that won't start, with logs mentioning a write failure
- MySQL or another database container crashing mid-write
- Deploys via
docker compose up -dhanging or timing out
Confirm it's Docker before you go digging elsewhere:
df -h /
docker system df
docker system df -v
docker system df breaks usage down by images, containers, local volumes, and build cache — this is the fastest way to see which bucket is actually the problem instead of guessing.
Cause: where Docker actually puts your disk space
Docker doesn't delete anything by default. Every build, every pull, every stopped container leaves something behind on disk until you explicitly tell it to clean up.
| Where it lives | What's there | Why it grows unnoticed |
|---|---|---|
/var/lib/docker/overlay2 | Image layers and container writable layers | Old image versions and dangling layers from every rebuild stay on disk after a new one replaces them |
/var/lib/docker/containers/<id>/*.json.log | Container stdout/stderr logs | The default json-file log driver has no size cap unless you set one |
/var/lib/docker/volumes | Named volumes (databases, uploads) | Volumes from removed containers stick around unless you pass -v on removal |
| Build cache | Layers cached by docker build / BuildKit | Every CI run or local rebuild adds more, and it's rarely cleared |
The single biggest offender on most VPS boxes we look at is unbounded container logs. A Node app that logs every request, running for six months without a size cap, can easily produce a multi-gigabyte log file that du -sh on your project won't ever show you.
Check log sizes directly
du -h $(docker inspect --format='{{.LogPath}}' $(docker ps -q)) 2>/dev/null | sort -rh
If one of these is several hundred MB or more, you've found a real chunk of your missing space.
Fix: clean up safely without breaking running containers
Don't reach for rm -rf /var/lib/docker — that nukes everything, including volumes with real data in them. Use Docker's own cleanup commands, which are safe by design: they never touch anything a running container or a named volume in use depends on.
1. Remove dangling images and stopped containers first
docker container prune -f
docker image prune -f
This clears exited containers and untagged (dangling) image layers — the safest cleanup, since nothing currently in use gets touched.
2. Remove unused images, not just dangling ones
If you've got old tagged versions sitting around from previous deploys (myapp:v1.2, myapp:v1.3, etc.), a plain prune won't remove them. Go further:
docker image prune -a -f
This removes every image not currently used by a running container. Fine on a production host where you deploy fresh images each release — just make sure nothing depends on pulling a cached older tag first.
3. Clear build cache
docker builder prune -f
# or, if disk space is critical:
docker builder prune -a -f
BuildKit's cache can be surprisingly large if you build images on the VPS itself rather than in CI. This is usually reclaimable with zero risk since it only affects future build speed, not anything running.
4. Volumes — be careful here
docker volume ls -f dangling=true
docker volume prune -f
Only run volume prune after checking the list it would remove. Named volumes from a docker compose down (without -v) are usually still attached to a compose project and won't show as dangling, but anonymous volumes from one-off docker run commands often are — and those can hold database data you actually want. When in doubt, list first, delete individually.
5. The one-liner, once you trust it
docker system prune -a --volumes -f
This is the nuclear option: dangling and unused images, stopped containers, unused networks, and unused volumes, all in one pass. Run the individual commands above first a few times so you know what's actually being removed before you trust this one on a production box.
Stop logs from refilling the disk
Cleanup only buys you time if nothing's capping log growth. Set a size limit on the default logging driver in /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
sudo systemctl restart docker
This caps each container's log at 10MB across 3 rotated files (30MB max per container) instead of growing forever. One catch: this only applies to new containers created after the restart — existing containers keep their old, uncapped log driver until you recreate them (docker compose up -d --force-recreate does it cleanly).
Prevention: keep it from creeping back up
- Automate cleanup on a schedule. A weekly cron running
docker system prune -f(skip-ain automation unless you're confident nothing needs those cached images) keeps dangling layers from accumulating between manual checks. - Set the log cap in daemon.json before you deploy your first container, not after you hit 100% disk.
- Use multi-stage builds so your final images don't carry build tools and intermediate layers that never get used at runtime.
- Watch
docker system dfalongsidedf -hin your monitoring, not just root partition usage — it tells you which bucket is growing before it becomes an emergency. - Tag and prune deliberately. If your CI/CD pushes a new image tag on every commit, add a retention policy (keep last 5, delete older) instead of letting tags accumulate indefinitely.
Once the log driver is capped and a prune job runs weekly, Docker's disk footprint on a VPS stays predictable instead of creeping up until something crashes.
Frequently asked questions
Is it safe to run 'docker system prune -a --volumes' on a production server?
Run the individual prune commands (container, image, builder) first so you can see exactly what gets removed. Volume pruning is the riskiest part — always run 'docker volume ls -f dangling=true' first and check the list before deleting, since a volume you think is unused might still hold data from a container that isn't currently running.
Why does 'du -sh' on my project folder not show the disk usage Docker is using?
Docker stores images, containers, and volumes under /var/lib/docker, completely separate from your application code. Checking your project directory will never reveal Docker's disk usage — use 'docker system df' instead to see usage broken down by images, containers, volumes, and build cache.
I set the log size limit in daemon.json but my old container's log is still huge. Why?
The log-opts setting in daemon.json only applies to containers created after you restart Docker. Existing containers keep using whatever log driver settings they were started with. Recreate the container (docker compose up -d --force-recreate, or docker rm and docker run again) to pick up the new limit.
Will 'docker image prune -a' delete images I still need?
No — it only removes images that aren't referenced by any container, running or stopped. If an image is currently in use, or a stopped container still references it, it stays. The risk is losing a cached older tag you might want to roll back to quickly, since prune doesn't know about your deployment history, only what's currently attached to a container.
How often should I run Docker cleanup on a VPS?
A weekly cron job with 'docker container prune -f' and 'docker image prune -f' is enough for most single-app VPS setups. If you build images directly on the server or deploy multiple times a day, run it more often, or add 'docker builder prune -f' to the same job since build cache grows fastest on frequent rebuilds.