SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Why unattended-upgrades does nothing on Debian

Debian ships unattended-upgrades switched off. Enable it properly, read Origins-Pattern, check the apt-daily timers, and prove it patches itself.

Why unattended-upgrades does nothing on a new Debian install

On Debian, unattended-upgrades can be installed and still never run a single upgrade, because installing the package and enabling it are two separate steps. The package asks a debconf question before it configures itself, and the Debian installer stores the answer false for that question. Ubuntu answers the same question the other way, which is why the same package looks like it works there and looks broken here.

Nothing on the box points this out. There is no error at boot, no warning at login, and no log file to read, because the code that would write that log is never called. Switching it on takes one command. The rest of this guide covers the four things that still stop it after that: which repositories it may upgrade from, when the systemd timers really fire, how you learn that a run failed, and whether the machine is allowed to reboot itself.

dpkg -l unattended-upgrades | tail -n 1
sudo apt install -y debconf-utils
sudo debconf-show unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
apt-config dump APT::Periodic

debconf-show prints the stored answer to unattended-upgrades/enable_auto_updates. A * at the start of that line means something set the value rather than leaving it at the package default. On a machine built by the Debian installer, that something is the installer.

The cat may print cat: /etc/apt/apt.conf.d/20auto-upgrades: No such file or directory. That on its own explains the silence: with no file there are no periodic keys, so nothing is ever scheduled.

apt-config dump is the command that matters. APT reads every file in /etc/apt/apt.conf.d/ in filename order and merges them, so a plain value set in 99local overrides the same value in 20auto-upgrades. Reading one file tells you what that file says. apt-config dump tells you what APT will actually do.

Two keys decide whether anything happens at all:

  • APT::Periodic::Update-Package-Lists refreshes the package lists, which is the job apt update does by hand.
  • APT::Periodic::Unattended-Upgrade runs the upgrade itself.

Their values are not true and false. They are intervals in days. "1" means "do this if it has not been done in the last day", "7" means weekly, and "0" means never. APT::Periodic::Unattended-Upgrade "0"; is valid configuration that runs nothing, forever, without complaining once. If your dump shows 0 for that key, or shows no such key at all, you have found your reason.

Enable it: dpkg-reconfigure, or write the keys yourself

sudo apt update
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

--priority=low is not optional here. The question sits at low priority, so at the default priority dpkg-reconfigure prints nothing, changes nothing and exits 0, which looks exactly like a command that worked. Answer yes to the dialog. The package postinst then writes /etc/apt/apt.conf.d/20auto-upgrades from your answer.

On a machine you build from a script there is no dialog to answer, so set the answer first:

echo 'unattended-upgrades unattended-upgrades/enable_auto_updates boolean true' \
  | sudo debconf-set-selections
sudo dpkg-reconfigure -f noninteractive unattended-upgrades
apt-config dump APT::Periodic

You can also write the two keys directly:

printf 'APT::Periodic::Update-Package-Lists "1";\nAPT::Periodic::Unattended-Upgrade "1";\n' \
  | sudo tee /etc/apt/apt.conf.d/20auto-upgrades
apt-config dump APT::Periodic

That works immediately and it leaves a trap behind. The debconf answer still says the old thing, so the next dpkg-reconfigure, or a reinstall of the package, rewrites the file from debconf and undoes your edit with no output at all. Set both, or set debconf and let the postinst own the file.

This is the one real difference from the other side of the family. There the installer enables the same package for you, so the Ubuntu unattended-upgrades setup starts from a box that is already patching itself and spends its time on tuning. Everything below this point applies to both.

Which updates does Debian actually install for you?

Enabling the timer is not the same as agreeing to install everything. Every package source carries release metadata: an origin, a label, a suite, a codename, a site. unattended-upgrades looks at the candidate version of each upgradable package, reads the metadata of the source it would come from, and installs it only if that source matches an entry in Unattended-Upgrade::Origins-Pattern. No match means no upgrade, silently, by design.

Print your patterns, then print the metadata they are matched against:

