
Automate a Weekly Patch-and-Report Routine for a Small Server Fleet
Before you run this
This sets up a weekly, unattended package upgrade on each server, then emails you a plain-text report of what was upgraded and whether a reboot is now pending. The upgrade step runs apt-get upgrade non-interactively, so it changes installed software on the host. Package upgrades are not cleanly reversible: apt has no "undo the last upgrade" button, so treat this with the same caution as any change to a running server.
- Privileges: the patch script must run as root (it installs packages and writes to
/var/log). The systemd unit below runs it as root. Editing the units and installing the mail tools also needssudo. - Test first: read the script, then deploy it to one non-production server and run it by hand once (
sudo systemctl start fleet-patch.service) before you trust the timer or roll it out. Watch that first run to completion and read the email it sends. - This does not auto-reboot by default. It reports when a reboot is required and leaves the reboot to you. If you later enable automatic reboots (I show where), understand it will bounce the box in the middle of the night whether or not you are watching.
- Config-file prompts: the script tells dpkg to keep your existing config files on conflict (
--force-confold). That is the safe default, but it means a package's new default config is not applied automatically — you review those yourself. - Rollback of the automation itself is simple:
sudo systemctl disable --now fleet-patch.timer. Rolling back a bad package means reinstalling a known-good version or reverting a VM/LVM/ZFS snapshot — so if these servers matter, have a snapshot or backup regime in place before you turn on unattended upgrades.
What I'm assuming
- Ubuntu 22.04 LTS or Debian 12, i.e.
aptandsystemd. On RHEL/Alma the idea is identical but the tooling isdnfand the reboot-check isneeds-restarting -rfromdnf-utils— the script below is Debian-family only. - Each server can send mail to your address. I use the
mailcommand from themailutilspackage, pointed at an already-working local MTA or smarthost. If your fleet has no mail path yet, that's a separate task — set it up and confirmecho test | mail -s test you@example.comarrives before wiring up this job. - You deploy per-host (a timer on each server). For more than a handful of boxes, the mainstream alternative is to drive the same steps from a control node with Ansible; I mention that at the end rather than walking through it.
Replace you@example.com throughout with your real address.
The patch-and-report script
Save this as /usr/local/sbin/fleet-patch.sh and chmod 750 it.
#!/usr/bin/env bash
set -euo pipefail
# ---- settings you edit ----
REPORT_TO="you@example.com" # replace with your address
HOSTNAME_SHORT="$(hostname -s)"
# ---------------------------
export DEBIAN_FRONTEND=noninteractive # never prompt during automation
LOG="$(mktemp)"
# Keep existing config files on conflict rather than overwriting them.
APT_OPTS=(-y \
-o Dpkg::Options::=--force-confdef \
-o Dpkg::Options::=--force-confold)
{
echo "Patch report for ${HOSTNAME_SHORT} — $(date -Is)"
echo "========================================================"
echo
echo "## Refreshing package lists"
apt-get update
echo
echo "## Packages that will be upgraded"
# apt list is for humans; harmless read-only preview in the report
apt list --upgradable 2>/dev/null || true
echo
echo "## Applying upgrades"
apt-get "${APT_OPTS[@]}" upgrade
echo
echo "## Removing packages no longer needed"
apt-get "${APT_OPTS[@]}" autoremove
echo
if [ -f /var/run/reboot-required ]; then
echo "## REBOOT REQUIRED on ${HOSTNAME_SHORT}"
cat /var/run/reboot-required.pkgs 2>/dev/null || true
else
echo "## No reboot required."
fi
} > "$LOG" 2>&1
# Email the report; also keep a copy on disk.
cp "$LOG" /var/log/fleet-patch.last.log
mail -s "[patch] ${HOSTNAME_SHORT} $(date +%F)" "$REPORT_TO" < "$LOG"
rm -f "$LOG"
A few notes on the non-obvious lines:
DEBIAN_FRONTEND=noninteractiveand the two--force-conf*options are the standard, documented way to run apt without a human present.--force-confoldkeeps your config file when a package ships a changed one;--force-confdeflets dpkg pick the default where there's no conflict./var/run/reboot-requiredis the file Debian/Ubuntu create when an upgrade (kernel, libc, etc.) needs a reboot. Its companion.pkgslists which packages triggered it. Both are standard on this platform.- I deliberately use
apt-get upgrade, notdist-upgrade/full-upgrade. Plainupgradewill not remove an installed package to satisfy a dependency, which is the conservative choice for unattended runs. Usefull-upgradeonly if you know you want that behaviour. set -euo pipefailmeans the script aborts on the first real error, so a failedapt-get updatewon't silently roll on into an upgrade.
Wire it to a systemd timer
Create the service unit /etc/systemd/system/fleet-patch.service:
[Unit]
Description=Weekly apt patch-and-report
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/fleet-patch.sh
Create the timer /etc/systemd/system/fleet-patch.timer:
[Unit]
Description=Run the weekly patch-and-report job
[Timer]
# Every Sunday at 02:30, with up to 30 min random spread so a whole
# fleet does not hammer the mirror at the same instant.
OnCalendar=Sun *-*-* 02:30:00
RandomizedDelaySec=1800
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true means if the box was off at 02:30 Sunday, the job runs once shortly after it next boots, so a machine that was down over the weekend still gets patched.
Enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now fleet-patch.timer
--now starts the timer (arming the schedule); it does not run the patch immediately.
Do the first run by hand
Before you trust the schedule, trigger the job once and watch it:
sudo systemctl start fleet-patch.service
journalctl -u fleet-patch.service -b --no-pager
Confirm the email arrived and that its contents make sense. If mail didn't arrive, the upgrade still happened — fix the mail path and re-run.
Optional: automatic security-only patching
If what you actually want is Debian/Ubuntu's own security-only unattended patching, that is the unattended-upgrades package, configured in /etc/apt/apt.conf.d/50unattended-upgrades, and it can auto-reboot via Unattended-Upgrade::Automatic-Reboot. That's the vendor-blessed path and it's worth reading up on — see the Debian wiki page "UnattendedUpgrades" and Ubuntu's automatic-updates documentation for the exact directive names and defaults, which I won't reproduce from memory here. The script above is the choice when you want a full upgrade plus a report you actually read, rather than silent security-only patching.
Verify it worked
Timer is armed and scheduled:
systemctl list-timers fleet-patch.timerLook at the
NEXTcolumn for the upcoming Sunday.What actually got upgraded (apt's own record, independent of this script):
less /var/log/apt/history.logLast report copy on disk:
cat /var/log/fleet-patch.last.log.
Undo / roll back
Turn off the automation:
sudo systemctl disable --now fleet-patch.timerThen delete the two unit files and the script if you're removing it entirely, and run
sudo systemctl daemon-reload.Revert a specific bad package to a known version:
apt-get install <package>=<known-good-version>List available versions with
apt-cache policy <package>. This is per-package and fiddly — for a genuinely broken box, restoring a VM/LVM/ZFS snapshot taken before the run is the reliable rollback, which is exactly why the safety note asks you to have one.
For more than a few hosts, run these same steps from an Ansible control node instead of per-host timers, using the ansible.builtin.apt module with upgrade: yes and a reboot-check task — same procedure, one place to manage it.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Batch-Convert and Optimise Images with a Watch Script
This sets up a background service that watches a directory and, whenever a new JPEG or PNG lands there, produces a resized, stripped, and re-compressed copy in a separate output directory. Its purpose is to keep uploaded or generated images small and consistent without anyone running a command by hand.
Automatically Restart a Hung Service on a Schedule, with Logging
Some services don't crash cleanly. The process stays alive, systemctl still calls it active , but it has stopped answering — a wedged worker pool, a deadlocked thread, a leaked connection table. systemd's own Restart=on-failure handles a process that exits , but it won't help you with one that's technically running and doing nothing. This guide sets up a periodic health check that catches that case, restarts the unit when it's genuinely stuck, and logs every decision so you can prove what happened at 3 a.m.
Automate Container Image Cleanup on a Docker Host
Docker hosts fill up quietly. Every docker pull , every CI build, every image rebuild leaves layers behind, and /var/lib/docker grows until a deploy fails with "no space left on device" at the worst possible moment. This guide sets up a scheduled job that prunes unused images older than a threshold you choose, so the host reclaims space on its own without you babysitting it.
Build a Service Health Dashboard with Cron and Static HTML
This guide builds a small read-only status page. A bash script checks whether a list of systemd services are active and whether a few HTTP endpoints answer, then writes a plain HTML file that your web server already serves. Cron re-runs it every few minutes so the page stays current. Nothing here restarts, reconfigures, or stops any service — it only reads state and writes one HTML file.




