Reclaiming Disk Space on a Full VPS: A Measured Procedure with du, lsof and Journald Limits

A full disk on a production VPS rarely announces itself politely. Services fail to write logs, databases refuse connections, and the first symptom is usually a generic 500 with no useful message. This is a methodical reclaim procedure: quantify what is large, decide what is safe to delete, and verify the space actually came back. Every step below is measured, not guessed.

Step 0: Confirm It Is Really a Space Problem

Two different failures look identical from the application side. Run both checks before deleting anything.

df -hT /
df -i /
stat -f -c 'blocks free: %f  files free: %d' /

If df -h shows free space but df -i shows 100 percent inode usage, the fix is finding millions of tiny files, not large ones. If both show space, check for deleted-but-open files, a classic cause of 100 percent usage that du cannot see.

lsof -nP +L1 | sort -k7 -n | tail -20

Any process holding megabytes of deleted files will not release them until restarted or reloaded (SIGHUP often works for daemons). Restarting the offending process returns the space instantly with no deletion risk.

Step 1: Attribute Usage by Directory, Sorted

Use du with a depth limit so it stays fast, then drill down only into the large branches.

du -x -d1 -h / 2>/dev/null | sort -h | tail -15
# then, for the biggest branch:
du -x -d1 -h /var 2>/dev/null | sort -h | tail -15

The -x flag keeps the walk on one filesystem, which prevents du from wandering into mounted volumes and misattributing their size. In practice four locations account for the vast majority of runaway usage on small VPS instances.

LocationTypical culpritSafe reclaim methodRisk
/var/log/journalUnbounded systemd journaljournalctl --vacuum-size=200MNone
/var/logRotated but uncompressed logslogrotate -f /etc/logrotate.confLow
/var/lib/dockerDangling images, stopped containersdocker system prune -aMedium
/var/lib/mysqlBinary logs / relay logsPURGE BINARY LOGS BEFORE ...High

Step 2: Cap Journald So It Cannot Regrow

Vacuuming once fixes today. Editing the config fixes every month after that.

mkdir -p /etc/systemd/journald.conf.d
cat >/etc/systemd/journald.conf.d/99-cap.conf <<'EOF'
[Journal]
SystemMaxUse=300M
SystemMaxFileSize=50M
MaxRetentionSec=2week
EOF
systemctl restart systemd-journald
journalctl --disk-usage

On a 25 GB VPS, a 300 MB journal cap is generous while still leaving weeks of history for post-incident forensics.

Container and Layer Bloat

Docker is the single most common silent consumer on a small VPS. Each image pull leaves an intermediate layer set, and stopped containers retain their writable layer. Inspect before you prune, because prune -a will remove images you still need.

docker system df -v | head -40
docker image ls --format '{{.Repository}}:{{.Tag}}\t{{.Size}}' | sort -k2 -h
docker container ls -a --size --format '{{.Names}}\t{{.Size}}'
# reclaim only dangling layers first
docker image prune -f
docker builder prune -f --filter until=168h

On a host that builds images, the build cache usually dwarfs the images. Cap it in the daemon config so it cannot grow without bound:

cat >/etc/docker/daemon.json <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
EOF
systemctl restart docker

The log-opts block matters more than the prune: unbounded container stdout logs are how /var/lib/docker silently doubles in a month on a busy host.

Finding the Million-File Case

When df -i is the number that is full, aggregate by directory count rather than size. A PHP session directory, a mail queue, or a web cache with a broken eviction policy can hold millions of 4 KB files.

find / -xdev -type d -printf '%p\n' 2>/dev/null | while read d; do
  n=$(find "$d" -maxdepth 1 -type f 2>/dev/null | wc -l)
  [ "$n" -gt 5000 ] && echo "$n $d"
done | sort -rn | head -20

Step 3: Find Duplicate or Stale Large Files

  • Files over 200 MB: find / -xdev -type f -size +200M -printf '%s %p\n' | sort -n | tail -30
  • Core dumps: find / -xdev -name 'core*' -size +10M and disable with sysctl -w kernel.core_pattern='|/bin/false' if you do not collect them.
  • Old kernels: apt-get autoremove --purge on Debian/Ubuntu reclaims hundreds of megabytes safely.
  • Package cache: apt-get clean and dnf clean all.

Step 4: Verify the Reclaim, Then Add a Guard Rail

Always re-run df -h after each action rather than batching everything and hoping. Record a before/after table in your runbook so the next operator knows what worked. Finally, install a simple threshold alert so you never discover a full disk through a customer again - a five-line systemd timer writing to your monitoring endpoint is enough, and our VPS comparison table flags plans that include disk-space alerting by default.

If reclaiming space becomes a monthly chore rather than an incident, the underlying plan is simply too small. The sizing breakdown in the VPS guides hub walks through matching disk allocation to workload, and the plan table at our VPS comparison table lists overcommit ratios per tier so you can see what you are actually buying.

Leave a Reply