apt-config dump Unattended-Upgrade::Origins-Pattern
grep -H -E '^(Origin|Label|Suite|Codename|Components):' /var/lib/apt/lists/*Release
apt-cache policy

grep -H keeps the filename on each line, so you can see which source every block of metadata belongs to. Those field values are the right-hand sides of your patterns. ${distro_codename} inside a pattern is substituted at run time with the codename of the release you are on, so one file keeps working across a release upgrade.

Read your own patterns with that metadata next to them, because the answer to "am I getting security fixes only, or point release updates too" is written there and nowhere else:

  • A pattern naming label=Debian-Security matches the security archive. That is where Debian security advisories land.
  • A pattern naming the -updates suite matches stable-updates, which carries what Debian ships between point releases, such as time zone data.
  • A pattern naming the plain release suite pulls point release changes as they are published, which is more churn and more testing work on your side.
  • A repository you added yourself matches nothing at all until you write a pattern for it.

That last one surprises people. A third-party repository has its own origin and its own label, so unattended-upgrades sees the candidate, sees that the source matches nothing, and moves on. Adding a pattern for one is a decision worth making slowly, because a vendor repository can ship a new major version inside the same suite, and you have then agreed to unattended major upgrades of that software in the middle of the night.

unattended-upgrades also never moves you between Debian releases. It upgrades packages inside the release you are on. Going from one stable release to the next stays a manual job you schedule yourself.

When you change the list, remember that APT lists append. A second Origins-Pattern block in another file adds to the shipped list instead of replacing it, so clear it first if replacing is what you mean:

#clear Unattended-Upgrade::Origins-Pattern;
Unattended-Upgrade::Origins-Pattern {
  "origin=Debian,codename=${distro_codename},label=Debian-Security";
  "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
};

Put local changes in a new file that sorts after the shipped one, for example /etc/apt/apt.conf.d/52unattended-upgrades-local. 50unattended-upgrades is a conffile, so editing it makes every future upgrade of the package stop and ask what to do with your version. A separate file never conflicts.

Two more keys are worth printing while you are here. Unattended-Upgrade::Allowed-Origins is the older form of the same idea, written as origin:archive pairs, and it is still read, so a configuration copied from a tutorial often ends up with both lists and no clear answer about which one matched. Unattended-Upgrade::Package-Blacklist holds regular expressions matched against package names, and a loose expression there blocks far more than you intended. The dry run below prints what actually applied.

Why did nothing run at the time you expected?

Two systemd timers drive this and they do different jobs. apt-daily.timer starts apt-daily.service, which refreshes package lists and downloads. apt-daily-upgrade.timer starts apt-daily-upgrade.service, which is the one that calls unattended-upgrade. Both run /usr/lib/apt/apt.systemd.daily with different arguments. If the second timer is disabled or masked, both periodic keys can read 1 and still nothing is ever installed.

systemctl list-timers 'apt-daily*' --all
systemctl is-enabled apt-daily.timer apt-daily-upgrade.timer
systemctl cat apt-daily-upgrade.timer

list-timers gives you NEXT, LEFT, LAST and PASSED for each unit. A timer with no NEXT is not going to run. is-enabled printing masked means someone turned it off hard, and nothing you write in apt.conf.d will change that.

Now read the [Timer] section that systemctl cat printed. OnCalendar is the earliest the timer may fire. RandomizedDelaySec adds a random wait after that point, so a fleet of Debian machines does not hit the same mirrors in the same second. This is why the NEXT column shows a time that does not match OnCalendar, and why yesterday's run happened at a different minute. It is working as designed. Persistent=true means a machine that was powered off at the scheduled time runs the job soon after the next boot instead of skipping the day.

To move the window, override the unit rather than editing it:

sudo systemctl edit apt-daily-upgrade.timer
[Timer]
OnCalendar=
OnCalendar=03:30
RandomizedDelaySec=30m

The empty OnCalendar= line is required, because list-valued unit settings accumulate: leave it out and you keep the shipped schedule and gain a second one. systemctl edit reloads systemd for you, so confirm the result with systemctl list-timers 'apt-daily*' and look at the new NEXT.

You never have to wait for a timer to test any of this:

sudo systemctl start apt-daily-upgrade.service
journalctl -u apt-daily-upgrade.service --since -1h

One thing confuses people who go looking in /etc/cron.daily: APT still ships /etc/cron.daily/apt-compat for systems without systemd. Read it with cat. On a systemd machine it exits early, so the work is not done twice.

Prove it works: unattended-upgrade --dry-run --debug

sudo unattended-upgrade --dry-run --debug

The binary is singular and the package name is plural. Typing unattended-upgrades here gives command not found, which plenty of people read as proof that the package is missing.

This one command answers almost every "why did it skip that package" question, because it prints its own reasoning. Near the top it prints the origins it computed from your patterns:

Allowed origins are: ...

Then one line per candidate package, carrying the origin record of the version it would install:

Checking: curl ([<Origin component:'main' archive:'stable' origin:'Debian' label:'Debian' site:'deb.debian.org' isTrusted:True>])

Then the list it would act on, or, on a box with nothing to do:

No packages found that can be upgraded unattended and no pending auto-removals

Put the first and second together and the diagnosis is direct. Find the Checking: line for the package you expected to see upgraded. Compare its origin record field by field against the allowed origins printed above it. One field that does not match, most often label or archive, is the entire reason it was skipped.

--dry-run marks packages in memory and installs nothing, so run it as often as you like. It still appends to /var/log/unattended-upgrades/unattended-upgrades.log.

If the origins match and a package is still held back, work through these:

  • apt-mark showhold lists packages you or a tool pinned. unattended-upgrades will not move a held package.
  • The upgrade would have to remove or add another package. unattended-upgrades avoids that unless the relevant keys allow it, so compare with sudo apt-get -s upgrade, which shows the same decision without those safety rules.
  • dpkg is half-configured after an interrupted run. Fix it with sudo dpkg --configure -a, then look again.
  • /var has no free space, so nothing downloads or unpacks. Check with df -h /var.
  • /boot is full of old kernels, which breaks the next kernel upgrade. Check df -h /boot, and print apt-config dump Unattended-Upgrade::Remove-Unused-Kernel-Packages to see whether cleanup is switched on.

If you run apt by hand while the timer is running you get Could not get lock /var/lib/dpkg/lock-frontend. That message means unattended-upgrades is doing its job. Wait for it to finish.

How do you find out when a run fails?

Start with the logs, since they exist whether or not you configure anything else:

sudo tail -n 40 /var/log/unattended-upgrades/unattended-upgrades.log
sudo tail -n 40 /var/log/unattended-upgrades/unattended-upgrades-dpkg.log
journalctl -u apt-daily-upgrade.service --since -7d

The first file is the decision log: what was checked, what was chosen, what was installed. The second holds the raw dpkg output, which is where a package whose postinst script failed shows itself. A separate shutdown log appears if upgrades run while the machine is powering down.

Mail is the usual reporting path:

Unattended-Upgrade::Mail "you@example.com";
Unattended-Upgrade::MailReport "on-change";

MailReport accepts always, on-change and only-on-error. only-on-error sounds like the disciplined choice and is usually the wrong one on a server nobody logs into, because a machine that has stopped upgrading entirely also sends no errors. Silence then covers a healthy box and a dead one equally well. on-change mails you whenever something was installed, so the mail doubles as proof that the timer is still alive.

Mail only leaves the machine if the machine can send mail. unattended-upgrades hands the message to the local mail system, so you need an MTA (mail transfer agent) such as postfix, or a sendmail-compatible relay client such as msmtp. Check with command -v sendmail and command -v mail. With neither present the report goes nowhere, the upgrade still succeeds, and the whole failure is invisible. Remember too that a new VPS IP address has no sending reputation, so mail sent straight to a public mailbox is often filed as spam. Relaying through a mail provider you already use is more reliable than running your own server for this.

If you would rather not run mail at all, watch the log timestamp from whatever monitoring you have:

stat -c '%y %n' /var/log/unattended-upgrades/unattended-upgrades.log

A timestamp that has not moved in a week means the timer stopped, whatever the config says. That check belongs beside the rest of your routine Linux server maintenance checks.

Should the box reboot itself?

Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";

With Automatic-Reboot set to true, unattended-upgrades reboots without confirmation, and only when the file /var/run/reboot-required exists after the run finishes. That marker is not written by unattended-upgrades. Another package has to create it, and on Debian that is normally needrestart. Do not assume it is there. Check after your next kernel upgrade:

ls -l /var/run/reboot-required /var/run/reboot-required.pkgs
uname -r
dpkg -l 'linux-image-*' | grep '^ii'

If that file never appears on your machine, Automatic-Reboot "true" never fires, and you can spend months believing you reboot for kernel updates while still running the old kernel. uname -r against the newest installed linux-image package settles it.

Automatic-Reboot-Time schedules the reboot for that clock time instead of doing it at once. Automatic-Reboot-WithUsers set to false skips the reboot while anyone is logged in, which on a server that always has a session open means it never reboots at all.

The decision is about what happens on the way back up, not about the reboot itself. A service someone started by hand does not return. An encrypted volume that needs a passphrase at boot does not mount. Two machines that depend on each other can come back in the wrong order. Turn automatic reboot on for a stateless web server you can lose for a minute at 02:00. Leave it off where a person is needed, and alert on the marker file instead so a human picks the moment. In between sits needrestart, which restarts services still running against an upgraded library and so covers everything except the kernel. Its default mode asks before restarting, so read /etc/needrestart/needrestart.conf before you rely on it in an unattended run.

Does this work the same on testing and unstable?

Everything above describes Debian stable, where security fixes arrive from a separate archive with a label of their own. That structure is what makes "security updates only" a setting you can express at all. The other suites are built differently, so a config copied from a stable server matches less than its author expects, and automatic upgrades on a moving suite mean unattended major version changes. That is a different thing to agree to. If you are weighing that choice, running Debian stable, testing or unstable on a server covers what each one promises. On the Red Hat side the same job has a different tool and a different vocabulary, and dnf-automatic on Rocky Linux and AlmaLinux does it with its own timer and config file.

Automatic upgrades shorten the gap between a fix being published and that fix being installed. They do not tell you what is still exposed, so pair them with a check for known CVEs on your server. CVE stands for common vulnerabilities and exposures, the public identifier a fix is tracked under.

The five minute check

  1. apt-config dump APT::Periodic prints both keys with a non-zero value.
  2. systemctl list-timers 'apt-daily*' prints a NEXT time for both timers.
  3. sudo unattended-upgrade --dry-run --debug prints allowed origins that include the security archive for your codename.
  4. sudo systemctl start apt-daily-upgrade.service finishes, and the timestamp on /var/log/unattended-upgrades/unattended-upgrades.log moves.
  5. A week later, that same log names packages it installed.

Passing the first four means the machine is configured. Passing the fifth means it works.

FAQ

Why does Debian install unattended-upgrades but leave it switched off?

The package asks a debconf question, unattended-upgrades/enable_auto_updates, and writes /etc/apt/apt.conf.d/20auto-upgrades from the answer. The Debian installer stores false for that question, so when the package arrives as part of a task or as a dependency it is configured to do nothing. Run sudo debconf-show unattended-upgrades to see the stored answer, then sudo dpkg-reconfigure --priority=low unattended-upgrades to change it. The low priority matters, because at the default priority the command exits without ever showing you the question.

How do I test unattended-upgrades without waiting for the timer?

Run sudo unattended-upgrade --dry-run --debug. It prints the origins it will accept, one Checking: line per upgradable package with that package's origin record attached, and the list it would install, while installing nothing. To exercise the real path, run sudo systemctl start apt-daily-upgrade.service, then read journalctl -u apt-daily-upgrade.service --since -1h together with /var/log/unattended-upgrades/unattended-upgrades.log.

Does unattended-upgrades install regular updates as well as security fixes?

Only if a pattern says so. A package upgrade is installed when the source it comes from matches an entry in Unattended-Upgrade::Origins-Pattern, and the security archive, the stable-updates suite and any repository you added yourself are separate entries. Print apt-config dump Unattended-Upgrade::Origins-Pattern on your own machine, then compare it against grep -H -E '^(Origin|Label|Suite|Codename):' /var/lib/apt/lists/*Release. It also never moves you between Debian releases.

Why does the upgrade not run at the time set in the timer?

apt-daily-upgrade.timer sets RandomizedDelaySec on top of OnCalendar, so systemd picks a random moment inside that window instead of firing on the calendar time. This spreads load across every Debian machine pointing at the same mirrors. systemctl list-timers 'apt-daily*' shows the moment it actually chose. To move the window, run sudo systemctl edit apt-daily-upgrade.timer and give it an empty OnCalendar= line followed by your own value.

Should I turn on automatic reboot for security updates?

Only where an unplanned restart is safe. Unattended-Upgrade::Automatic-Reboot "true" reboots with no confirmation whenever /var/run/reboot-required exists after a run, and that marker is written by another package, usually needrestart, not by unattended-upgrades itself. Confirm the file appears on your box after a kernel upgrade before you trust the setting. On a machine that needs a passphrase at boot, or that runs services someone started by hand, leave it false and alert on the marker file so a person picks the moment.