SSD Nodes Learn 🎉 VPS from $4.99/mo
Guides Matt ConnorBy Matt Connor

dnf-automatic security updates on Rocky/Alma

Configure dnf-automatic on Rocky Linux and AlmaLinux: security-only mode, the systemd timer, email alerts, and a reboot policy that avoids surprises.

What dnf-automatic does on Rocky Linux and AlmaLinux

dnf-automatic is how you get unattended security updates on Rocky Linux and AlmaLinux. It is one small program, started by a systemd timer, that reads /etc/dnf/automatic.conf and applies what that file allows. The install takes one command. The rest of this guide is about the settings that decide whether it protects the box or quietly does nothing.

If you came from Debian or Ubuntu, this is the same job unattended-upgrades does on an Ubuntu VPS. One difference matters more than all the others: what the word "security" means to the package manager. On Ubuntu it is a separate archive pocket. On the RHEL family it is metadata attached to published advisories, and that metadata can be missing or out of date. Point dnf-automatic at a repository with no advisory data and it installs nothing while reporting success.

This guide is written against Rocky Linux 9 and AlmaLinux 9, which use DNF 4 (DNF is the package manager on the RHEL family), as of August 2026. The 10 releases moved to DNF5 and the names change there, so they get their own section near the end. Every command below is one for you to run on your own box, with the output you should expect next to it.

Install dnf-automatic and read the config it ships

Enabling automatic updates belongs with the rest of the setup in the first ten minutes on a new VPS, right after you have a non-root user and a firewall.

sudo dnf install -y dnf-automatic
rpm -q dnf dnf-automatic
systemctl is-enabled dnf-automatic.timer

systemctl is-enabled prints disabled on a fresh install, because installing the package starts nothing. That is the most common reason a server that "has dnf-automatic" has never applied a single update.

The DNF version matters for one option. The reboot setting arrived upstream in DNF 4.15, and Red Hat backported it into dnf-4.14.0-6.el9 in November 2023 through advisory RHBA-2023:6645. Rocky 9 and AlmaLinux 9 rebuild that package, so a current box has it and a box left untouched since 2023 does not.

The config file is /etc/dnf/automatic.conf. The shipped copy lists every option this build understands, with its default, commented. Read it once before you edit it, because that file is the truth about your version.

The two switches that decide what happens

download_updates and apply_updates in the [commands] section decide the behaviour. Both are no by default on EL9 (enterprise Linux 9, the shared base of Rocky 9 and AlmaLinux 9), so an unedited dnf-automatic that you enable will only tell you what is available.

  • Both no: dnf-automatic reports available updates and changes nothing on the box.
  • download_updates = yes with apply_updates = no: packages are fetched into the DNF cache. The install is then fast and needs no network, but nothing changed tonight.
  • Both yes with upgrade_type = default: every available update is installed, security or not.
  • Both yes with upgrade_type = security: only packages named in a security advisory are installed.

A reasonable starting point for a public-facing VPS:

[commands]
upgrade_type = security
download_updates = yes
apply_updates = yes
random_sleep = 0
network_online_timeout = 60
reboot = never

network_online_timeout is how many seconds the run waits for a working network before giving up, which matters on a box that has just booted. random_sleep is an older way to spread load across many machines, and the timer does that job now. Run systemctl cat dnf-automatic.service to see the exact flags the shipped service passes.

Prove the file does what you think, without waiting until 06:00:

sudo systemctl start dnf-automatic.service
sudo journalctl -u dnf-automatic.service -n 50 --no-pager

The journal shows what the run considered and what it did. You can also force one behaviour from the command line, which overrides the file for that run only:

sudo dnf-automatic --downloadupdates --no-installupdates

What upgrade_type = security really means on Rocky and Alma

DNF does not work out that an update is a security update by comparing version numbers. It reads errata metadata: a file called updateinfo.xml published inside the repository, where each advisory lists the packages that fix it. AlmaLinux publishes these as ALSA advisories, Rocky publishes them as RLSA. upgrade_type = security builds a filter from that metadata and upgrades only the packages it matches.

Two consequences follow, and both surprise people.

First, no metadata means no updates. If the repository carries no updateinfo.xml, the filter matches nothing and the run ends with this line in the journal:

No security updates needed, but 3 updates available

The box is not patched, and nothing reported a failure. Check it yourself:

dnf updateinfo list --security
dnf check-update

If dnf check-update lists packages while dnf updateinfo list --security prints nothing at all, either nothing pending carries an advisory, or the repository has no advisory data to read. Rocky and AlmaLinux both publish it, so on those two an empty list is usually honest. CentOS Stream does not publish it at all.

Second, security mode is not a minimal change. dnf-automatic adds the security filter and then runs the ordinary upgrade path, so a package named in an advisory moves to the newest version in the repository and pulls its dependencies with it. The smaller step, moving only to the earliest version that fixes the advisory, is dnf upgrade-minimal --security run by hand. dnf-automatic has no setting for it.

