VPS Disk Space Forensics: The Complete Guide to Finding and Reclaiming Storage on a Small VPS

A 20-40 GB VPS disk fills up faster than you expect. One day df -h shows 100%, apt refuses to run, MySQL stops writing, and your site starts returning random 500s. The fix is rarely “upgrade to a bigger plan” — it is methodically finding what you forgot about. This guide covers the full forensic routine: from the initial panic to long-term prevention, with tools and techniques for every Linux distribution.

Phase 1: Diagnose the Problem

When disk space vanishes, start with three commands that rule out the most common false leads:

# Check actual disk usage vs. inode exhaustion
df -h /
df -i /

# Check for deleted files still held open by processes
sudo lsof +L1 | head -20

# Find the biggest directories (one filesystem only)
sudo du -xhd1 / | sort -hr | head -20

1. Inode Exhaustion

If df -i shows 100% but df -h shows free space, you have run out of inodes. The filesystem cannot create new files even though space exists. This is common on VPS images with small default inode tables. The culprit is usually a directory full of tiny files: mail spools (/var/mail), PHP session files (/var/lib/php/sessions), or Docker overlay filesystems. Deleting files is the only fix — ext4 cannot add inodes after creation.

2. Deleted Files Held Open

A process (commonly a logging daemon, database, or web server) may keep a file open after it has been deleted. The kernel does not release the disk blocks until the file descriptor is closed. lsof +L1 lists these hidden consumers. Restart the owning process to release the space — do not just kill the PID without understanding what it is.

3. Rapid Space Survey with ncdu

For interactive exploration, ncdu is the best tool. It walks the filesystem, shows directory sizes in a sortable interface, and lets you delete with a single keypress. Install it, run it on the root filesystem, and drill into the largest directories:

sudo apt install ncdu   # Debian/Ubuntu
sudo dnf install ncdu   # Fedora/RHEL
sudo ncdu -x /

Phase 2: Attack the Usual Suspects

systemd Journal (the #1 Hidden Consumer)

On modern Linux distributions, journald stores logs in a binary format that is not managed by logrotate. It can easily consume several gigabytes. Check and cap it:

# See current usage
journalctl --disk-usage

# Shrink immediately
sudo journalctl --vacuum-size=200M

# Cap future growth
# Edit /etc/systemd/journald.conf:
# SystemMaxUse=200M
# MaxFileSec=1week
sudo systemctl restart systemd-journald

Package Cache and Old Kernels

APT and DNF keep downloaded package files in a cache. Old kernels accumulate in /boot and are never removed automatically on most distributions:

# Debian/Ubuntu
sudo apt clean
sudo apt autoremove --purge

# Check installed kernels
dpkg --list | grep linux-image

# Fedora/RHEL
sudo dnf clean all
sudo dnf autoremove

Docker and Container Overhead

Docker images, build cache, and container logs grow without bound unless you enforce limits. Run a prune and configure log rotation:

# Check usage
docker system df

# Prune everything (caution: removes unused images, containers, volumes)
sudo docker system prune -af --volumes

# Cap container logs in /etc/docker/daemon.json:
# {
#   "log-driver": "json-file",
#   "log-opts": {
#     "max-size": "10m",
#     "max-file": "3"
#   }
# }

Database Binary Logs and WAL Files

MySQL/MariaDB binary logs and PostgreSQL WAL segments accumulate without bound when retention is misconfigured:

# MySQL: show and purge old binary logs
mysql -e "SHOW BINARY LOGS;"
mysql -e "PURGE BINARY LOGS BEFORE NOW() - INTERVAL 2 DAY;"

# PostgreSQL: check WAL directory size
sudo du -sh /var/lib/postgresql/*/pg_wal/

Snap Packages (Ubuntu Only)

Snap keeps old revisions of every installed package. A handful of snaps can quietly eat several gigabytes:

# List all revisions
snap list --all

# Limit retained revisions
sudo snap set system refresh.retain=2

# Remove old revisions
sudo snap remove --revision OLD_REVISION package-name

Application-Specific Caches

Common cache directories that grow unbounded: Composer (~/.cache/composer), npm (~/.npm/_cacache), pip (~/.cache/pip), and PHP sessions (/var/lib/php/sessions). Set up a cron job to clear caches older than 30 days, or configure tmpwatch / tmpreaper for automatic cleanup.

Phase 3: Configure Long-Term Prevention

Three guardrails prevent 90% of recurrence:

  1. Cap journald at 200 MB as shown above.
  2. Set up disk usage alerts — a simple cron check that emails or pushes a notification when disk exceeds 85%:
#!/bin/bash
# /usr/local/bin/disk-alert.sh
THRESH=85
USE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USE" -ge "$THRESH" ]; then
  echo "Disk ${USE}% full on $(hostname)" | mail -s "Disk Alert" [email protected]
fi
# Add to crontab: 0 * * * * /usr/local/bin/disk-alert.sh
  1. Automate log rotation — verify that logrotate runs daily, and add custom configs for any service that writes logs outside the standard paths.

The Last Resort: What to Do When You Cannot Free Enough Space

If you have pruned everything and still sit at 80%+ usage, you have three options:

  • Move data to block storage. Attach a separate volume for databases, logs, or user uploads. Most VPS providers let you add storage volumes without changing plans.
  • Offload static assets. Use an object storage service (S3, B2, or similar) for images, backups, and archives. Mount it with rclone.
  • Upgrade the plan. If your workload genuinely needs more space, compare storage specs before you pay. SSD vs. NVMe, IOPS guarantees, and provisioning matter — the VPS provider comparison table breaks down storage types and sizes across providers.

Finally, run sudo fstrim -av weekly (or enable the fstrim.timer systemd service) so the SSD keeps reclaiming freed blocks. This does not create space, but it prevents a nearly-full disk from getting slower over time as the SSD controller struggles to find reusable blocks.

Leave a Reply