
Find what's eating your Ubuntu VPS disk, clean logs and Docker leftovers safely, turn on unattended updates, and set a weekly routine that keeps the box healthy.
I've lost count of how many times I've SSH'd into a VPS at some weird hour because a site was down, only to find the real culprit was a full disk. Not a hack. Not a bad deploy. Just logs, old Docker images, and apt cache quietly eating every last megabyte.
If you followed my guides on deploying Next.js on a VPS, Docker rollout, or SSH hardening, you already have something running in production. This post is about keeping that box alive for the long haul — cleanup, maintenance, and a routine that doesn't depend on you remembering to "check the server someday."
I'm writing this against Ubuntu 22.04 / 24.04 in 2026. Commands are the ones I actually run.
Most VPS failures aren't dramatic. They're boring:
Cleanup without a plan is how you delete the wrong folder. Maintenance without a schedule is how you rediscover the same fire every three months. We'll do both.
Don't run destructive cleanup commands on a machine you haven't backed up. Local backups on the same disk don't count when the disk is the thing that fails.
Before deleting anything, figure out whether you're out of space, out of inodes, or both.
df -h
df -ihdf -h is bytes. df -ih is inodes (millions of tiny files — session caches, mail, node_modules leftovers). I've seen disks at 40% space and 100% inodes. Cleaning "big files" does nothing in that case.
Find the heavy directories:
sudo du -xhd1 / | sort -h | tail -n 20
sudo du -xhd1 /var | sort -h | tail -n 20Usual suspects on my boxes:
/var/log — nginx, journald, app logs/var/lib/docker — images, layers, build cache/var/cache/apt — package cache/home or /var/www — "temporary" uploads that never leftFind giant files when you need a smoking gun:
sudo find / -xdev -type f -size +200M -printf "%s\t%p\n" 2>/dev/null | sort -n | tail -n 20Persistent journals are useful. Unlimited journals are a slow outage.
Check current usage:
sudo journalctl --disk-usagePut hard limits in place:
sudo mkdir -p /etc/systemd/journald.conf.d
sudo tee /etc/systemd/journald.conf.d/99-disk-limits.conf >/dev/null <<'EOF'
[Journal]
SystemMaxUse=500M
RuntimeMaxUse=200M
MaxRetentionSec=14day
EOF
sudo systemctl restart systemd-journaldFree space right now without waiting for rotation:
sudo journalctl --vacuum-size=300M
# or
sudo journalctl --vacuum-time=14d500M is plenty for debugging a VPS that runs a couple of apps. Bump it if you actually dig through weeks of logs.
Ubuntu ships with logrotate. Confirm it's alive, then make sure your app logs aren't writing somewhere that never gets rotated.
systemctl status logrotate --no-pager
ls /etc/logrotate.d/
sudo logrotate -d /etc/logrotate.confFor nginx, a sane baseline looks like this:
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -s /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}The important part is postrotate. Without reopening the log file, nginx keeps writing to the old (deleted/rotated) handle and your disk still fills. I've burned an evening on that exact bug.
If an access log is already huge and you're in a pinch:
sudo truncate -s 0 /var/log/nginx/access.logTruncate, don't rm. Deleting an open file frees nothing until the process restarts.
This is the boring stuff that still reclaims a surprising amount of space:
sudo apt update
sudo apt -y upgrade
sudo apt autoremove --purge -y
sudo apt cleanOld kernels pile up after months of unattended upgrades. autoremove usually clears them. If a kernel update landed, reboot on your own schedule — don't let the box sit in a "reboot required" state for weeks:
[ -f /var/run/reboot-required ] && cat /var/run/reboot-required.pkgsI want security patches applied automatically. I do not want the VPS rebooting itself at 3am while a deploy is halfway done.
sudo apt -y install unattended-upgrades
sudo tee /etc/apt/apt.conf.d/20auto-upgrades >/dev/null <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
sudo tee /etc/apt/apt.conf.d/52unattended-upgrades-local >/dev/null <<'EOF'
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
EOF
sudo systemctl enable --now unattended-upgradesCheck that it actually runs:
systemctl status unattended-upgrades --no-pager
sudo unattended-upgrade --dry-runYou still reboot manually when /var/run/reboot-required shows up — ideally inside a maintenance window you chose.
If you use the docker-rollout deploy flow, leftover images and build cache are the second biggest disk hog after logs.
Safe cleanup — dangling stuff only:
docker container prune -f
docker image prune -f
docker builder prune -f --filter until=168hWhat I deliberately avoid:
# Too aggressive — deletes base images you still need (oven/bun, node, etc.)
docker image prune -af
# Wipes every cache mount. Next build is cold. Painful.
docker builder prune -afSee what's eating Docker space:
docker system df
sudo du -xhd1 /var/lib/docker | sort -h | tail -n 20Also cap container logs so one noisy container can't fill the disk. In /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}Then restart Docker:
sudo systemctl restart dockerRestarting Docker restarts your containers. Do this in a quiet window, not mid-traffic spike.
Clear old temp and crash junk:
sudo find /tmp -type f -atime +7 -delete
sudo find /var/tmp -type f -atime +14 -delete
sudo rm -rf /var/crash/*If df still looks full after deletions, something is holding deleted files open:
sudo lsof +L1 | head -n 40Restart the offending process (or the whole service) and the space comes back. This one gets people because "I deleted the log" and df doesn't move.
I run this whenever I SSH in. Takes less time than checking Twitter.
df -h
free -h
systemctl --failed --no-pager
sudo ss -lntup
timedatectlWhat you're looking for:
systemctl --failedSystem clock synchronized: yes) — TLS and cron get weird when it's notIf you're on a 1–2GB RAM VPS and free -h shows no swap, add some. Random OOM kills are miserable to debug:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl -p /etc/sysctl.d/99-swappiness.confDoing this by hand once is fine. Doing it every week by hand is how it stops happening. Drop a small script and a cron job.
#!/usr/bin/env bash
set -euo pipefail
echo "[$(date -Is)] weekly cleanup start"
apt-get autoremove --purge -y
apt-get clean
journalctl --vacuum-size=300M >/dev/null 2>&1 || true
find /tmp -type f -atime +7 -delete 2>/dev/null || true
find /var/tmp -type f -atime +14 -delete 2>/dev/null || true
rm -rf /var/crash/* 2>/dev/null || true
if command -v docker >/dev/null 2>&1; then
docker container prune -f >/dev/null 2>&1 || true
docker image prune -f >/dev/null 2>&1 || true
docker builder prune -f --filter until=168h >/dev/null 2>&1 || true
fi
echo "[$(date -Is)] disk after cleanup:"
df -h /
echo "[$(date -Is)] weekly cleanup done"Make it executable and schedule it for Sunday early morning (server time):
sudo chmod +x /usr/local/bin/vps-weekly-cleanup.sh
sudo tee /etc/cron.d/vps-weekly-cleanup >/dev/null <<'EOF'
15 3 * * 0 root /usr/local/bin/vps-weekly-cleanup.sh >> /var/log/vps-weekly-cleanup.log 2>&1
EOFRun it once by hand so you trust the output:
sudo bash /usr/local/bin/vps-weekly-cleanup.shCleanup keeps the disk healthy. Backups keep you employed when the disk dies anyway.
Minimum that I actually trust:
.env + nginx/systemd configsIf you run Laravel, I already wrote about daily backups to Google Drive. Same idea applies to anything else: retention policy, off-site copy, and a restore test once in a while. A backup you've never restored is a rumor.
Weekly (or let cron do most of it)
df -h and systemctl --failed/var/log/vps-weekly-cleanup.logMonthly
docker system df/var/run/reboot-required exists, during a quiet windowQuarterly
ss -lntuprm on an active log does nothing useful. Use truncate -s 0 or fix logrotate's postrotate.docker system prune -af feels satisfying until the next deploy re-pulls every base image and rebuilds cold. Prefer the gentler prune commands above.df -ih once before you spiral.Automatic-Reboot "false" and reboot yourself./var/lib/docker with rm -rf. Use Docker's own prune tools or you'll corrupt the daemon state.A VPS doesn't need constant babysitting. It needs a few hard limits (journald, Docker logs, logrotate), a weekly cleanup that runs without you, security updates that don't reboot on their own schedule, and backups that live somewhere else.
Do the manual pass once — disk audit, journal caps, unattended upgrades, Docker log limits — then let the cron script handle the boring part. The goal isn't a perfectly empty disk. It's a box that stays boring for months at a time, which is exactly what you want from production.