One more caveat applies to Rocky. Rocky generates its errata from Red Hat data through its own pipeline, and that pipeline has fallen behind. In September 2025 users reported that the Rocky 9 BaseOS updateinfo.xml had not moved since December 2024, so --security was missing recent advisories, and Rocky staff confirmed it as a known issue. If you depend on upgrade_type = security, compare the advisory list against recent RLSA announcements now and then. On a box where coverage matters more than change control, upgrade_type = default on a schedule you choose is the safer setting.

The systemd timer that actually runs it

sudo systemctl enable --now dnf-automatic.timer
systemctl list-timers dnf-automatic.timer

list-timers should print one row with a NEXT time about a day away. An empty table means the timer is not enabled, so nothing will ever run.

The shipped timer fires at *-*-* 6:00 with RandomizedDelaySec=60m and Persistent=true. The random delay spreads a fleet across an hour so every server does not hit the mirror in the same second. Persistent=true means a machine that was powered off at 06:00 runs the missed job shortly after it boots, instead of skipping the day.

Change the schedule with a drop-in. Do not edit the shipped unit, because a package upgrade replaces files under /usr/lib/systemd/system.

sudo systemctl edit dnf-automatic.timer
[Timer]
OnCalendar=
OnCalendar=*-*-* 03:30
RandomizedDelaySec=30m

The empty OnCalendar= line is required. OnCalendar accumulates, so without that reset you keep the 06:00 entry and add a second one, and the job runs twice a day. Confirm the result with systemctl list-timers dnf-automatic.timer and read the NEXT column. The same drop-in rules apply to anything else you schedule, which is covered in writing systemd service and timer units.

Now the trap. The package ships three more timers: dnf-automatic-notifyonly.timer, dnf-automatic-download.timer and dnf-automatic-install.timer. Each starts the same program with command-line flags, and those flags override download_updates and apply_updates from your config file. Enable one of them next to dnf-automatic.timer and the job runs twice with two different behaviours, which looks exactly like your config file being ignored. Enable one timer and check:

systemctl list-unit-files 'dnf-automatic*'

How do I know when something was installed?

emit_via in the [emitters] section controls reporting. Under systemd the stdio emitter writes to the journal, which is the dependable option because it needs nothing else installed:

sudo journalctl -u dnf-automatic.service --since -7d --no-pager

The motd emitter writes the report into /etc/motd and replaces the contents of that file. If you keep a login banner there, leave this emitter out.

The email emitter opens an SMTP (simple mail transfer protocol) connection to email_host on email_port, which default to localhost and 25. A fresh VPS has nothing listening there, so the connection is refused and no mail is sent. Run ss -lnt | grep ':25' before you rely on it, and set up a relay-only Postfix if the output is empty. When mail does work, the subject reads Updates applied on 'web01'., taking the name from system_name.

For anything else, the command emitter hands the report to a program of yours on standard input:

[emitters]
emit_via = stdio, command
system_name = web01.example.com
send_error_messages = yes

[command]
command_format = /usr/local/bin/notify-ops
stdin_format = {body}

send_error_messages defaults to no, which means a failed run reports nothing at all. Turn it on. A patching system that only announces its successes is worse than none, because the silence reads as health.

dnf-automatic does not restart your services

Installing a package replaces files on disk. A process that is already running keeps the old code in memory, so a patched library does nothing for a daemon that started last month. That gap between installed and effective is why unattended patching needs a restart policy and not only an install policy.

sudo dnf install -y dnf-plugins-core
dnf needs-restarting -s
dnf needs-restarting -r

-s lists the systemd services whose files changed after they started. -r answers one question, and prints one of two blocks:

Core libraries or services have been updated since boot-up:
  * kernel
Reboot is required to fully utilize these updates.
No core libraries or services have been updated since boot-up.
Reboot should not be necessary.

-r is not a deep analysis. It checks a fixed list of packages: kernel, kernel-core, kernel-rt, glibc, linux-firmware, systemd, dbus, dbus-broker, dbus-daemon and microcode_ctl. If one of them was installed after the last boot, you get the first answer. Add your own package names in a file ending in .conf under /etc/dnf/plugins/needs-restarting.d/ when something else on the box also needs a reboot to take effect.

One caveat for scripts: dnf needs-restarting -r exits non-zero both when a reboot is required and when the command itself failed, so the exit status alone cannot tell those apart. Read the output text.

Restarting a service is the smaller move and usually the right one. Restart the SSH daemon from a second SSH session that is already open, so a bad config does not lock you out. A new kernel is the case where only a reboot helps, because the running kernel cannot be replaced in place.

Should the box reboot itself?

[commands]
reboot = when-needed
reboot_command = shutdown -r +5 'Rebooting after applying package updates'

