
Server Maintenance Checklist for 2026
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.
Why VPS Boxes Die Quietly
Most VPS failures aren't dramatic. They're boring:
- Disk hits 100% → nginx/MySQL start failing → SSH feels broken
- journald and app logs grow forever (it's almost always logs)
- Docker images and build cache pile up after every deploy
- Security updates get ignored until something urgent forces a reboot
- Tiny RAM + no swap → random OOM kills at peak traffic
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.
Prerequisites
- An Ubuntu/Debian VPS you can SSH into with sudo
- Roughly 10–15 minutes the first time
- Optional but common: Docker if you deploy containers
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.
Step 1: See What's Actually Full
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/homeor/var/www— "temporary" uploads that never left
Find 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 20Step 2: Cap journald Before It Owns the Disk
Persistent 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.
Step 3: Make logrotate Do Its Job
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.
Step 4: Safe Package and Kernel Cleanup
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.pkgsStep 5: Turn On Unattended Security Updates (Without Surprise Reboots)
I 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.
Step 6: Docker Cleanup (If You Deploy Containers)
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.
Step 7: Temp Files, Crash Reports, and Deleted-but-Open Files
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.
Step 8: A Two-Minute Health Check
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:
- Disk under ~80% (alert yourself earlier than 100%)
- No failed units in
systemctl --failed - Time synced (
System clock synchronized: yes) — TLS and cron get weird when it's not - Only the ports you expect listening
If 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.confStep 9: Automate a Weekly Cleanup Script
Doing 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.shStep 10: Backups Still Beat Perfect Cleanup
Cleanup keeps the disk healthy. Backups keep you employed when the disk dies anyway.
Minimum that I actually trust:
- App code +
.env+ nginx/systemd configs - Database dump on a schedule
- Copies off the VPS (S3, another server, Google Drive — whatever you'll actually check)
If 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.
The Routine I Actually Follow
Weekly (or let cron do most of it)
- Glance at
df -handsystemctl --failed - Confirm unattended upgrades aren't stuck
- Skim
/var/log/vps-weekly-cleanup.log
Monthly
- Review Fail2Ban / auth noise (see the SSH hardening post)
- Check Docker disk with
docker system df - Reboot if
/var/run/reboot-requiredexists, during a quiet window - Confirm off-site backups are recent
Quarterly
- Restore a backup somewhere that isn't production
- Audit sudo users and open ports with
ss -lntup - Drop packages and containers you don't run anymore
Gotchas I Hit So You Don't Have To
rmon an active log does nothing useful. Usetruncate -s 0or fix logrotate'spostrotate.docker system prune -affeels satisfying until the next deploy re-pulls every base image and rebuilds cold. Prefer the gentler prune commands above.- Inodes can be full when space isn't. Always run
df -ihonce before you spiral. - Automatic reboots sound nice until they land mid-deploy. Keep
Automatic-Reboot "false"and reboot yourself. - Backups on the same VPS are not backups. When the disk fills or the provider nukes the instance, those files go with it.
- Don't clean
/var/lib/dockerwithrm -rf. Use Docker's own prune tools or you'll corrupt the daemon state.
Conclusion
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.