A hands-on manual for the package manager of the Red Hat family: look before you install, install and remove cleanly, update safely, undo a change from the history, add repositories, and hold a package back. Includes the errors and what they mean.
▶Prefer to watch? This short video runs this page's main commands in a real lab, with on-screen captions and a synthetic voiceover (an AI voice trained on the author's own recording). Click to play, or watch it on YouTube.
sudo dnf install tree # install a package and what it needs
sudo dnf upgrade # bring the whole system up to date
sudo dnf history undo last # take back the last change
dnf is the tool behind every install, update and removal on Rocky Linux, RHEL, AlmaLinux and Fedora. This guide goes through what you use every day, in the order you meet it: finding a package, installing it, removing it, updating the machine, reading the history, and the repositories that everything comes from.
Then the parts that rescue you: undoing a transaction, holding a package at one version, going back a version, reading the error when a repository or a package name is wrong. A table at the end sets dnf next to apt.
Every example ran for real, as an ordinary user with sudo, on Rocky Linux 9 (dnf 4.14.0, starting from the 9.3 image and updated to 9.8 along the way). The output you see is the output the machine gave.
What dnf is, and the lab
dnf is the package manager of Red Hat Enterprise Linux, Rocky Linux, AlmaLinux, CentOS Stream and Fedora. It installs, removes and updates software, works out which other packages each one needs, downloads everything from the repositories you have switched on, and checks the signature on every package before it installs it. Under the hood it installs .rpm files, the same format the packaging guides in this series build.
Every example below ran for real on Rocky Linux 9, as an ordinary user with sudo, in a clean container made from the rockylinux:9 image. Rocky 9 is rebuilt from the sources of Red Hat Enterprise Linux 9, so everything here works on RHEL 9 and AlmaLinux 9 too. The image was built in 2023 (Rocky Linux 9.3), long before the current repositories, which is useful: it gives the update section something real to do.
Start by asking the machine what it is and which dnf it has:
$ cat /etc/rocky-release
Rocky Linux release 9.3 (Blue Onyx)
$ dnf --version | head -1
4.14.0
$ ls -l /usr/bin/dnf /usr/bin/yum
lrwxrwxrwx 1 root root 5 Nov 1 2023 /usr/bin/dnf -> dnf-3
lrwxrwxrwx 1 root root 5 Nov 1 2023 /usr/bin/yum -> dnf-3
yum, the name of the old tool, is only a link to the same program, so every yum command you remember still works. This guide uses dnf.
dnf needs root to change anything, so the commands that install, remove or update start with sudo. The commands that only look (search, info, list, provides) work as an ordinary user.
Look before you install: search, info, provides
Four commands find software without changing anything. Search by word, over names and descriptions:
$ dnf search nmap
Rocky Linux 9 - BaseOS 2.3 MB/s | 45 MB 00:19
Rocky Linux 9 - AppStream 1.5 MB/s | 30 MB 00:20
Rocky Linux 9 - Extras 14 kB/s | 17 kB 00:01
========================== Name Exactly Matched: nmap ==========================
nmap.x86_64 : Network exploration tool and security scanner
========================= Name & Summary Matched: nmap =========================
nmap-ncat.x86_64 : Nmap's Netcat replacement
Show the details of one package: version, size, which repository it comes from, a description:
$ dnf info nmap
Last metadata expiration check: 0:00:02 ago on Tue Nov 3 15:44:18 2026.
Available Packages
Name : nmap
Epoch : 3
Version : 7.92
Release : 5.el9
Architecture : x86_64
Size : 5.4 M
Source : nmap-7.92-5.el9.src.rpm
Repository : appstream
Summary : Network exploration tool and security scanner
URL : http://nmap.org/
License : Nmap
Description : Nmap is a utility for network exploration or security auditing. It supports
: ping scanning (determine which hosts are up), many port scanning techniques
: (determine what services the hosts are offering), and TCP/IP fingerprinting
: (remote host operating system identification). Nmap also offers flexible target
: and port specification, decoy scanning, determination of TCP sequence
: predictability characteristics, reverse-identd scanning, and more. In addition
: to the classic command-line nmap executable, the Nmap suite includes a flexible
: data transfer, redirection, and debugging tool (netcat utility ncat), a utility
: for comparing scan results (ndiff), and a packet generation and response
: analysis tool (nping).
Find out which package supplies a file or a command. This is the answer to "dig: command not found". The pattern */bin/dig matches the file in any folder. The answer is bind-utils; head keeps the start of the output, which is the package and its repository:
$ dnf provides '*/bin/dig' | head -6
Last metadata expiration check: 0:00:02 ago on Tue Nov 3 15:44:18 2026.
bind-utils-32:9.16.23-40.el9_8.1.x86_64 : Utilities for querying DNS name servers
Repo : appstream
Matched from:
Filename : /usr/bin/dig
List what is available, or what is already installed. Both lists are long, so count them:
$ dnf list available | wc -l
7283
$ dnf list installed | wc -l
146
A package name can end in .noarch (no compiled code) or .x86_64 (built for 64-bit Intel and AMD CPUs). dnf list installed also shows, in the last column, where each package came from: @baseos or @appstream is the repository it was installed from, and @System means it was already on the system before dnf recorded where it came from.
Install a package
Without -y, dnf shows the plan and asks before it does anything. Answer n here, to see what it would do:
$ echo n | sudo dnf install tree
Last metadata expiration check: 0:01:09 ago on Tue Nov 3 15:43:13 2026.
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Installing:
tree x86_64 1.8.0-10.el9 baseos 55 k
Transaction Summary
================================================================================
Install 1 Package
Total download size: 55 k
Installed size: 113 k
Is this ok [y/N]: Operation aborted.
The table is the plan: the package, the repository, the version and the size. Then the question, and the answer, which stops the whole operation. For scripts, -y answers yes automatically:
$ sudo dnf install -y tree
Last metadata expiration check: 0:01:10 ago on Tue Nov 3 15:43:13 2026.
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Installing:
tree x86_64 1.8.0-10.el9 baseos 55 k
Transaction Summary
================================================================================
Install 1 Package
Total download size: 55 k
Installed size: 113 k
Downloading Packages:
tree-1.8.0-10.el9.x86_64.rpm 2.1 kB/s | 55 kB 00:26
--------------------------------------------------------------------------------
Total 2.1 kB/s | 55 kB 00:26
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Installing : tree-1.8.0-10.el9.x86_64 1/1
Running scriptlet: tree-1.8.0-10.el9.x86_64 1/1
Verifying : tree-1.8.0-10.el9.x86_64 1/1
Installed:
tree-1.8.0-10.el9.x86_64
Complete!
$ tree --version
tree v1.8.0 (c) 1996 - 2018 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro
$ rpm -q tree
tree-1.8.0-10.el9.x86_64
rpm -q asks the rpm database whether a package is installed and which version. dnf and rpm share that database; dnf is the part that fetches, resolves and installs, rpm is the part that unpacks the files.
A package with dependencies shows dnf's main job. --assumeno answers no for you: it prints the plan and stops, which is a safe way to look before you install. nmap needs a few libraries, and the table lists them under "Installing dependencies":
$ sudo dnf install -y --assumeno nmap
Last metadata expiration check: 0:01:38 ago on Tue Nov 3 15:43:13 2026.
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Installing:
nmap x86_64 3:7.92-5.el9 appstream 5.4 M
Installing dependencies:
libibverbs x86_64 61.0-2.el9 baseos 463 k
libnl3 x86_64 3.11.0-1.el9 baseos 358 k
libpcap x86_64 14:1.10.0-4.el9_8.1 baseos 171 k
nmap-ncat x86_64 3:7.92-5.el9 appstream 222 k
Transaction Summary
================================================================================
Install 5 Packages
Total download size: 6.6 M
Installed size: 27 M
Operation aborted.
(exit status 1)
Now install it for real. The output is long, so tail keeps the end of it:
$ sudo dnf install -y nmap | tail -12
Verifying : libibverbs-61.0-2.el9.x86_64 1/5
Verifying : libnl3-3.11.0-1.el9.x86_64 2/5
Verifying : libpcap-14:1.10.0-4.el9_8.1.x86_64 3/5
Verifying : nmap-3:7.92-5.el9.x86_64 4/5
Verifying : nmap-ncat-3:7.92-5.el9.x86_64 5/5
Installed:
libibverbs-61.0-2.el9.x86_64 libnl3-3.11.0-1.el9.x86_64
libpcap-14:1.10.0-4.el9_8.1.x86_64 nmap-3:7.92-5.el9.x86_64
nmap-ncat-3:7.92-5.el9.x86_64
Complete!
$ nmap --version | head -1
Nmap version 7.92 ( https://nmap.org )
Install a package file you downloaded or built yourself. dnf install ./file.rpm resolves its dependencies from the repositories, which rpm -i does not. dnf download fetches the file without installing it. It is not part of the core: a few commands live in plugins, in the package dnf-plugins-core, so install that first (the repositories section uses it again):
$ sudo dnf install -y dnf-plugins-core | tail -3
python3-systemd-234-19.el9.x86_64
Complete!
$ mkdir ~/rpms && cd ~/rpms
$ dnf download bc
Last metadata expiration check: 0:00:41 ago on Tue Nov 3 15:44:18 2026.
bc-1.07.1-14.el9.x86_64.rpm 102 kB/s | 118 kB 00:01
$ ls
bc-1.07.1-14.el9.x86_64.rpm
rpms
$ sudo dnf install -y ./bc-*.rpm | tail -4
Installed:
bc-1.07.1-14.el9.x86_64
Complete!
$ rpm -q bc
bc-1.07.1-14.el9.x86_64
Remove packages and clean up what is left
Remove a package by name. dnf also removes the dependencies that nothing else uses, because clean_requirements_on_remove is on in /etc/dnf/dnf.conf:
$ grep clean_requirements /etc/dnf/dnf.conf
clean_requirements_on_remove=True
$ sudo dnf remove -y nmap
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Removing:
nmap x86_64 3:7.92-5.el9 @appstream 24 M
Removing unused dependencies:
libibverbs x86_64 61.0-2.el9 @baseos 1.3 M
libnl3 x86_64 3.11.0-1.el9 @baseos 1.1 M
libpcap x86_64 14:1.10.0-4.el9_8.1 @baseos 397 k
nmap-ncat x86_64 3:7.92-5.el9 @appstream 469 k
Transaction Summary
================================================================================
Remove 5 Packages
Freed space: 27 M
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Erasing : nmap-3:7.92-5.el9.x86_64 1/5
Running scriptlet: nmap-ncat-3:7.92-5.el9.x86_64 2/5
Erasing : nmap-ncat-3:7.92-5.el9.x86_64 2/5
Erasing : libpcap-14:1.10.0-4.el9_8.1.x86_64 3/5
Erasing : libibverbs-61.0-2.el9.x86_64 4/5
Erasing : libnl3-3.11.0-1.el9.x86_64 5/5
Running scriptlet: libnl3-3.11.0-1.el9.x86_64 5/5
Verifying : libibverbs-61.0-2.el9.x86_64 1/5
Verifying : libnl3-3.11.0-1.el9.x86_64 2/5
Verifying : libpcap-14:1.10.0-4.el9_8.1.x86_64 3/5
Verifying : nmap-3:7.92-5.el9.x86_64 4/5
Verifying : nmap-ncat-3:7.92-5.el9.x86_64 5/5
Removed:
libibverbs-61.0-2.el9.x86_64 libnl3-3.11.0-1.el9.x86_64
libpcap-14:1.10.0-4.el9_8.1.x86_64 nmap-3:7.92-5.el9.x86_64
nmap-ncat-3:7.92-5.el9.x86_64
Complete!
Packages that came in only as dependencies of something you have since removed are called orphans. autoremove deletes them (here nothing is left, because remove already did it):
$ sudo dnf autoremove -y
Last metadata expiration check: 0:01:52 ago on Tue Nov 3 15:43:13 2026.
Dependencies resolved.
Nothing to do.
Complete!
dnf will not let you cut off the branch you are sitting on. Try to remove dnf itself:
$ sudo dnf remove dnf
Error:
Problem: The operation would result in removing the following protected packages: yum
(try to add '--skip-broken' to skip uninstallable packages or '--nobest' to use not only best candidate packages)
The error names the protected package, yum: the dnf command is only a link to the same program, so removing either would take the package manager away. It is a safety net, not a bug: a system without its package manager, or without systemd, is a system you rebuild. Remove the other test packages and check that they are gone:
$ sudo dnf remove -y tree bc
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Removing:
bc x86_64 1.07.1-14.el9 @@commandline 226 k
tree x86_64 1.8.0-10.el9 @baseos 113 k
Transaction Summary
================================================================================
Remove 2 Packages
Freed space: 339 k
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Erasing : tree-1.8.0-10.el9.x86_64 1/2
Erasing : bc-1.07.1-14.el9.x86_64 2/2
Running scriptlet: bc-1.07.1-14.el9.x86_64 2/2
Verifying : bc-1.07.1-14.el9.x86_64 1/2
Verifying : tree-1.8.0-10.el9.x86_64 2/2
Removed:
bc-1.07.1-14.el9.x86_64 tree-1.8.0-10.el9.x86_64
Complete!
$ rpm -q tree bc
package tree is not installed
package bc is not installed
(exit status 2)
Bring the system up to date
The image used for this guide was built some time ago, so there is a lot to update, which makes it a good example. First ask what is waiting. dnf check-update lists the updates; its exit status is 100 when there are updates and 0 when there are none, which is what scripts use:
$ dnf check-update | tail -n +2 | head -8
alternatives.x86_64 1.24-2.el9 baseos
audit-libs.x86_64 3.1.5-8.el9 baseos
basesystem.noarch 11-13.el9.0.1 baseos
bash.x86_64 5.1.8-9.el9 baseos
binutils.x86_64 2.35.2-72.el9 baseos
binutils-gold.x86_64 2.35.2-72.el9 baseos
bzip2-libs.x86_64 1.0.8-11.el9 baseos
$ dnf check-update > /dev/null; echo "exit status $?"
exit status 100
Count the updates by type. Rocky publishes security notices (errata) for its packages, and dnf can read them:
$ dnf updateinfo summary
Last metadata expiration check: 0:00:52 ago on Tue Nov 3 15:44:18 2026.
Updates Information Summary: available
142 Security notice(s)
1 Critical Security notice(s)
52 Important Security notice(s)
80 Moderate Security notice(s)
9 Low Security notice(s)
56 Bugfix notice(s)
3 Enhancement notice(s)
Update one package first, to see what an upgrade looks like. The table says Upgrading, and the Cleanup line in the output names the old version that is being replaced:
$ sudo dnf upgrade -y tzdata
Last metadata expiration check: 0:01:58 ago on Tue Nov 3 15:43:13 2026.
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Upgrading:
tzdata noarch 2026e-1.el9_8 baseos 499 k
Transaction Summary
================================================================================
Upgrade 1 Package
Total download size: 499 k
Downloading Packages:
tzdata-2026e-1.el9_8.noarch.rpm 330 kB/s | 499 kB 00:01
--------------------------------------------------------------------------------
Total 277 kB/s | 499 kB 00:01
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Upgrading : tzdata-2026e-1.el9_8.noarch 1/2
Cleanup : tzdata-2023c-1.el9.noarch 2/2
Verifying : tzdata-2026e-1.el9_8.noarch 1/2
Verifying : tzdata-2023c-1.el9.noarch 2/2
Upgraded:
tzdata-2026e-1.el9_8.noarch
Complete!
Update only the packages that fix a security problem. This is the safest way to stay current on a server you do not want to change much:
$ sudo dnf upgrade -y --security 2>&1 | tail -6
xz-libs-5.2.5-8.el9_8.1.x86_64
Installed:
gawk-all-langpacks-5.1.0-6.el9_8.1.x86_64
openssl-fips-provider-1:3.5.8-2.el9_8.x86_64
Complete!
Then update everything. The -y answers yes, and tail shows the end of the long output:
$ sudo dnf upgrade -y 2>&1 | tail -6
yum-4.14.0-34.el9_8.rocky.0.1.noarch
Installed:
attr-2.6.0-1.el9_8.x86_64 keyutils-1.6.3-1.el9.x86_64
libtool-ltdl-2.4.6-46.el9.x86_64 libxcrypt-compat-4.4.18-3.el9.x86_64
Complete!
$ cat /etc/rocky-release
Rocky Linux release 9.8 (Blue Onyx)
$ dnf check-update > /dev/null; echo "exit status $?"
exit status 100
The exit status can still be 100 right after an upgrade: the mirrors published newer builds (curl, as the exclude example below shows) while the upgrade ran, so run it once more. The release changed because Rocky 9 is one stream: a minor release is just a set of updates, and dnf upgrade moves the machine to the newest one. A new kernel only runs after a reboot; the old one stays installed, as the fallback (installonly_limit=3 keeps the last three).
History: see what changed, and undo it
dnf records every transaction. List them: the ID, the command, the date and how many packages were touched:
$ sudo dnf history | head -12
ID | Command line | Date and time | Action(s) | Altered
-------------------------------------------------------------------------------
10 | upgrade -y | 2026-11-03 15:45 | I, U | 56 EE
9 | upgrade -y --security | 2026-11-03 15:45 | I, U | 60
8 | upgrade -y tzdata | 2026-11-03 15:45 | Upgrade | 1
7 | remove -y tree bc | 2026-11-03 15:45 | Removed | 2
6 | remove -y nmap | 2026-11-03 15:45 | Removed | 5
5 | install -y ./bc-1.07.1-1 | 2026-11-03 15:45 | Install | 1
4 | install -y dnf-plugins-c | 2026-11-03 15:44 | Install | 7
3 | install -y nmap | 2026-11-03 15:44 | Install | 5
2 | install -y tree | 2026-11-03 15:44 | Install | 1
1 | -y -q install sudo man-d | 2026-11-03 15:43 | Install | 4
Make a change worth undoing, install a package:
$ sudo dnf install -y tree
Last metadata expiration check: 0:02:38 ago on Tue Nov 3 15:43:13 2026.
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Installing:
tree x86_64 1.8.0-10.el9 baseos 55 k
Transaction Summary
================================================================================
Install 1 Package
Total download size: 55 k
Installed size: 113 k
Downloading Packages:
tree-1.8.0-10.el9.x86_64.rpm 67 kB/s | 55 kB 00:00
--------------------------------------------------------------------------------
Total 57 kB/s | 55 kB 00:00
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Installing : tree-1.8.0-10.el9.x86_64 1/1
Running scriptlet: tree-1.8.0-10.el9.x86_64 1/1
Verifying : tree-1.8.0-10.el9.x86_64 1/1
Installed:
tree-1.8.0-10.el9.x86_64
Complete!
Show the details of the last transaction. Every package it touched and the action are listed:
$ sudo dnf history info last
Transaction ID : 11
Begin time : Tue Nov 3 15:45:52 2026
Begin rpmdb : 70340a773242cab8379f95da7bc64af82df752cb98e282e92b5ee7ebc56c7466
End time : Tue Nov 3 15:45:52 2026 (0 seconds)
End rpmdb : 14fe7fca6a51eee65dbfa2cbcedf79a3e35d16d850eacbaf0f8fa8a60285370b
User : System <unset>
Return-Code : Success
Releasever : 9
Command Line : install -y tree
Persistence : Persist
Comment :
Packages Altered:
Install tree-1.8.0-10.el9.x86_64 @baseos
Undo it. dnf does the opposite of every action in that transaction (a remove, here):
$ sudo dnf history undo -y last
Last metadata expiration check: 0:02:41 ago on Tue Nov 3 15:43:13 2026.
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Removing:
tree x86_64 1.8.0-10.el9 @baseos 113 k
Transaction Summary
================================================================================
Remove 1 Package
Freed space: 113 k
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Erasing : tree-1.8.0-10.el9.x86_64 1/1
Running scriptlet: tree-1.8.0-10.el9.x86_64 1/1
Verifying : tree-1.8.0-10.el9.x86_64 1/1
Removed:
tree-1.8.0-10.el9.x86_64
Complete!
$ rpm -q tree
package tree is not installed
(exit status 1)
The undo is itself a new transaction in the list, so you can undo the undo:
$ sudo dnf history | head -4
ID | Command line | Date and time | Action(s) | Altered
-------------------------------------------------------------------------------
12 | history undo -y last | 2026-11-03 15:45 | Removed | 1
11 | install -y tree | 2026-11-03 15:45 | Install | 1
history undo reverses one transaction. history rollback N reverses everything after transaction N, which can be a lot of work, so read the list first. This is the answer to "I installed something yesterday and now something is broken": the history says exactly what changed and when.
Repositories: what is switched on
Everything dnf installs comes from a repository: a web folder with packages and an index. The definitions are in /etc/yum.repos.d. List the repositories that are enabled, then all of them:
$ dnf repolist
repo id repo name
appstream Rocky Linux 9 - AppStream
baseos Rocky Linux 9 - BaseOS
extras Rocky Linux 9 - Extras
$ dnf repolist --all | head -12
repo id repo name status
appstream Rocky Linux 9 - AppStream enabled
appstream-debuginfo Rocky Linux 9 - AppStream - Debug disabled
appstream-source Rocky Linux 9 - AppStream - Source disabled
baseos Rocky Linux 9 - BaseOS enabled
baseos-debuginfo Rocky Linux 9 - BaseOS - Debug disabled
baseos-source Rocky Linux 9 - BaseOS - Source disabled
crb Rocky Linux 9 - CRB disabled
crb-debuginfo Rocky Linux 9 - CRB - Debug disabled
crb-source Rocky Linux 9 - CRB - Source disabled
devel Rocky Linux 9 - Devel WARNING! FOR BUILDROOT disabled
devel-debuginfo Rocky Linux 9 - Devel Debug WARNING! FOR BUI disabled
A repository file is plain text. Look at the start of Rocky's own:
$ grep -A8 '^\[baseos\]' /etc/yum.repos.d/rocky.repo
[baseos]
name=Rocky Linux $releasever - BaseOS
mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=BaseOS-$releasever$rltype
#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/BaseOS/$basearch/os/
gpgcheck=1
enabled=1
countme=1
metadata_expire=6h
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9
enabled=1 switches a repository on, gpgcheck=1 makes dnf verify every package against the key in gpgkey. The extra repositories (crb, devel, plus and others) are there but off.
Switch one on or off without editing the file. This is another plugin command, from the dnf-plugins-core package you installed earlier:
$ sudo dnf config-manager --set-enabled crb
$ dnf repolist
repo id repo name
appstream Rocky Linux 9 - AppStream
baseos Rocky Linux 9 - BaseOS
crb Rocky Linux 9 - CRB
extras Rocky Linux 9 - Extras
crb is CodeReady Builder: libraries and headers that other repositories, EPEL for one, depend on. Turn it off again for now, because nothing here needs it:
$ sudo dnf config-manager --set-disabled crb
$ dnf repolist
repo id repo name
appstream Rocky Linux 9 - AppStream
baseos Rocky Linux 9 - BaseOS
extras Rocky Linux 9 - Extras
To use a repository for one command only, without changing any file, enable it on the command line:
$ dnf --enablerepo=crb repoquery --info libuv-devel | head -4
Rocky Linux 9 - BaseOS 4.0 kB/s | 4.3 kB 00:01
Rocky Linux 9 - AppStream 5.4 kB/s | 4.8 kB 00:00
Rocky Linux 9 - CRB 4.8 MB/s | 6.4 MB 00:01
Name : libuv-devel
Epoch : 1
Version : 1.42.0
Release : 2.el9_4
EPEL: thousands more packages
The standard repositories carry what Red Hat ships. Search for a tool that is not part of it, htop:
$ dnf info htop
Last metadata expiration check: 0:00:05 ago on Tue Nov 3 15:46:02 2026.
Error: No matching Packages to list
Not found. EPEL (Extra Packages for Enterprise Linux) is the Fedora project's add-on repository for RHEL and its rebuilds. On Rocky it is one package in the extras repository, which installs the repository file and its signing key:
$ sudo dnf install -y epel-release
Rocky Linux 9 - BaseOS 7.3 kB/s | 4.3 kB 00:00
Rocky Linux 9 - BaseOS 19 MB/s | 45 MB 00:02
Rocky Linux 9 - AppStream 5.6 kB/s | 4.8 kB 00:00
Rocky Linux 9 - AppStream 774 kB/s | 30 MB 00:39
Last metadata expiration check: 0:00:04 ago on Tue Nov 3 15:46:54 2026.
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Installing:
epel-release noarch 9-10.el9 extras 19 k
Transaction Summary
================================================================================
Install 1 Package
Total download size: 19 k
Installed size: 26 k
Downloading Packages:
epel-release-9-10.el9.noarch.rpm 27 kB/s | 19 kB 00:00
--------------------------------------------------------------------------------
Total 23 kB/s | 19 kB 00:00
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Installing : epel-release-9-10.el9.noarch 1/1
Running scriptlet: epel-release-9-10.el9.noarch 1/1
Many EPEL packages require the CodeReady Builder (CRB) repository.
It is recommended that you run /usr/bin/crb enable to enable the CRB repository.
Verifying : epel-release-9-10.el9.noarch 1/1
Installed:
epel-release-9-10.el9.noarch
Complete!
$ dnf repolist
repo id repo name
appstream Rocky Linux 9 - AppStream
baseos Rocky Linux 9 - BaseOS
epel Extra Packages for Enterprise Linux 9 - x86_64
epel-cisco-openh264 Extra Packages for Enterprise Linux 9 openh264 (From Cisco) - x86_64
extras Rocky Linux 9 - Extras
The same search now works. Check where the package would come from before you install it:
$ dnf info htop | grep -E "^(Name|Version|Repository|From repo)"
Name : htop
Version : 3.3.0
Repository : epel
$ sudo dnf install -y htop
Extra Packages for Enterprise Linux 9 - x86_64 4.7 MB/s | 20 MB 00:04
Extra Packages for Enterprise Linux 9 openh264 1.6 kB/s | 2.5 kB 00:01
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Installing:
htop x86_64 3.3.0-1.el9 epel 198 k
Installing dependencies:
hwloc-libs x86_64 2.4.1-6.el9_7 baseos 2.1 M
libnl3 x86_64 3.11.0-1.el9 baseos 358 k
Transaction Summary
================================================================================
Install 3 Packages
Total download size: 2.6 M
Installed size: 4.5 M
Downloading Packages:
(1/3): htop-3.3.0-1.el9.x86_64.rpm 1.1 MB/s | 198 kB 00:00
(2/3): libnl3-3.11.0-1.el9.x86_64.rpm 461 kB/s | 358 kB 00:00
(3/3): hwloc-libs-2.4.1-6.el9_7.x86_64.rpm 2.6 MB/s | 2.1 MB 00:00
--------------------------------------------------------------------------------
Total 1.5 MB/s | 2.6 MB 00:01
Extra Packages for Enterprise Linux 9 - x86_64 1.6 MB/s | 1.6 kB 00:00
Importing GPG key 0x3228467C:
Userid : "Fedora (epel9) <epel@fedoraproject.org>"
Fingerprint: FF8A D134 4597 106E CE81 3B91 8A38 72BF 3228 467C
From : /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-9
Key imported successfully
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Installing : libnl3-3.11.0-1.el9.x86_64 1/3
Installing : hwloc-libs-2.4.1-6.el9_7.x86_64 2/3
Installing : htop-3.3.0-1.el9.x86_64 3/3
Running scriptlet: htop-3.3.0-1.el9.x86_64 3/3
Verifying : htop-3.3.0-1.el9.x86_64 1/3
Verifying : hwloc-libs-2.4.1-6.el9_7.x86_64 2/3
Verifying : libnl3-3.11.0-1.el9.x86_64 3/3
Installed:
htop-3.3.0-1.el9.x86_64 hwloc-libs-2.4.1-6.el9_7.x86_64
libnl3-3.11.0-1.el9.x86_64
Complete!
$ rpm -q htop
htop-3.3.0-1.el9.x86_64
$ dnf info htop | grep -E "^(Repository|From repo)"
Repository : @System
From repo : epel
The first install of a package from a new repository asks you to import the repository's GPG key (with -y it is accepted for you; read the fingerprint when you do it by hand). EPEL packages are community packages with no Red Hat support, so keep a note of what comes from it: dnf list installed shows @epel in the last column.
Groups and modules
A group installs a whole set of packages with one name: a development toolchain, a desktop, a set of console tools. List them, and look inside one:
$ dnf group list
Last metadata expiration check: 0:00:29 ago on Tue Nov 3 15:47:18 2026.
Available Environment Groups:
Server with GUI
Server
Minimal Install
Workstation
KDE Plasma Workspaces
Custom Operating System
Virtualization Host
Available Groups:
Fedora Packager
VideoLAN Client
Xfce
Legacy UNIX Compatibility
Console Internet Tools
Container Management
Development Tools
.NET Development
Graphical Administration Tools
Headless Management
Network Servers
RPM Development Tools
Scientific Support
Security Tools
Smart Card Support
System Tools
$ dnf group info "RPM Development Tools"
Last metadata expiration check: 0:00:30 ago on Tue Nov 3 15:47:18 2026.
Group: RPM Development Tools
Description: Tools used for building RPMs, such as rpmbuild.
Mandatory Packages:
redhat-rpm-config
rpm-build
Default Packages:
rpmdevtools
Optional Packages:
rpmlint
The info lists Mandatory, Default and Optional packages. group install installs the mandatory and the default ones; the optional ones are added only with --with-optional. This group is the toolbox of the packaging guides in this series:
$ sudo dnf group install -y "RPM Development Tools" | tail -5
xz-5.2.5-8.el9_8.1.x86_64
zip-3.0-35.el9.x86_64
zstd-1.5.5-1.el9.x86_64
Complete!
$ rpm -q rpm-build rpmdevtools rpmlint
rpm-build-4.16.1.3-40.el9.x86_64
rpmdevtools-9.5-1.el9.noarch
package rpmlint is not installed
(exit status 1)
rpmlint was optional, so it is not there. group remove takes the group's packages out again:
$ sudo dnf group remove -y "RPM Development Tools" | tail -3
zstd-1.5.5-1.el9.x86_64
Complete!
$ rpm -q rpm-build
package rpm-build is not installed
(exit status 1)
Another group worth knowing is Development Tools, for compiling software. A module is something different: a package stream, so that several versions of the same software can be offered side by side. nginx has three:
$ dnf module list nginx
Last metadata expiration check: 0:00:49 ago on Tue Nov 3 15:47:18 2026.
Rocky Linux 9 - AppStream
Name Stream Profiles Summary
nginx 1.22 common [d] nginx webserver
nginx 1.24 common [d] nginx webserver
nginx 1.26 common [d] nginx webserver
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled
The [d] marks the default stream. Choose a version by enabling its stream, then install with the usual command:
$ sudo dnf module enable -y nginx:1.26
Last metadata expiration check: 0:00:29 ago on Tue Nov 3 15:47:39 2026.
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Enabling module streams:
nginx 1.26
Transaction Summary
================================================================================
Complete!
$ sudo dnf install -y nginx | tail -4
systemd-pam-252-67.el9_8.6.rocky.0.1.x86_64
systemd-rpm-macros-252-67.el9_8.6.rocky.0.1.noarch
Complete!
$ nginx -v
nginx version: nginx/1.26.3
Switching to another stream takes a reset first, so that dnf forgets the choice, and you reinstall the package for the new stream:
$ dnf module list nginx
Last metadata expiration check: 0:00:58 ago on Tue Nov 3 15:47:18 2026.
Rocky Linux 9 - AppStream
Name Stream Profiles Summary
nginx 1.22 common [d] nginx webserver
nginx 1.24 common [d] nginx webserver
nginx 1.26 [e] common [d] nginx webserver
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled
$ sudo dnf remove -y nginx | tail -3
rocky-logos-httpd-90.17-1.el9.noarch
Complete!
$ sudo dnf module reset -y nginx
Last metadata expiration check: 0:00:39 ago on Tue Nov 3 15:47:39 2026.
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Resetting modules:
nginx
Transaction Summary
================================================================================
Complete!
$ dnf module list nginx
Last metadata expiration check: 0:01:02 ago on Tue Nov 3 15:47:18 2026.
Rocky Linux 9 - AppStream
Name Stream Profiles Summary
nginx 1.22 common [d] nginx webserver
nginx 1.24 common [d] nginx webserver
nginx 1.26 common [d] nginx webserver
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled
Hold a package, exclude one, go back a version
Sometimes an update is the problem. First, the lighter tool: leave a package out of one update run with --exclude, which takes a name or a pattern (here, never touch kernel packages):
$ sudo dnf upgrade -y --exclude='kernel*' | tail -3
libcurl-minimal-7.76.1-40.el9_8.9.x86_64
Complete!
To go back one version after an update, downgrade installs the previous version from the repository. Rocky's repositories keep several older builds of each package, which is why this works, but not forever:
$ sudo dnf downgrade -y tzdata | tail -4
Downgraded:
tzdata-2026c-1.el9_8.noarch
Complete!
$ rpm -q tzdata
tzdata-2026c-1.el9_8.noarch
Now the machine runs the older tzdata, and the next upgrade would replace it again. To keep it there, the versionlock plugin holds a package at its installed version:
$ sudo dnf install -y python3-dnf-plugin-versionlock | tail -2
Complete!
$ sudo dnf versionlock add tzdata
Last metadata expiration check: 0:00:52 ago on Tue Nov 3 15:47:39 2026.
Adding versionlock on: tzdata-0:2026c-1.el9_8.*
$ sudo dnf versionlock list
Last metadata expiration check: 0:00:53 ago on Tue Nov 3 15:47:39 2026.
tzdata-0:2026c-1.el9_8.*
$ sudo dnf upgrade -y tzdata
Last metadata expiration check: 0:00:54 ago on Tue Nov 3 15:47:39 2026.
Dependencies resolved.
Nothing to do.
Complete!
The upgrade finds nothing to do, because the lock hides the newer builds from dnf. A lock is a decision you have to remember, so list it now and then, and remove it when the reason is gone:
$ sudo dnf versionlock delete tzdata
Last metadata expiration check: 0:00:56 ago on Tue Nov 3 15:47:39 2026.
Deleting versionlock for: tzdata-0:2026c-1.el9_8.*
$ sudo dnf versionlock list
Last metadata expiration check: 0:00:57 ago on Tue Nov 3 15:47:39 2026.
$ sudo dnf upgrade -y tzdata | tail -3
tzdata-2026e-1.el9_8.noarch
Complete!
$ rpm -q tzdata
tzdata-2026e-1.el9_8.noarch
The history undo of the earlier section is the other way back, and the better one when the change was a whole transaction and not one package.
The cache and the configuration
dnf keeps the repository indexes and the downloaded packages in /var/cache/dnf. See how much space that takes, then clean it:
$ sudo du -sh /var/cache/dnf
191M /var/cache/dnf
$ sudo dnf clean all
45 files removed
$ sudo du -sh /var/cache/dnf
4.1M /var/cache/dnf
clean all removes the cache; clean packages removes only the downloaded .rpm files. The next command rebuilds the indexes, which is what makecache does ahead of time:
$ sudo dnf makecache 2>&1 | tail -3
Rocky Linux 9 - AppStream 4.7 MB/s | 30 MB 00:06
Rocky Linux 9 - Extras 2.2 kB/s | 17 kB 00:07
Metadata cache created.
dnf checks for new index files on its own: the line "Last metadata expiration check" is that. To force a fresh look at the repositories for one command, add --refresh:
$ dnf --refresh check-update > /dev/null; echo "exit status $?"
exit status 0
The settings live in /etc/dnf/dnf.conf. The default file is short. Two options that help on a real server are max_parallel_downloads and fastestmirror:
$ cat /etc/dnf/dnf.conf
[main]
gpgcheck=1
installonly_limit=3
clean_requirements_on_remove=True
best=True
skip_if_unavailable=False
Add them to the end of the file, and the next update downloads several packages at once:
$ sudo sh -c 'printf "max_parallel_downloads=10\nfastestmirror=True\n" >> /etc/dnf/dnf.conf'
$ cat /etc/dnf/dnf.conf
[main]
gpgcheck=1
installonly_limit=3
clean_requirements_on_remove=True
best=True
skip_if_unavailable=False
max_parallel_downloads=10
fastestmirror=True
Any setting can also be tried for one run, without touching the file, with --setopt:
$ dnf --setopt=max_parallel_downloads=1 check-update > /dev/null; echo "exit status $?"
exit status 0
That is the whole set of knobs you need most days; everything else in the file is documented in man dnf.conf.
dnf next to yum and apt
If you come from Debian or Ubuntu, the verbs are different but the jobs are the same. This is the table the guide's examples add up to:
apt update dnf makecache (dnf does it by itself when needed)
apt upgrade dnf upgrade
apt install pkg dnf install pkg
apt remove pkg dnf remove pkg
apt autoremove dnf autoremove
apt search word dnf search word
apt show pkg dnf info pkg
dpkg -S /path/file dnf provides /path/file
dpkg -l dnf list installed
apt-mark hold pkg dnf versionlock add pkg
/var/log/apt/history dnf history
Two differences are worth knowing. dnf upgrade also fetches new index files, so there is no separate update step, and dnf remove removes the dependencies that are no longer needed, where apt remove leaves them for autoremove.
yum is the older name, a link to the same program, as the first section showed. A script that calls yum keeps working on Rocky 9. For new scripts use dnf, which is the name the documentation uses.
When something goes wrong
The common messages, with a real example of each. A name that does not exist, or a typo:
$ sudo dnf install -y notarealpackage
Last metadata expiration check: 0:00:09 ago on Tue Nov 3 16:00:15 2026.
No match for argument: notarealpackage
Error: Unable to find a match: notarealpackage
The two lines say the same thing, and the second one is the failure. Search for part of the name with dnf search, or find the file that you need with dnf provides.
A repository that cannot be reached shows as a metadata error. Make one on purpose, with a repository file that points nowhere, and run a command that uses it:
$ sudo sh -c 'printf "[broken]\nname=Broken\nbaseurl=http://repo.invalid/el9\nenabled=1\ngpgcheck=0\n" > /etc/yum.repos.d/broken.repo'
$ sudo dnf makecache 2>&1 | tail -4
Broken 0.0 B/s | 0 B 00:00
Errors during downloading metadata for repository 'broken':
- Curl error (6): Couldn't resolve host name for http://repo.invalid/el9/repodata/repomd.xml [Could not resolve host: repo.invalid]
Error: Failed to download metadata for repo 'broken': Cannot download repomd.xml: Cannot download repodata/repomd.xml: All mirrors were tried
"Failed to download metadata for repo" names the repository that failed. The fix is to correct the URL, or to switch the repository off for one command with --disablerepo=broken, or to set skip_if_unavailable=True in the repository section so that dnf carries on without it. Remove the broken file:
$ sudo rm /etc/yum.repos.d/broken.repo
$ dnf repolist
repo id repo name
appstream Rocky Linux 9 - AppStream
baseos Rocky Linux 9 - BaseOS
epel Extra Packages for Enterprise Linux 9 - x86_64
epel-cisco-openh264 Extra Packages for Enterprise Linux 9 openh264 (From Cisco) - x86_64
extras Rocky Linux 9 - Extras
Two dnf commands at once give a lock error ("Waiting for process with pid ... to finish"); wait for the other one, it clears itself. A package whose signature does not verify stops with "GPG check FAILED": do not get past it with --nogpgcheck. Find out why; the signed-repository guide in this series shows what a failed check looks like and what causes it.
To see what is inside any package that is installed, and to check that its files are unchanged, ask rpm:
$ rpm -ql htop | head -5
/usr/bin/htop
/usr/lib/.build-id
/usr/lib/.build-id/40
/usr/lib/.build-id/40/903c7141f92e6a18eb9f2d4a48b9f2beee065e
/usr/share/applications/htop.desktop
$ rpm -V htop; echo "exit status $?"
exit status 0
No output from rpm -V is the good answer. A letter in the first column of an output line, S for size or 5 for checksum, shows which part of a file differs from the package.