reboot = never is the default. when-changed reboots after any applied update. when-needed reboots only when the check behind needs-restarting -r says a core package was replaced, which is what most single-server owners want, paired with a timer window they picked themselves. The default reboot_command gives logged-in users five minutes of warning through shutdown, and you can widen that.

Settle two things before you turn this on. Every service you depend on has to start at boot on its own, which is the usual gap for a Docker Compose stack that was started by hand. And you need console or rescue access from your provider, because a kernel that does not boot cannot be fixed over SSH. If either is missing, keep reboot = never and reboot yourself after reading the journal.

Rocky, AlmaLinux and CentOS Stream: where they differ

On Rocky 9 and AlmaLinux 9 everything above is identical, down to the config path and the unit names. Both publish errata, so upgrade_type = security has data to filter on.

CentOS Stream is the exception, and it is a hard one. Stream repositories carry no updateinfo.xml, so the security filter can never match and every run reports No security updates needed. On Stream, use upgrade_type = default and accept that you are taking every update. Stream also runs ahead of RHEL, so that setting moves more on a Stream box than the same setting on Rocky or AlmaLinux.

Rocky 10 and AlmaLinux 10 moved to DNF5, which renames things. The upstream DNF5 documentation gives the timer as dnf5-automatic.timer, puts the shipped defaults in /usr/share/dnf5/dnf5-plugins/automatic.conf with your overrides still in /etc/dnf/automatic.conf, defaults download_updates to yes rather than no, and adds distro-sync as an upgrade_type. The advisory query is dnf advisory list, with updateinfo kept as an alias. Confirm what your release actually installed before copying package or unit names out of a guide written for 9:

dnf list --available '*automatic*'
systemctl list-unit-files '*automatic*'

Many published guides for this topic still cover Rocky 8 only. The option set has grown since those were written, so check the commented file on your own box rather than trusting an old article.

Failure modes and the strings you will see

Nothing ever runs. systemctl list-timers dnf-automatic.timer prints an empty table and systemctl is-enabled dnf-automatic.timer prints disabled. The package was installed, the timer never was.

The job runs and installs nothing. The journal holds No security updates needed, but 3 updates available. The security filter matched nothing, either because nothing pending carries an advisory or because the repository publishes no advisory data.

A setting looks ignored. DNF logs an unknown option in automatic.conf at debug level and then uses the default, so a misspelled key changes nothing and warns nobody. Write apply_update = yes and apply_updates stays at no, so the box downloads forever and installs never. After any edit, run sudo systemctl start dnf-automatic.service and read the journal instead of trusting the file.

The job runs twice a day. Two timers are enabled. systemctl list-unit-files 'dnf-automatic*' shows which, and the extra ones pass flags that beat your config file.

No mail arrives. Either nothing is listening on port 25 for the email emitter, or send_error_messages is still no and the only thing worth reporting was an error.

A patched service still reports the old version. The file on disk is new and the process in memory is old. dnf needs-restarting -s names the services to restart.

FAQ

Does dnf-automatic install only security updates on Rocky Linux?

Only if you set upgrade_type = security in /etc/dnf/automatic.conf, and only if your repositories publish errata metadata. Rocky Linux and AlmaLinux both publish it, so the filter has advisories to match against. The shipped default is upgrade_type = default, which installs every available update once apply_updates = yes.

Why does dnf-automatic report "No security updates needed, but 3 updates available"?

DNF decides what counts as a security update by reading updateinfo.xml from the repository, where each advisory lists the packages that fix it. When that metadata is missing or stale, the security filter matches nothing while ordinary updates are still pending, which produces exactly that line. It is expected on CentOS Stream, which publishes no errata at all. On Rocky or AlmaLinux, compare dnf updateinfo list --security against dnf check-update and check that your metadata is current.

Will dnf-automatic reboot my server after a kernel update?

Not unless you ask it to. The reboot option defaults to never. Set reboot = when-needed and a run reboots only when the check behind dnf needs-restarting -r finds that a core package such as kernel or glibc was replaced since boot. reboot = when-changed reboots after any applied update. Both use reboot_command, which defaults to shutdown -r +5 with a warning message to logged-in users.

How do I change the time dnf-automatic runs?

Run sudo systemctl edit dnf-automatic.timer and add a [Timer] section with an empty OnCalendar= line followed by your schedule, for example OnCalendar=*-*-* 03:30. The empty line is required because OnCalendar accumulates, so leaving it out keeps the shipped 06:00 run and adds a second one. Verify with systemctl list-timers dnf-automatic.timer and read the NEXT column.

Do I still need to check a server that patches itself?

Yes. dnf-automatic installs packages and stops there. It does not restart daemons, and it reports nothing you will see unless emit_via names an emitter you actually read. Set emit_via to stdio at minimum, turn on send_error_messages so failures are reported too, and run dnf needs-restarting -s after a patch window to find services still running old code.

#dnf-automatic#rocky-linux#almalinux#security-updates#systemd