Pin package versions with dnf versionlock
Hold one package, like nginx or MariaDB, at its current version on Rocky Linux 9 and AlmaLinux 9 while dnf-automatic keeps updating everything else.
How to pin a package version with dnf versionlock
To pin a package version on Rocky Linux 9 or AlmaLinux 9, install the python3-dnf-plugin-versionlock plugin and run sudo dnf versionlock add with the package name. dnf then keeps that package at its exact installed version and release. Every other package keeps updating as normal, including the updates that dnf-automatic installs for you.
The lock is one line in a plain text file. You can list it with one command and delete it with another. This guide covers EL9 (Enterprise Linux 9, the family that Rocky Linux 9 and AlmaLinux 9 belong to), which ships dnf 4. If you come from Debian or Ubuntu, this is the dnf answer to apt-mark hold, and the dnf and apt command equivalents map the rest of that vocabulary.
Why hold one package back
Automatic updates are the right default. If you followed the guide to automatic updates with dnf-automatic on Rocky and Alma, your server now installs updates on a timer. That is good for almost every package. It is a problem for the few packages where a new build changes something you depend on.
Two cases are common. The first is a database server whose new build you want to test against a copy of your data before it touches the real one. The second is an nginx that loads a third-party module compiled against one exact nginx build, so a new nginx build can stop the module from loading. In both cases you want everything else patched, and this one package frozen until you decide to move it.
If you are still building the server, start with the first steps to secure a new Rocky Linux server. Come back once updates run on their own.
Install the versionlock plugin on Rocky Linux 9 and AlmaLinux 9
The plugin is a separate package in the distribution repositories. A minimal image does not include it.
sudo dnf install -y python3-dnf-plugin-versionlock
rpm -q python3-dnf-plugin-versionlock
cat /etc/dnf/plugins/versionlock.confrpm -q should print the package name followed by its version. If it prints package python3-dnf-plugin-versionlock is not installed, the install failed, so read the dnf output above it.
Two settings in the config file matter. enabled = 1 turns the plugin on for every dnf command. locklist names the file that holds your locks. If enabled reads 0, the plugin is off, so every lock you add is ignored.
This guide uses nginx from the AppStream repository as the example package. Install it if you want to follow along.
sudo dnf install -y nginx
rpm -qa 'nginx*'Lock nginx at its installed version
rpm -qa 'nginx*' shows that nginx is more than one package. The nginx package pulls in companion packages built from the same source. Lock the companions as well as nginx. The section on dependency failures below explains why.
sudo dnf versionlock add $(rpm -qa --queryformat '%{NAME}\n' 'nginx*')
dnf versionlock listThe rpm part prints the names of the installed nginx packages. dnf versionlock add then locks each one at the build that is installed now. For each package, dnf prints a line that starts with Adding versionlock on:, followed by the lock it wrote. dnf versionlock list prints the same locks. Each lock names the package, its version and its release, and ends in .*, which matches any CPU architecture.
The lock pins the release as well as the version. A rebuild with the same upstream version and a new release number is held back too. Distribution security fixes often arrive in exactly that form, as a new release of the same version. An exact pin exists to do this, and it is also what the pin costs you. The last section covers that cost.
Where the plugin stores the lock
cat /etc/dnf/plugins/versionlock.list/etc/dnf/plugins/versionlock.list is the default path, set by locklist in the config file. It is plain text with one lock per line, in the same form that dnf versionlock list prints. Lines that start with # are comments. The locks live in an ordinary file, so you can keep that file in git or in your configuration management and copy it to a rebuilt server.
Write down why each lock exists while you still remember. A comment line with the reason and a review date costs nothing:
echo '# nginx: third-party module built for this release. Review 2027-01-15.' | sudo tee -a /etc/dnf/plugins/versionlock.listWhat dnf check-update reports for a locked package
The plugin works by hiding newer builds. Before dnf works out a transaction, the plugin removes every version of a locked package except the locked one from the set of packages dnf can see. dnf never sees a newer nginx build as an option, so it reports nothing about it.
dnf check-update; echo "exit code: $?"
sudo dnf upgrade --assumenodnf check-update lists available updates. It exits with code 100 when there are updates and 0 when there are none. A locked package should be missing from that list. dnf upgrade --assumeno builds the full transaction, prints it and answers no, so nothing changes. The locked packages should be missing from the Upgrading section.
To see what the lock holds back, run the same check with the plugin turned off for one command:
dnf --disableplugin=versionlock check-update 'nginx*'Every package this lists is an update that the lock hides from you. If it lists no packages, no newer build exists yet, and the lock has cost you nothing so far.
Why does dnf upgrade fail after I lock one package?
Say you lock only nginx and leave its companion packages unlocked. The next upgrade can then stop with a dependency problem. The cause is in the package metadata. Packages built from one source often require each other at the exact same version and release. Check it on your own server:
rpm -q --requires nginx | grep nginxA line with an = and a full version means the locked nginx requires that companion at exactly that build. A newer companion would break that requirement, so dnf cannot install it.
EL9 sets best=True in /etc/dnf/dnf.conf. With that setting, dnf does not quietly keep the old companion. It stops, prints a Problem: block that names the package it could not upgrade, and suggests --nobest. Do not use --nobest as the fix, because it lets dnf skip other updates too. Lock the package that the Problem: block names, then run sudo dnf upgrade --assumeno again until it completes without a problem.
This matters more on a server with dnf-automatic. A transaction that cannot be solved installs nothing. One lock that misses a companion package can therefore stop every update on the server, security fixes included.
Does dnf-automatic leave a locked package alone?
This is the question that matters once updates run on a timer. Answer it on your own server rather than trust a guide. dnf-automatic is built on the same dnf library as the dnf command, and the versionlock plugin runs inside that library. You can check in a few minutes whether a dnf-automatic run on your image actually loads the plugin.
before=$(rpm -qa 'nginx*' | sort)
dnf --disableplugin=versionlock check-update 'nginx*'
sudo dnf-automatic --installupdates
after=$(rpm -qa 'nginx*' | sort)
if [ "$before" = "$after" ]; then echo "locks held"; else echo "nginx packages changed"; fiThe two variables hold the sorted list of installed nginx packages before and after the run, so the test catches a companion package that moved as well as nginx itself. The shell compares them directly, so you need no extra tool. The test only proves something when the check-update command lists a newer build. If it lists nothing, the lock has nothing to hold back, so repeat the test the first time it does. When a newer build exists, the last line must print locks held. If it prints nginx packages changed, dnf-automatic upgraded through your lock. Do not rely on the lock on that image until you know why.
The timer runs the same code without you watching. To read what it did, find the active timer with systemctl list-timers 'dnf-automatic*'. Then read the log of the matching service with journalctl -u, for example journalctl -u dnf-automatic-install.service -n 50. A Problem: block in that log is the dependency failure from the section above.
dnf versionlock or exclude= in dnf.conf: which one to use
Both stop a package from moving. They answer different questions.
versionlock pins one exact version and release. dnf still manages the package and can reinstall it, but only at the locked build. Use it when the instruction is "this build, until I say otherwise". The lock is per package, it lives in its own file, and --disableplugin=versionlock lifts it for one command.
exclude= hides packages by name pattern, at every version. It is a setting in the [main] section of /etc/dnf/dnf.conf. Add one line under [main]:
exclude=nginx*With that line, dnf acts as if no package whose name matches nginx* exists in any repository. It cannot install one, and it cannot upgrade one. --disableexcludes=main lifts it for one command. A repository file can apply the same filter to one repository only, as an excludepkgs= line in that repository's section.
Use exclude= when the instruction is "dnf must never take this package from here". The usual case is two repositories that both ship a package, where you want it from only one of them. Put excludepkgs= on the repository you do not want it from. This comes up once you add the EPEL and CRB repositories on Rocky and Alma or a vendor repository on top of the base ones.
versionlock has one more mode, which sits between the two. sudo dnf versionlock exclude followed by a full name, version and release hides that one build only. dnf writes it to the lock list as a line that starts with !. Use it to skip a single known-bad build while still accepting the next one.
The kernel is a special case. dnf keeps several kernels installed side by side (installonly_limit=3 in dnf.conf), and every kernel update is a new install rather than an upgrade. Freezing the kernel with either tool means the server keeps booting the kernel it has, without the fixes in newer ones. Use a kernel freeze only as a short pause while you test. Remove it once the test is done.
The cost: a pinned package stops getting security fixes
Every lock is an update you are not installing. On a server running dnf-automatic, that includes security fixes for the locked package. Nothing warns you, because the lock hides the newer build from the same reports that would tell you about it. So check for advisories with the plugin turned off:
dnf --disableplugin=versionlock updateinfo list --security 'nginx*'Each line names an advisory, its severity and the package build that fixes it. Advisory IDs start with RLSA- on Rocky Linux and ALSA- on AlmaLinux. To read one in full, run dnf updateinfo info followed by the ID. If it lists a CVE (Common Vulnerabilities and Exposures entry) that affects how you use the package, your review date is today.
Put the check where you will see it. A short script that prints your locks next to the pending security advisories makes a forgotten lock visible:
sudo tee /usr/local/bin/lock-report >/dev/null <<'EOF'
#!/bin/sh
echo "== Version locks =="
dnf -q versionlock list
echo "== Security advisories, locks ignored =="
dnf -q --disableplugin=versionlock updateinfo list --security
EOF
sudo chmod 755 /usr/local/bin/lock-report
sudo /usr/local/bin/lock-reportRun it weekly from cron or a systemd timer and mail yourself the output. You can also make the opposite choice: install every security fix and nothing else. Applying only security updates with dnf shows that setup.
Delete the lock when you are ready to move
sudo dnf versionlock delete $(rpm -qa --queryformat '%{NAME}\n' 'nginx*')
dnf versionlock list
sudo dnf upgrade --assumenoFor each lock it removes, dnf prints a line that starts with Deleting versionlock for:. The nginx entries should be gone from the list. --assumeno should now show the held-back builds in the Upgrading section. To remove every lock at once, use sudo dnf versionlock clear.
Run the real upgrade, then test the service. Have a way to undo it. Rolling back an update with dnf history undo returns the old build if the new one fails, and you can lock it again afterwards. A running nginx keeps the old code in memory until it restarts, so check which services need a restart after updates before you call the upgrade done.
Clear your locks before a major version upgrade, such as upgrading AlmaLinux 9 to 10 with ELevate. A lock names an EL9 build, and no EL10 repository carries that build.
FAQ
How do I pin a package version on Rocky Linux 9 or AlmaLinux 9?
Install the plugin with sudo dnf install -y python3-dnf-plugin-versionlock, then run sudo dnf versionlock add with the package name. dnf locks the installed version and release. Every later dnf upgrade leaves that package at that build. List the locks with dnf versionlock list. Lock the companion packages built from the same source too, or upgrades can fail with a dependency problem.
Does dnf-automatic respect dnf versionlock?
dnf-automatic uses the same dnf library that the versionlock plugin runs in, but test it on your own image instead of assuming it works. Wait until dnf --disableplugin=versionlock check-update shows a newer build of the locked package. Then record rpm -q for it, run sudo dnf-automatic --installupdates, and run rpm -q again. The two lines must match. If they differ, the lock did not hold for dnf-automatic on that server.
What is the difference between dnf versionlock and exclude in dnf.conf?
versionlock pins one exact version and release of a package, and dnf keeps managing the package at that build. exclude= in the [main] section of /etc/dnf/dnf.conf hides every package whose name matches a pattern, at any version, so dnf can neither install nor upgrade it. Use versionlock to hold a build while you test. Use exclude=, or excludepkgs= on one repository, to stop dnf from ever taking a package from a source.
Why does dnf upgrade fail with a Problem block after I lock a package?
The locked package often requires its companion packages at its own exact version and release, so dnf cannot upgrade those companions. EL9 sets best=True in /etc/dnf/dnf.conf, which turns that into an error instead of a silent skip. Check the requirement with rpm -q --requires on the locked package. Lock the package that the Problem: block names, and run sudo dnf upgrade --assumeno until it completes without a problem.
How do I see security updates for a package I have locked?
Run dnf --disableplugin=versionlock updateinfo list --security with the package name. Turning the plugin off for that one command stops the lock from hiding the newer build, so the advisories that fix it appear. Advisory IDs start with RLSA- on Rocky Linux and ALSA- on AlmaLinux. dnf updateinfo info followed by an ID prints the full advisory.