Almost every server that gets popped was running a version of something with a known, patched CVE. Not a zero-day. A bug that had a fix available for weeks, sitting in the security pocket, waiting for somebody to type apt upgrade. Nobody typed it, because typing it is boring and there was always something more urgent.
unattended-upgrades fixes the boring part. It ships with Ubuntu, it costs no memory worth measuring, and once it is configured properly you stop thinking about security patches entirely. The catch is that "configured properly" is doing a lot of work in that sentence. The default install downloads and applies security updates but never reboots, so your kernel patches sit unused for months. And on 22.04 and newer, needrestart will happily pause an automated run waiting for a human to answer a question about restarting services.
This post sets it up the way you actually want it: security patches applied nightly, a reboot window you chose, services restarted without prompting, and a notification when something changes.
sudo apt install unattended-upgrades - it is likely already theresudo dpkg-reconfigure --priority=low unattended-upgrades, which writes 20auto-upgradesAllowed-Origins to the -security pocket only, leave -updates commented outAutomatic-Reboot "true" with Automatic-Reboot-Time "03:00" or kernel fixes never loadneedrestart to $nrconf{restart} = 'a' or automated runs can hang on a promptsudo unattended-upgrade --dry-run --debug before trusting it/var/log/unattended-upgrades/unattended-upgrades.log after the first nightTotal time: about 20 minutes.
Nothing here needs Docker, and it uses essentially no RAM - the process runs once a day and exits.
Before automating anything, look at the current state. This is usually the moment people realize how far behind they are.
sudo apt update
apt list --upgradable
To see only the security ones:
apt list --upgradable 2>/dev/null | grep -i security
And check whether a reboot is already pending from some previous upgrade:
ls -l /var/run/reboot-required
cat /var/run/reboot-required.pkgs
If that file exists, your running kernel is not the kernel you have installed. Get to a clean baseline first:
sudo apt full-upgrade -y
sudo apt autoremove -y
sudo reboot
Start automation from a known-good state, not from a six-month backlog. If a mass upgrade is going to break something, you want it to break now, while you are watching, rather than at 3am next Tuesday.
The package is usually preinstalled on Ubuntu server images, but make sure:
sudo apt install unattended-upgrades apt-listchanges -y
apt-listchanges is optional but nice - it puts the changelog of what was upgraded into the report email.
Now enable it:
sudo dpkg-reconfigure --priority=low unattended-upgrades
Answer yes. All that does is write /etc/apt/apt.conf.d/20auto-upgrades. You can also just write the file yourself:
sudo tee /etc/apt/apt.conf.d/20auto-upgrades > /dev/null <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOF
What those mean:
Update-Package-Lists "1" - run apt update dailyDownload-Upgradeable-Packages "1" - pre-download so the upgrade window is shortUnattended-Upgrade "1" - actually apply them dailyAutocleanInterval "7" - clear the old .deb cache weekly so /var does not fill upThe numbers are days, not booleans. "7" means every seven days. "0" disables.
Confirm the values took:
apt-config dump APT::Periodic
This is the file that matters: /etc/apt/apt.conf.d/50unattended-upgrades. Open it:
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
The top block controls which repositories are allowed to push updates at you unattended. The Ubuntu default looks like this:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
// "${distro_id}:${distro_codename}-updates";
// "${distro_id}:${distro_codename}-proposed";
// "${distro_id}:${distro_codename}-backports";
};
Leave this alone. Seriously. The default is the right answer for a server: security fixes only.
The temptation is to uncomment -updates so you get everything. Resist it. The -security pocket is a narrow, heavily-tested stream of fixes where Ubuntu goes out of its way not to change behavior - they backport the patch into the old version rather than bump you to a new one. -updates is a much broader stream of bug fixes and feature changes, and that is where unattended upgrades start surprising you. Apply -updates by hand, on a day you are paying attention.
Anything you upgrade deliberately - a database, Docker, a runtime your app is pinned against - should never move on its own at 3am. Find the Package-Blacklist block and fill it in:
Unattended-Upgrade::Package-Blacklist {
"docker-ce";
"docker-ce-cli";
"containerd.io";
"mariadb-server";
"postgresql-16";
};
Entries are regular expressions matched against the package name, and they are unanchored. "postgres" would match every package with "postgres" anywhere in the name, which is probably not what you want. Be specific, and anchor with ^...$ if you need exactness:
Unattended-Upgrade::Package-Blacklist {
"^docker-ce$";
"^linux-image-.*$";
};
Blacklisting kernels is a real choice some people make for hosts where an unplanned reboot is worse than a delayed patch. If you do that, you are signing up to patch kernels manually. Put it in a calendar.
These are all in the same file, mostly commented out by default:
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Dependencies "false";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::SyslogEnable "true";
Remove-Unused-Kernel-Packages "true" matters more than it looks. /boot on a VPS is often a small partition, and Ubuntu keeps old kernels around. Left alone for a year, /boot fills, the next kernel upgrade fails halfway, and you get to debug a broken initramfs on a console at an unpleasant hour. Let it clean up after itself.
MinimalSteps "true" splits the upgrade into the smallest independent chunks it can, so if the run gets interrupted (or hits the timeout) you are left with a consistent dpkg state rather than a half-configured mess.
Here is the part that makes the difference between "I have automatic updates" and "I am actually patched."
A kernel security fix is a file on disk. It does nothing until you boot into it. The default unattended-upgrades config never reboots, so on a stock setup your kernel CVEs get downloaded, installed, and then ignored indefinitely while /var/run/reboot-required quietly accumulates.
You have two honest options. Pick one.
Option A: reboot on a schedule you chose.
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
It only reboots when /var/run/reboot-required exists, so this is not a nightly reboot - it is a reboot on the handful of nights a kernel or core library actually changed. Automatic-Reboot-WithUsers "true" means it reboots even if someone is logged in over SSH; set it to "false" if you would rather a stray screen session block the reboot forever, which is usually the wrong tradeoff on a server.
Pick a real time, in the server's timezone. Check what that is:
timedatectl
If your VPS is on UTC and you are in Berlin, 03:00 is 5am local in summer. Fine for most people, but know which one you picked.
Option B: do not auto-reboot, and monitor for pending reboots instead.
Leave Automatic-Reboot "false" and make the pending-reboot state visible so it does not rot. A one-line check you can wire into any monitoring:
test -f /var/run/reboot-required && echo "reboot pending: $(cat /var/run/reboot-required.pkgs | tr '\n' ' ')"
Point Uptime Kuma or a Grafana dashboard at it, or have it push to ntfy. The rule: if you choose Option B, you must build the reminder. Option B without monitoring is just Option "never reboot", and that is the failure mode this whole post exists to prevent.
On Ubuntu 22.04 and newer, needrestart is installed by default. When a library like openssl or glibc is patched, the running processes that linked it are still using the old copy in memory. needrestart spots this and restarts the affected services - which is exactly what you want, because otherwise you patched the file and left the vulnerable code running.
The problem is its default mode is interactive. It wants to ask you which services to restart. In an automated run that means the process either hangs or bails, depending on the version and how it was invoked.
Fix it:
sudo nano /etc/needrestart/needrestart.conf
Find and set:
$nrconf{restart} = 'a';
'a' means automatic, 'i' is interactive (the default), 'l' means list-only and restart nothing. You want 'a' on a server.
While you are there, this line controls the kernel-upgrade dialog:
$nrconf{kernelhints} = -1;
-1 suppresses the "pending kernel upgrade" prompt. You do not need it, because Step 4 already handles reboots deliberately.
If you would rather not edit the main config, drop an override instead - it survives package upgrades more cleanly:
sudo tee /etc/needrestart/conf.d/99-unattended.conf > /dev/null <<'EOF'
$nrconf{restart} = 'a';
$nrconf{kernelhints} = -1;
EOF
Note the file must be valid Perl, which is why the lines end in semicolons.
Two systemd timers drive the whole thing:
systemctl list-timers 'apt-daily*'
apt-daily.timer - refreshes package lists and downloadsapt-daily-upgrade.timer - runs the actual unattended upgradeBy default apt-daily-upgrade fires around 06:00 with a randomized delay of up to an hour, which exists so that ten thousand VPSs do not hammer the mirrors at the same second. If that clashes with your traffic or your backup window, override it:
sudo systemctl edit apt-daily-upgrade.timer
Add:
[Timer]
OnCalendar=
OnCalendar=02:30
RandomizedDelaySec=30m
The empty OnCalendar= first is not a typo. systemd list-type settings accumulate, so without the empty line to reset it you end up with both the old schedule and the new one. Then:
sudo systemctl daemon-reload
systemctl list-timers apt-daily-upgrade.timer
Sequence the times so they make sense together: package lists refresh, then upgrades run, then - if a reboot is needed - it happens at Automatic-Reboot-Time. Set the reboot time after the upgrade window, not before, or the reboot fires on the previous day's pending flag and the actual upgrade waits another 24 hours. 02:30 upgrade and 03:00 reboot works. 03:00 upgrade and 02:30 reboot does not.
Do not wait until tomorrow morning to find out it is broken:
sudo unattended-upgrade --dry-run --debug
This changes nothing and prints exactly what it would have done. Read the output. You are looking for three things:
Allowed origins are: ... - the origins it resolved. If your codename is wrong or the pocket does not exist, you will see it here.Checking: <package> lines with is blacklisted next to anything you blacklisted in Step 3. Confirm your regexes match what you think they match.Packages that will be upgraded: ... at the end, or No packages found that can be upgraded unattended if you are current.That second point is worth doing carefully. A blacklist regex that silently matches nothing is a very quiet failure - everything looks fine right up until the night your database gets a minor version bump.
To force one real run right now, without waiting for the timer:
sudo unattended-upgrade -v
Then read the log:
sudo tail -n 50 /var/log/unattended-upgrades/unattended-upgrades.log
You can also trigger the whole systemd path end to end, which tests the unit and not just the binary:
sudo systemctl start apt-daily-upgrade.service
journalctl -u apt-daily-upgrade.service -n 50 --no-pager
Silent automation is how you end up with a service that has been failing for four months.
If you have working mail on the box (see our Postfix relay guide - a VPS almost never delivers direct-to-inbox), set:
Unattended-Upgrade::Mail "[email protected]";
Unattended-Upgrade::MailReport "on-change";
MailReport takes always, on-change, or only-on-error. on-change is the right default: you hear about it when something moved, and you are not training yourself to ignore a nightly "nothing happened" email.
No mail server and no interest in running one? Ping a dead-man's-switch instead. Add a drop-in on the upgrade service:
sudo systemctl edit apt-daily-upgrade.service
[Service]
ExecStartPost=-/usr/bin/curl -fsS -m 10 --retry 3 https://hc-ping.com/YOUR-UUID-HERE
The leading - on ExecStartPost= means a failed ping does not mark the unit failed - you do not want a monitoring hiccup to look like a broken upgrade. Now Healthchecks.io (or a self-hosted equivalent) alerts you when the run stops happening, which is the failure you are least likely to notice on your own.
unattended-upgrade --dry-run says "No packages found that can be upgraded unattended" but apt list --upgradable shows plenty. Working as intended, almost certainly. Those pending packages are in -updates, not -security. Check with apt-cache policy <package> and look at which pocket the candidate comes from.
The run dies with "Could not get lock /var/lib/dpkg/lock-frontend". Another apt process was running - often apt-daily.service overlapping with your manual command. Wait for it: systemd-run --property="After=apt-daily.service" --wait /bin/true. If it is stuck permanently, find the holder with sudo lsof /var/lib/dpkg/lock-frontend rather than deleting the lock file.
Upgrades apply but the server never reboots. Either Automatic-Reboot is still "false", or /var/run/reboot-required does not exist because nothing needing a reboot was upgraded, or Automatic-Reboot-WithUsers is "false" and you have a logged-in session. Check all three in that order.
A run hangs forever and the timer never completes. This is needrestart in interactive mode - go back to Step 5. Confirm with grep -r 'nrconf{restart}' /etc/needrestart/.
/boot is full and upgrades fail. Set Remove-Unused-Kernel-Packages "true", then clear the backlog by hand: sudo apt autoremove --purge. Check what is left with dpkg -l 'linux-image-*' | grep ^ii. Never rm files out of /boot directly - dpkg still thinks they are there, and you will end up hand-repairing its state.
Config changes did not take effect. Ordering matters: files in /etc/apt/apt.conf.d/ are read in lexical order, and the last assignment wins. A stray 52unattended-upgrades-local overrides 50unattended-upgrades. Ask apt what it actually believes with apt-config dump | grep -i unattended.
unattended-upgrades patches the host. The Debian userland inside your Docker images is frozen at build time and gets patched by rebuilding and redeploying, not by anything here. If you self-host with Compose, pair this with Watchtower or a scheduled docker compose pull && docker compose up -d, and see our Docker security guide.Automatic security updates are the highest ratio of protection to effort available on a Linux server. Twenty minutes once, and the class of attack that accounts for most real compromises - a known bug with a published fix - stops being your problem. The only work left is picking a reboot window and reading the occasional email.
Need a VPS to run this on? Our Linux VPS plans ship with clean Ubuntu 22.04 and 24.04 images, NVMe storage, and a web console for the night an upgrade goes sideways. See the options.