A Linux server maintenance checklist
Weekly and monthly checks for a Linux server, plus the release upgrades, with the failure each one prevents and the restore test almost everyone skips.
What Linux server maintenance actually means
Linux server maintenance is a short list of checks run on a fixed schedule, not a project with an end. Weekly you confirm that updates installed, that the disk has room, that no service died, and that the backup job finished. Monthly you test a restore, check certificate expiry, audit accounts and keys, and clear out old kernels and logs. Once per distribution release you plan the version upgrade and take the reboot you keep postponing.
Building the server is a different job, and the first ten minutes on a new VPS covers that part. This page is the year after it. Every item below names the failure it prevents, because a checklist without consequences is one that people quietly stop running.
The commands here are illustrative and meant to be read before they are run. Compare their output against your own server, since a healthy value for free space or process count depends on what the box does. Where a check differs between distributions, the text says so. Examples use Debian and Ubuntu with apt. On the RHEL family the tooling is dnf and several paths are different.
How to pick a Linux server maintenance cadence you will keep
Weekly checks cover the things that change without you: packages, disk usage, service state, scheduled jobs. Those move on their own, so a week is about the longest you can leave them unread.
Monthly checks cover slow decay: certificates approaching expiry, accounts nobody removed, kernels piling up in /boot, log files growing past a rotation rule that stopped matching. None of these breaks tomorrow. All of them break eventually.
Release checks are calendar-driven. A distribution release is the one maintenance item with an external deadline, because support for your current version ends whether or not you were ready.
Put the run in a fixed slot, a Monday morning for the weekly pass and the first of the month for the monthly one. A checklist run "when I get to it" is not a checklist. Past a handful of machines, run these from one place instead of by hand, which is the subject of managing multiple Linux servers from one place.
Weekly: did the updates actually install?
Enabling unattended-upgrades is not the same as knowing it ran. The service can be masked, the configuration can be limited to an origin you do not use, and one held package can break every run after it. Installing it is covered in automatic security updates on Ubuntu. The weekly job is proving that what you installed did the work.
systemctl status unattended-upgrades
journalctl -u unattended-upgrades --since "8 days ago" --no-pager
sudo tail -n 50 /var/log/unattended-upgrades/unattended-upgrades.log
apt list --upgradable
apt-mark showholdapt list --upgradable is the honest measure, because it reports the current state rather than the intent. Security updates still sitting in that list mean the automation is not doing its job, so read the log before assuming the machine is patched. A package pinned with apt-mark hold is skipped forever and reports nothing, which is why apt-mark showhold belongs in the same pass.
The failure this prevents: running a known-vulnerable package for months while believing updates were automatic.
Weekly: disk and inode headroom
A full root filesystem breaks things that look unrelated to disk. The database refuses writes, logging stops, a package upgrade dies half configured, and on some setups you cannot open a new session because it cannot write its own files.
df -h
df -i
sudo du -xh --max-depth=1 / | sort -h | tail -20df -i is the half most people skip. Inodes are the fixed-count structures that hold file metadata, and a filesystem can run out of them while df -h still reports free gigabytes. Writes then fail with No space left on device next to an output showing space left, which costs a confusing hour the first time. Millions of small files, from a stuck mail queue or a session directory nobody prunes, are the usual cause.
du -xh stays on one filesystem, which is what you want on a box with bind mounts or attached storage. On a Docker host the answer is usually in image layers and dead volumes, cleared as described in pruning Docker disk usage on a VPS.
Free space tells you about capacity. The storage underneath fails on its own schedule, which is a separate check covered in disk health monitoring on a VPS.
Weekly: what stopped without telling you?
systemctl list-units --state=failed
systemctl list-timers --all
journalctl -p err --since "8 days ago" --no-pagerA unit that crashed and hit its restart limit sits in the failed state and stays there, silent. Nothing emails you about it. list-timers is the more useful half: it shows when each timer last ran and when it fires next, so a LAST value older than the timer's own interval means that job did not run at all.
Read the journal for the unit before restarting it, with journalctl -u <unit> -n 100 --no-pager. A restart clears the symptom, and then you have no reason to look again until the same thing happens at a worse hour.
The failure this prevents: a monitoring agent, a queue worker or a backup service that has been dead since a memory spike three weeks ago.
Weekly: did the backup job actually finish?
A scheduled backup and a completed backup are different facts, and only one of them restores. Check completion.
systemctl list-timers --all | grep -i backup
journalctl -u <your-backup-unit> --since "8 days ago" --no-pager
ls -lh /path/to/backup/target | tailConfirm two things. The last run exited zero, and the newest archive is both recent and roughly the size you expect. A backup file that is suddenly a tenth of its usual size is a failed dump that still wrote a file, which is the most dangerous shape of backup failure because everything downstream looks normal.
If your script pipes a dump into a compressor, add set -o pipefail at the top. Without it the exit status of the pipeline is the compressor's, and the compressor succeeded: it compressed the error message. The job then reports success every night while writing a small archive of nothing.
Monthly: restore a backup somewhere else
This is the item most people skip, and it is the one that decides whether the rest of the list mattered.
Restore to a different machine or a fresh container, never over the live data. Then open what you restored and confirm it is real. Count rows in a table. Open a document. Log in to the restored application. An extraction that finished proves the archive is readable, and nothing more than that.
Repository tools have their own verification: restic check --read-data-subset=5% and borg check --verify-data read the stored data rather than the index. Run them, and treat them as a smoke test rather than a substitute. Verification checks that bytes survived. A restore checks that the bytes are the ones your application needs.
Two details people learn the hard way. Test the decryption passphrase on a machine that does not already hold the key in an agent, because a backup you cannot decrypt is not a backup. And time the restore, because that duration is your real recovery time, and the usual moment to discover it is during an outage.
Monthly: which certificates expire soon?
Renewal automation fails quietly. The certbot timer can renew the file on disk while the web server keeps serving the old certificate from memory, because the deploy hook that reloads the service did not run. So ask the running server what it is serving, from outside the box.
sudo certbot certificates
systemctl list-timers --all | grep -i certbot
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -datesThe -servername flag sets SNI (server name indication), which is required on any address hosting more than one site, otherwise you are handed the default certificate rather than yours. If certbot was installed from a snap the timer is named differently, so match on the word rather than a unit you assumed.
Remember the certificates with no automation at all: a mail server, a VPN, an internal certificate authority. Those are the ones that expire on a weekend, and browsers and clients refuse them outright rather than warning.
Monthly: users, sudo access and SSH keys
awk -F: '$3>=1000 && $3<65534 {print $1}' /etc/passwd
getent group sudo
sudo find /home /root -name authorized_keys -exec ls -l {} +
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication|port'
last -n 25sshd -T prints the effective configuration after every Include has been merged, which is what the daemon will actually use. Recent Ubuntu images ship drop-in files in /etc/ssh/sshd_config.d/, and those can override the main file, so reading sshd_config alone can tell you the opposite of the truth. On the RHEL family the administrative group is wheel rather than sudo, so adjust the getent line.
Then read the authorized_keys files themselves. Access is granted by key, not by account, so a key left behind by a contractor who finished six months ago is a working login that no user list will flag. Keys carry a comment field. Use it, and delete anything you cannot attribute to a person.
For login history, journalctl -t sshd --since "30 days ago" | grep -i accepted matches on the syslog identifier rather than a unit name. That matters because Ubuntu 24.04 activates SSH through a socket, so each connection is logged under a generated per-connection unit, and a plain journalctl -u ssh can miss them.
Monthly: old kernels and a full /boot
/boot is often a separate partition of a few hundred megabytes on a stock VPS image. Every kernel update adds an image and an initramfs to it. When it fills, the next upgrade fails partway through and leaves packages unconfigured, which is a bad state to meet by surprise on a Friday.
uname -r
df -h /boot
dpkg -l 'linux-image-*' | grep '^ii'
sudo apt autoremove --purgeuname -r first, always: it names the kernel you are running right now, and that one must survive whatever you remove. apt autoremove handles the normal case on Debian and Ubuntu, since kernels are marked as automatically installed and the current one is protected. The edge cases, a manually installed kernel or a /boot that is already full enough to block apt itself, are covered in clearing old kernels on Ubuntu.
Monthly: log growth and the systemd journal
journalctl --disk-usage
sudo du -xh --max-depth=1 /var/log | sort -h | tail
sudo logrotate --debug /etc/logrotate.conflogrotate --debug is a dry run and writes nothing, so it is safe on a live box. It is worth running because rotation rules match on paths: an application that changed its log location during an upgrade is no longer covered by its own rule, and that file now grows without limit until it fills the disk.
The journal is capped by systemd, but by a fraction of the filesystem rather than a number you chose. Set SystemMaxUse= in /etc/systemd/journald.conf and restart systemd-journald if you want a specific ceiling. sudo journalctl --vacuum-time=14d reclaims space immediately, and it is a one-time action rather than a policy, so pair it with the config change.
Per release: the reboot you keep postponing
An updated kernel package on disk is not a running kernel. Until the reboot, the machine still runs the old one, and live patching, where it is available, covers only a subset of fixes.
uname -r
ls -l /var/run/reboot-required
cat /var/run/reboot-required.pkgs
sudo needrestartThat flag file is a Debian and Ubuntu convention written by package scripts. RHEL family systems do not create it, and the equivalent question there is answered by needs-restarting -r, which comes from dnf-utils. needrestart, installed by default on recent Ubuntu server images, answers the level below the kernel: it lists processes still mapping a library that was replaced on disk, which is why a patched OpenSSL does not take effect until the services using it restart.
Schedule the reboot instead of avoiding it. In /etc/apt/apt.conf.d/50unattended-upgrades, Unattended-Upgrade::Automatic-Reboot "true"; and Unattended-Upgrade::Automatic-Reboot-Time "03:00"; hand the decision to a time you picked. A planned reboot is also the only test of whether the box comes back, because a broken fstab entry or a service you never enabled shows itself at boot and nowhere else.
Per release: planning the distribution upgrade
Ubuntu LTS releases carry five years of standard support and interim releases carry nine months, so the choice sets your upgrade workload for years. That trade-off is LTS versus interim releases on a server.
lsb_release -a
cat /etc/update-manager/release-upgradesdo-release-upgrade reads that file, and Prompt=lts restricts it to LTS-to-LTS moves. The LTS-to-LTS path normally opens at the first point release of the new version rather than on release day, so check what your machine is offered instead of planning around a date you assumed. The mechanics of the jump are in upgrading Ubuntu 24.04 to 26.04.
Plan three months of margin. Take a snapshot you have tested restoring, list your third-party apt repositories (the upgrade disables them, and each one needs a new target for the new release), and decide the rollback before you start. As of August 2026, Ubuntu 24.04 LTS has standard support into April 2029, so this is scheduling rather than an emergency.
What to automate, and what to keep manual
Automate the decisions you have already made: security updates, log rotation, certificate renewal, backup jobs. Automate the alerting as well, because a check that depends on you remembering it is a check that will not happen at 2am. An external monitor, such as self-hosted status monitoring with Uptime Kuma, catches the one thing no on-box script can report, which is the box being unreachable.
Keep two things manual: the restore test and the account audit. Both need a person deciding whether the result is right. If you would rather read machine state in a browser than a terminal, Cockpit versus Webmin for server management compares the two usual web consoles.
The automation then needs its own check, which is why the first weekly item on this list is verifying the updater. Automation that fails silently is worse than none, because it removes the failure and the habit of looking at the same time.
The whole checklist in one place
Weekly and monthly commands, ready to copy
# weekly
systemctl list-units --state=failed
systemctl list-timers --all
journalctl -u unattended-upgrades --since "8 days ago" --no-pager
apt list --upgradable
apt-mark showhold
df -h
df -i# monthly
sudo certbot certificates
awk -F: '$3>=1000 && $3<65534 {print $1}' /etc/passwd
getent group sudo
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication'
uname -r
dpkg -l 'linux-image-*' | grep '^ii'
journalctl --disk-usageThe restore test is missing from this block on purpose. It is not one command, and it does not belong on the same machine. Restore somewhere else, then open the data and confirm it is real.
FAQ
How often should I run Linux server maintenance?
Weekly for anything that changes on its own: update status, disk and inode headroom, failed units, and whether the backup job finished. Monthly for slow decay: a restore test, certificate expiry, the account and SSH key audit, old kernels, log growth. Once per distribution release for the version upgrade and the reboot into the current kernel. The weekly pass takes a few minutes on a healthy box, which is the point of running it weekly rather than when something looks wrong.
Why test a restore if the backup job reports success?
Because the job reports on its own exit status, and that status can be true while the archive is useless. A dump piped into a compressor without set -o pipefail returns the compressor's status, so a failed dump that produced only an error message still exits zero and writes a small file. Restore to a different machine, open the data, and count something. The restore also times itself, and that duration is your real recovery time.
Do I have to reboot after every kernel update?
You have to reboot before the new kernel is the running one. On Debian and Ubuntu the presence of /var/run/reboot-required says a package asked for it, and /var/run/reboot-required.pkgs names which. On the RHEL family that file does not exist, and needs-restarting -r from dnf-utils answers the same question. Set an automatic reboot window in /etc/apt/apt.conf.d/50unattended-upgrades rather than deferring it indefinitely, because a machine that has not rebooted in a year has an untested boot path as well as an old kernel.
Which of these checks can I safely automate?
Automate the actions whose decision is already made: security updates, log rotation, certificate renewal, scheduled backups. Automate the notification too, so a failed unit or a filling disk reaches you without a human running a command. Keep the restore test and the key audit manual, because each needs a person to judge whether the result is correct. Then add one check on the automation itself, since a silent updater failure looks exactly like everything working.