📦 dnf Package Manager Guide

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.

Video: dnf Package Manager Guide (Sysadmin video)
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.