SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor

How to Free VPS Disk Space Wey Docker Dey Use

VPS disk full? Use docker system df to find images, containers, build cache or volumes wey hold space, then prune only wetin safe without losing data.

Find wetin dey use the disk space before you prune anything

Docker dey use disk space for VPS in four places: images, stopped containers, build cache, and local volumes. Run docker system df first to find which one hold the space, then run the narrowest prune wey go clear am. Order matter, because the last command for this guide, docker volume prune -a, dey delete data and you no fit undo am.

Start with the filesystem, no be Docker.

df -h /
sudo du -xh --max-depth=1 /var/lib/docker | sort -h

df go show you how serious the problem be. du go show you where the space go. The -x flag make du stay for one filesystem, so e no go follow mount enter separate volume and count am twice. Five directories matter here: overlay2 hold image and container layers, volumes hold volume data, containers hold container metadata and log files, buildkit hold the build cache, and image hold layer metadata.

Make we talk about sudo and shell wildcards, because dis one dey waste plenty people time. /var/lib/docker belong to root and your normal user no fit read am, so ls /var/lib/docker go return Permission denied. Command like sudo du -sh /var/lib/docker/* go fail too, because your shell go expand * before sudo ever run, and your shell no fit read that directory. Every command below use find or --max-depth instead of wildcard for exactly that reason.

Now make we check Docker own view.

docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          24        6         12.4GB    8.91GB (71%)
Containers      31        6         1.42GB    1.39GB (97%)
Local Volumes   14        5         6.03GB    4.11GB (68%)
Build Cache     212       0         9.87GB    9.87GB

Those figures come from one machine and dem no tell you anything about your own machine. Read the shape instead. TOTAL dey count objects, ACTIVE dey count the ones wey dey use now, and RECLAIMABLE na Docker estimate of wetin prune fit free from that row.

Two things about RECLAIMABLE dey catch people. E dey count shared image layers once for every image wey use dem, so the image row usually promise more than wetin you go actually get. And e never include container log files, because Docker no treat log file as object wey fit reclaim. When du report directory wey much bigger than wetin docker system df show, na log files cause am, and one section below explain this.

Add -v for breakdown of each object.

docker system df -v

This one split the summary into one section for each object type. The image section add SHARED SIZE and UNIQUE SIZE columns, so you fit see the real cost of one image. The volume section add a LINKS count, wey be the number of containers attached to that volume. Remember LINKS, because value of 0 na the complete test wey volume prune commands dey apply.

Dangling images and unused images

These two terms no be interchangeable. Filters dey behave differently because the objects no be the same.

Dangling image na image wey no get tag. E dey show as <none> for docker images. You create one for every rebuild: docker build -t myapp:latest . moves the myapp:latest tag go the new image, while the old image keep all im layers but lose im name. Nothing dey reference am, and nothing dey clean am up by itself.

Unused image na any image, whether e get tag or not, wey no container currently dey refer to. A postgres:16 wey you pull last month and you no dey run now na unused image, but e no be dangling image.

docker image prune        # dangling images only
docker image prune -a     # every image no container refers to

The second one go ask you first.

WARNING! This will remove all images without at least one container associated to them.
Are you sure you want to continue? [y/N]

Read that prompt well. "Associated to them" mean an existing container object, whether e dey run or e stop. If you run docker compose down, the containers don comot, so every image wey those services use don become unused, and -a go delete all of dem. You no lose anything wey you no fit get back, but the next docker compose up -d go pull or rebuild everything again. This go use bandwidth and build time for small VPS. Na one practical reason be this to know wetin docker compose down dey remove and wetin stop dey leave running before you prune anything.

Filter fit keep recent images out.

docker image prune -a --filter "until=240h"

That one go remove unused images wey older than 240 hours (10 days), and leave newer ones alone. The until value dey take a Go duration string like 240h, or an absolute timestamp such as 2026-08-01T00:00:00.

Wetin build cache be and why e dey grow without limit

BuildKit na the builder wey Docker dey use by default for docker build and docker compose build since Docker Engine 23.0. E dey cache the result of every step for every Dockerfile wey e run, and e dey keep that cache for /var/lib/docker/buildkit. Na the cache make your second build finish within seconds, so e dey do im work. The problem be say nothing dey expire the old entries by default. If you build the same image fifty times with a COPY step wey dey change every time, you go keep fifty sets of layers.

docker image prune no dey show the build cache. E na separate object type wey get im own command.

docker builder prune                        # dangling cache
docker builder prune -a                     # all unused cache
docker builder prune --filter until=168h    # cache untouched for 7 days

None of these go touch your images or your data. The only cost of clearing the build cache na say the next build go run slowly one time. For VPS wey dey rebuild images regularly, Build Cache often na the biggest row for docker system df, so na the safest big thing wey you fit delete.

Prune commands, wey dey move from safe reach destructive

Work down this list, and stop immediately df -h / don look healthy again. Every command go print one Total reclaimed space: line when e finish.

  1. docker container prune dey remove containers wey don stop. E go remove their writable layers too, so anything wey container write outside a volume go delete with am. E no dey touch volumes.
  2. docker image prune dey remove only dangling images. This na the safest image command.
  3. docker builder prune dey remove dangling build cache. The cost na one slow build.
  4. docker image prune -a dey remove every image wey no container dey refer to. The cost na to pull am again or rebuild am.
  5. docker system prune dey do the first three together and add unused networks.
  6. docker volume prune dey remove unused anonymous volumes.
  7. docker volume prune -a dey remove unused volumes, including named ones. Na this command dey delete databases.

docker system prune go state its own scope before e run.

WARNING! This will remove:
        - all stopped containers
        - all networks not used by at least one container
        - all dangling images
        - unused build cache
Are you sure you want to continue? [y/N]

We leave volumes out of that list deliberately. If you add --volumes, anonymous volumes go enter the scope again. If you add -a, the image step go expand from dangling images reach every unused image. Full docker system prune -a --volumes -f for production host na how people dey lose data when dem dey try free space.

Why pruning volumes fit delete your database

Dis na the section wey you suppose read twice.

A volume count as unused when no container attach to am. Na the complete test be that. Docker no check whether the volume empty, whether compose file still declare am, or whether na the only copy of your database e dey hold. LINKS 0 for docker system df -v mean say e fit prune, and na only that e mean.

Now put two normal actions one after another. You run docker compose down to restart a stack cleanly. E remove the containers and leave the named volumes for place, exactly as the documentation talk say e go do. Your Postgres volume don attach to nothing now. Ten minutes later, you run docker volume prune -a to free space, and the database don disappear. Both commands work correctly. Na the sequence destroy the data.

Since Docker Engine 23.0 (API version 1.42), the plain command no dey cover as much as e used to.

WARNING! This will remove anonymous local volumes not used by at least one container.

An anonymous volume na one wey Docker create for you, usually because an image declare VOLUME and you no give am name. Those ones normally hold data wey you no ask to keep. Docker only remove a named volume, the kind wey you write for your compose file, when you add -a. Older Docker versions remove both with the plain command, so no trust habits wey you form for a machine wey you don upgrade since. You go only understand the difference after you know how named volumes dey differ from bind mounts, because bind mount no be Docker volume at all, and no prune command go ever touch am.

Check before you delete. Replace myapp_pgdata with the volume name wey you dey check.

docker volume ls -f dangling=true
docker volume inspect myapp_pgdata
sudo ls -la /var/lib/docker/volumes/myapp_pgdata/_data

The dangling=true filter for a volume mean say nothing dey reference am, e no mean say the volume empty. Listing _data go show you wetin really dey inside. If you see pgdata or mysql directory inside, stop and take a copy before you continue. The same data loss fit happen through docker compose down -v, wey remove every volume wey the compose file declare and no ask you anything first.

A volume na the one thing for Docker host wey rebuild no fit recreate. Na why volume data suppose dey inside a restic backup wey dey run outside the server, where wrong flag no fit reach am.

When nothing prunes: container log files

You don prune everything, docker system df show say almost nothing fit free, but disk still full. Check the logs.

sudo find /var/lib/docker/containers -name '*-json.log' -exec du -h {} + | sort -h | tail -10

Every container dey write e standard output and standard error inside JSON file for /var/lib/docker/containers/. For default install, max-size no set. That mean e get no limit. So one container wey dey stuck for crash loop fit write until partition full. No prune command dey remove these files, because the containers wey dey produce dem still dey run. By definition, dem no fit prune.

No delete the file. If you run rm on log file wey still open, e no free anything. Docker daemon still hold open file descriptor, and kernel keep those blocks allocated until that handle close. df no go move at all. Truncate the file instead. This one keep the same inode and allow the daemon continue to write.

sudo find /var/lib/docker/containers -name '*-json.log' -exec truncate -s 0 {} +
df -h /

Na temporary fix be that. docker logs for those containers go now return nothing, and the files go start grow again immediately. The real fix na rotation, wey dey for the next section.

Measure before and after, every time

No dey guess wetin prune do. Take one reading, run one command, then take another reading.

df -h /
docker system df
docker builder prune -f --filter until=168h
docker image prune -f
docker system df
df -h /

Compare the two df outputs. Na only this number decide whether your server go continue to serve. docker system df go then tell you which row actually move, and every prune go print im own Total reclaimed space: figure.

If df no move but docker system df say space don free, one open file handle dey hold deleted blocks. Na the log file problem wey we talk above be this. If both move and disk fill up again within one day, na growth problem be this, no be cleanup problem. The answer na rotation plus one scheduled job.

How to stop disk from full up again

Set log size limit. Create or edit /etc/docker/daemon.json.

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

This one go limit each container to 30 MB logs. Every value under log-opts must be string, including the numeric ones. Check whether the file parse correctly before you restart, because malformed daemon.json go stop the daemon from starting at all and carry every container down with am.

python3 -m json.tool /etc/docker/daemon.json
sudo systemctl restart docker
docker info | grep -i 'logging driver'

docker info suppose report Logging Driver: json-file now. The limits themselves go show for LogConfig section of docker inspect on container wey you create after the restart. Na this detail matter: this setting apply only to new containers. Existing containers keep the configuration wey dem build with, so recreate dem.

docker compose up -d --force-recreate

You fit put the same limit per service inside compose file. This one better when one noisy service need im own number.

services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Schedule small prune. Run am weekly, and scope am to dangling images and old build cache. Never put -a or --volumes inside scheduled job, because if the job run while stack dey down, e go delete that stack images. With --volumes, e go start work on your data.

sudo tee /etc/cron.weekly/docker-prune >/dev/null <<'EOF'
#!/bin/sh
docker image prune -f
docker builder prune -f --filter until=168h
EOF
sudo chmod +x /etc/cron.weekly/docker-prune
sudo /etc/cron.weekly/docker-prune

That last line run the script once by hand, so you fit see the output before e ever run unattended. The file must be executable, and im name must not contain dot, because run-parts skips anything wey no be executable and anything wey carry extension.

Set alarm for free space. Prune wey you run after disk don full na recovery. Alert for 80 percent na prevention.

usage=$(df --output=pcent / | tr -dc '0-9')
[ "$usage" -ge 80 ] && echo "root filesystem at ${usage} percent full"

Put am for cron with any notifier wey you already dey use. Free space na only half of the matter, so join the alarm with disk health monitoring for your VPS, because failing disk and full disk both fit stop your containers, but each one need different fix.

Everything above assume say na standard install you get, with data root for /var/lib/docker. If you move am with data-root key inside daemon.json, replace the path for every command. Getting this layout correct on fresh box na part of setting up Docker on VPS, and e much easier to decide before 40 GB of containers dey sit for wrong partition.

FAQ

Docker system prune dey delete my volumes?

No. The plain command dey remove stopped containers, unused networks, dangling images, and unused build cache. E confirmation prompt list exactly those things. Volumes only enter when you add --volumes. Since Docker Engine 23.0, that flag dey cover anonymous volumes, no be named ones. docker volume prune -a and docker compose down -v dey remove named volumes. Na these two commands you must handle with care.

Why my disk still full after I run docker prune?

Two common causes dey. The first one na container log files under /var/lib/docker/containers/. No prune command dey touch dem, and dem go keep growing without limit until you set max-size. The second one na deleted file wey process still hold open. If you remove log with rm while e container still dey run, the daemon keep the file descriptor. The kernel no release the blocks, so df report no change. Compare sudo du -xh --max-depth=1 /var/lib/docker with docker system df to see which one dey affect you.

Wetin be the difference between docker image prune and docker image prune -a?

The plain command dey remove dangling images only. That means images wey lose their tag, almost always because of rebuild. The -a form dey remove every image wey no existing container dey refer to, including tagged images wey you deliberately pull. After docker compose down, the containers don go, so -a go carry that stack images comot too. Nothing lose permanently, because the next start go pull or rebuild dem again. But if the link slow, you go wait long.

How I fit stop Docker logs from filling the disk?

Set max-size and max-file under log-opts inside /etc/docker/daemon.json. Then restart the daemon with sudo systemctl restart docker. The setting only apply to containers wey you create after that restart. So recreate the ones wey dey run with docker compose up -d --force-recreate. You fit set the same two options for each service inside compose file under a logging key. Use this when one service dey produce far more logs than the others.

E safe to run docker system prune inside cron job?

The plain docker system prune -f safe for host where every stack dey stay up. But e dey remove stopped containers, so e go delete container wey you stop intentionally and plan to restart later. The safer scheduled job na docker image prune -f plus docker builder prune -f --filter until=168h. E free the two resources wey dey grow fastest, and e no fit touch volume. Never schedule -a or --volumes.