Skip to content
Server Maintenance Checklist for 2026
Server

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.

10 min read

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 -ih

df -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 20

Usual 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 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 20

Step 2: Cap journald Before It Owns the Disk

Persistent journals are useful. Unlimited journals are a slow outage.

Check current usage:

sudo journalctl --disk-usage

Put 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-journald

Free space right now without waiting for rotation:

sudo journalctl --vacuum-size=300M
# or
sudo journalctl --vacuum-time=14d

500M 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.conf

For nginx, a sane baseline looks like this:

/etc/logrotate.d/nginx
/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.log

Truncate, 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 clean

Old 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.pkgs

Step 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-upgrades

Check that it actually runs:

systemctl status unattended-upgrades --no-pager
sudo unattended-upgrade --dry-run

You 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=168h

What 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 -af

See what's eating Docker space:

docker system df
sudo du -xhd1 /var/lib/docker | sort -h | tail -n 20

Also cap container logs so one noisy container can't fill the disk. In /etc/docker/daemon.json:

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

Then restart Docker:

sudo systemctl restart docker

Restarting 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 40

Restart 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
timedatectl

What 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.conf

Step 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/local/bin/vps-weekly-cleanup.sh
#!/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
EOF

Run it once by hand so you trust the output:

sudo bash /usr/local/bin/vps-weekly-cleanup.sh

Step 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 -h and systemctl --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-required exists, 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

  • rm 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.
  • Inodes can be full when space isn't. Always run df -ih once 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/docker with rm -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.