Shore Up
A night watchman oiling and repairing a row of machines by lamplight, with one machine switched off to rest beneath a wall clock reading two o'clock.
LinuxSecurity

Automate Debian/Ubuntu Package Updates With a Safe Reboot

Ketan Aagja9 min read
No ratings yet

Before you run this

This guide sets up a small script, run by a systemd timer, that does three things on a schedule: refreshes the package lists (apt-get update), installs available upgrades non-interactively, and — only if the upgrade left the system needing a reboot — reboots the machine during a maintenance window.

It needs root. apt-get and shutdown both require it, so the script runs as root via a systemd service. Nothing here works as an unprivileged user.

What it changes, and what is disruptive. Installing upgrades can restart services and change the behaviour of running software. The reboot is the part to respect: an automatic reboot will drop every session and running service on the box. There is no "undo" for a reboot that has already happened. For that reason the script I give you will not reboot unless you explicitly turn that on, and even then only inside the time window you choose.

Test it first. Read the script before you install it. Run it once by hand on a throwaway VM or a non-production host before you let a timer loose on anything you care about. You can preview exactly what apt would upgrade, changing nothing, with:

sudo apt-get update
sudo apt-get -s upgrade   # -s simulates; it makes no changes

I assume Debian 12 or Ubuntu 22.04 LTS, systemd, and bash. On RHEL/Alma the equivalent is dnf-automatic; this guide does not cover it.

The mainstream alternative, named up front

The most common way to do unattended patching on Debian/Ubuntu is the unattended-upgrades package. It has its own reboot support through the directives Unattended-Upgrade::Automatic-Reboot "true"; and Unattended-Upgrade::Automatic-Reboot-Time "02:00"; in /etc/apt/apt.conf.d/50unattended-upgrades. If you want the boring, officially-blessed path, install that package and read man unattended-upgrades — it is a fine choice.

I'm writing the scripted version here because it gives you one readable file you fully control, which suits a lot of small fleets. Pick one approach; don't run both.

The update script

Create /usr/local/sbin/auto-updates.sh:

#!/usr/bin/env bash
set -euo pipefail

# Answer package prompts non-interactively.
export DEBIAN_FRONTEND=noninteractive

# Set to "yes" to allow this script to reboot when a reboot is required.
# Leave as "no" until you have tested the update run itself.
ALLOW_REBOOT="no"

LOG=/var/log/auto-updates.log
exec >>"$LOG" 2>&1
echo "=== $(date -Is) update run starting ==="

apt-get update

# Conservative upgrade: does NOT install new packages or remove any.
# On config-file conflicts, keep the existing file (confold) or the
# package default where none was set (confdef).
apt-get -y \
  -o Dpkg::Options::=--force-confdef \
  -o Dpkg::Options::=--force-confold \
  upgrade

# Clean up packages left orphaned by the upgrade.
apt-get -y autoremove

# Ubuntu (and Debian with update-notifier-common) drops this file when a
# reboot is needed. If it exists, act on the reboot policy above.
if [ -f /var/run/reboot-required ]; then
    echo "Reboot required. Packages: $(cat /var/run/reboot-required.pkgs 2>/dev/null | tr '\n' ' ')"
    if [ "$ALLOW_REBOOT" = "yes" ]; then
        echo "Scheduling reboot in 5 minutes."
        /sbin/shutdown -r +5 "Automated reboot after package updates"
    else
        echo "ALLOW_REBOOT is not set; leaving reboot to an operator."
    fi
else
    echo "No reboot required."
fi

echo "=== $(date -Is) update run finished ==="

Two notes on choices here:

  • I use upgrade, not full-upgrade (aka dist-upgrade). Plain upgrade never removes a package and never pulls in a new one to satisfy a changed dependency — the conservative behaviour you want unattended. The cost is that a held-back package won't upgrade until you handle it by hand. If you deliberately want the more aggressive behaviour, full-upgrade exists; know what it does before you enable it.
  • The --force-confold/--force-confdef pair keeps your edited config files instead of stopping for an interactive prompt. That is the standard non-interactive combination, but it means a package's new default config won't overwrite yours — occasionally you'll want to review .dpkg-dist files afterwards.

About the reboot-required file: on Ubuntu this is created by an apt hook from update-notifier-common, which is installed by default. On Debian it is not guaranteed to be present unless that package (or a tool like needrestart) is installed. Confirm the mechanism exists on your host — see "Verify" below — before you trust the reboot logic.

Make it root-owned and executable:

sudo chown root:root /usr/local/sbin/auto-updates.sh
sudo chmod 750 /usr/local/sbin/auto-updates.sh

The systemd service and timer

A oneshot service runs the script; a timer fires it on a schedule. Create /etc/systemd/system/auto-updates.service:

[Unit]
Description=Automated apt updates and conditional reboot
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/auto-updates.sh

Then /etc/systemd/system/auto-updates.timer:

[Unit]
Description=Run automated apt updates daily

[Timer]
# Every day at 02:00 local time.
OnCalendar=*-*-* 02:00
# Spread load: start up to 30 minutes late at random.
RandomizedDelaySec=1800
# If the machine was off at 02:00, run at next boot.
Persistent=true

[Install]
WantedBy=timers.target

Load and enable it:

sudo systemctl daemon-reload
sudo systemctl enable --now auto-updates.timer

Because the timer runs inside a maintenance window (02:00 here), the shutdown -r +5 in the script is a reasonable "reboot now, give five minutes' warning" once you've set ALLOW_REBOOT="yes". Adjust the OnCalendar line and the warning delay to suit your change window.

Verify it works

First, run the service by hand and watch it — do this while ALLOW_REBOOT is still no:

sudo systemctl start auto-updates.service
journalctl -u auto-updates.service --no-pager
sudo cat /var/log/auto-updates.log

You should see the update run recorded and either "No reboot required" or the list of packages that want one.

Check the reboot-detection mechanism is actually present on this host:

dpkg -l update-notifier-common   # Ubuntu: should be installed
ls -l /var/run/reboot-required    # present only when a reboot is pending

Confirm the timer is scheduled and see its next run:

systemctl list-timers auto-updates.timer
systemctl status auto-updates.timer

Only once the manual run looks right, edit the script and set ALLOW_REBOOT="yes" if you want unattended reboots.

Undo and roll back

The automation itself is fully reversible — stop and remove it:

sudo systemctl disable --now auto-updates.timer
sudo rm /etc/systemd/system/auto-updates.timer
sudo rm /etc/systemd/system/auto-updates.service
sudo rm /usr/local/sbin/auto-updates.sh
sudo systemctl daemon-reload

The package upgrades it applied are a different matter. Individual packages can be reverted with apt-get install <package>=<old-version> if the old version is still in your cache or a repo, but rolling back a whole upgrade set is not something apt does cleanly. If a specific package must not be touched, hold it beforehand with apt-mark hold <package> (and apt-mark unhold to release it). This is exactly why you test the update run on a VM first: the reboot you can gate, but the upgrades you cannot casually take back.

For the official details on any flag here, see the apt-get, dpkg, systemd.timer, and shutdown man pages on the system itself — they are the authoritative reference for the exact syntax on your release.

Written by
Ketan Aagja

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.