RHEL Basics #3: dnf and Package Management — repo, modules, AppStream

12 min read

In #2 we got a RHEL machine running and registered. Next up: what we install and remove on top of it. Package management on RHEL is unified under one command, dnf — the equivalent of Ubuntu’s apt.

Where this post sits in the RHEL Basics series:

rpm / yum / dnf — one-line summary #

Three words people get confused about, sorted out in a single table:

ToolWhat it handlesDescription
rpmA single .rpm fileNo automatic dependency resolution. Used occasionally to install a .rpm you have on hand.
yumRepos + dependenciesThe standard on RHEL 5–7. From RHEL 8 onward it’s effectively a nickname for dnf.
dnfRepos + dependencies + modulesThe standard since RHEL 8. yum rewritten in Python.

On RHEL 9 the yum command still works — it’s just a symlink to dnf. New material and new commands like dnf module are dnf-only, so it’s better to use dnf everywhere.

check
$ which yum
/usr/bin/yum
$ ls -l /usr/bin/yum
lrwxrwxrwx. 1 root root 5 ... /usr/bin/yum -> dnf-3

Daily dnf commands #

Starting with the ones you’ll meet first.

Search — search / info / list #

finding packages
$ dnf search nginx
=================== Name & Summary Matched: nginx ===================
nginx.aarch64 : A high performance web server and reverse proxy server
nginx-mod-http-image-filter.aarch64 : Nginx HTTP image filter module
...

$ dnf info nginx
Name         : nginx
Version      : 1.20.1
Release      : 20.el9
Architecture : aarch64
Size         : 593 k
Source       : nginx-1.20.1-20.el9.src.rpm
Repository   : rhel-9-for-aarch64-appstream-rpms
Summary      : A high performance web server and reverse proxy server
URL          : https://nginx.org
License      : BSD
Description  : ...

info packs more useful detail and you’ll reach for it more often than search. The Repository line tells you where it comes from — AppStream, in this case.

lists
$ dnf list installed | head           # installed packages
$ dnf list available nginx*           # installable packages
$ dnf list updates                    # packages with updates

Install — install #

install
$ sudo dnf install nginx
Dependencies resolved.
============================================================
 Package                Arch     Version           Repository      Size
============================================================
Installing:
 nginx                  aarch64  1:1.20.1-20.el9   appstream      593 k
Installing dependencies:
 nginx-filesystem       noarch   1:1.20.1-20.el9   appstream       11 k
 ...

Transaction Summary
============================================================
Install  6 Packages

Total download size: 1.3 M
Installed size: 4.2 M
Is this ok [y/N]:

You can install several at once (dnf install nginx git vim); dependencies tag along. -y skips the confirmation. Good for automation, but for hand-driven installs it’s healthier to look at the dependency list once before saying yes.

local .rpm install
$ sudo dnf install ./some-package.rpm

Even a single downloaded .rpm resolves dependencies properly through dnf install. It beats rpm -i almost every time.

Remove — remove #

remove
$ sudo dnf remove nginx
Dependencies resolved.
=========================================================
Removing:
 nginx              ...
Removing unused dependencies:
 nginx-filesystem   ...

Dependencies that came along during install get cleaned up too if nothing else uses them (autoremove behavior is built in) — one step instead of apt remove + apt autoremove on Ubuntu.

Watch out — if dnf remove tries to take core packages (e.g. systemd, kernel) with it, stop and re-read the prompt. With dependencies tangled across the system, removing one package can leave it unable to boot. Never blindly say yes to a list you don’t recognize.

Update — update / upgrade #

update
$ sudo dnf check-update           # show available updates only (no changes)
$ sudo dnf update                 # bring everything to latest
$ sudo dnf update nginx           # update one package

update and upgrade are effectively the same on RHEL/dnf (other distros differ). update is the more common one.

Tracing dependencies — repoquery #

The deep-dive command.

repoquery
$ dnf repoquery --requires nginx       # what nginx depends on
$ dnf repoquery --whatrequires nginx   # what depends on nginx
$ dnf repoquery -l nginx               # files installed by the package
$ dnf repoquery --provides nginx       # virtual names the package provides

dnf repoquery -l <pkg> in particular — figuring out which package laid down which file is something you reach for a lot.

dnf history — rewinding time #

The most distinctly RHEL feature of the bunch. Every dnf transaction is logged, and you can roll back at the transaction level.

view history
$ sudo dnf history
ID  | Command line          | Date and time    | Action(s)   | Altered
----------------------------------------------------------------------
 12 | install nginx          | 2026-04-11 14:23 | Install     |    6
 11 | update -y              | 2026-04-11 09:01 | I, U        |   42
 10 | install git vim        | 2026-04-10 17:45 | Install     |    8
  9 | system upgrade ...     | 2026-04-10 16:02 | Install     |  362
one transaction in detail
$ sudo dnf history info 12
Transaction ID : 12
Begin time     : 2026-04-11 14:23:08
Command Line   : install nginx
Packages Altered:
    Install nginx-1:1.20.1-20.el9.aarch64        @appstream
    Install nginx-filesystem-1:1.20.1-20.el9.noarch  @appstream
    ...
undo / redo
$ sudo dnf history undo 12      # revert transaction 12
$ sudo dnf history redo 12      # apply it again

The single most-reached-for command in production is dnf history right after an incident — “what did I just change?” gets answered there. apt doesn’t have anything like it.

BaseOS and AppStream — why they’re separate repos #

Running dnf repolist (back in #2) showed two lines:

repo list
$ dnf repolist
repo id                               repo name
rhel-9-for-aarch64-appstream-rpms     Red Hat Enterprise Linux 9 - AppStream
rhel-9-for-aarch64-baseos-rpms        Red Hat Enterprise Linux 9 - BaseOS

There’s a reason for the split.

BaseOS — the unchanging OS core #

What's in BaseOS
kernel / glibc / systemd / coreutils / openssl / NetworkManager
        ↑ the parts an OS needs to act like an OS

Major versions barely change for the entire 10-year lifecycle of RHEL 9. Only security patches go in. “Stability of running systems” is the BaseOS promise.

AppStream — applications that can move faster #

What's in AppStream
PostgreSQL / Python / Node.js / nginx / Apache / Redis / Ruby / PHP / ...
        ↑ applications / languages / DBs / servers you pick and install

This is where you get to choose between major versions even within the same RHEL 9 — PostgreSQL 13 / 15 / 16, Python 3.9 / 3.11 / 3.12. The mechanism behind it is modules.

This split is the AppStream concept introduced in RHEL 8. On RHEL 7 and earlier, only one major version of a package was possible — to use a newer PostgreSQL you had to attach an external repo (the PGDG repo from postgresql.org). After AppStream you can pick from inside the official RHEL repos.

dnf module — multiple versions of the same package #

Take PostgreSQL as an example.

module list
$ dnf module list postgresql
Red Hat Enterprise Linux 9 for aarch64 - AppStream
Name         Stream    Profiles                  Summary
postgresql   13        client, server [d]        PostgreSQL server and client module
postgresql   15        client, server [d]        PostgreSQL server and client module
postgresql   16        client, server [d]        PostgreSQL server and client module

Three major versions are listed. The Stream next to each version is the version’s identifier, and Profile is a preset bundle that defines what gets installed for a given use case.

enable a stream + install
$ sudo dnf module enable postgresql:16
$ sudo dnf module install postgresql:16/server

postgresql:16 enables the stream; postgresql:16/server installs the server profile. Only one stream can be active on the system at a time — you can’t run PG 13 and PG 16 on the same machine (use containers for that).

info / disable / reset a module
$ dnf module info postgresql:16
$ sudo dnf module disable postgresql        # disable all streams (before removal)
$ sudo dnf module reset postgresql          # reset enable / disable state

Which stream should you pick? #

Sticking with the default-marked stream ([d]) is the safest call. Whatever default ships with RHEL 9 is guaranteed patches through the end of the RHEL lifecycle (or per-module, often 5 years).

Pin a specific stream only when you need a specific version. Once a stream is set, dnf update only applies minor updates within that stream — PG 16 won’t suddenly jump to PG 17.

2026 note — Based on usage patterns, modules are expected to play a smaller role from RHEL 10 onward. Newer RHEL is starting to ship more modules with only a “default stream.” Still, on RHEL 9 modules sit at the center.

External repos — EPEL, COPR #

When something you need isn’t in BaseOS + AppStream, two external repos come up most often.

EPEL — Extra Packages for Enterprise Linux #

A collection of additional packages the Fedora community builds for RHEL compatibility. Tools that aren’t in RHEL itself (e.g. htop, ncdu, bat, parts of tmux) live here.

enable EPEL
$ sudo dnf install -y epel-release
$ sudo dnf repolist
repo id                          repo name
epel                             Extra Packages for Enterprise Linux 9
epel-cisco-openh264              ...
rhel-9-for-aarch64-appstream-rpms ...
rhel-9-for-aarch64-baseos-rpms    ...

Now you can pull EPEL packages.

install from EPEL
$ sudo dnf install htop ncdu

AlmaLinux / Rocky users — the dnf flow for epel-release differs slightly. You usually need to enable the CodeReady Linux Builder (CRB) repo first via sudo dnf config-manager --set-enabled crb so EPEL’s dependencies resolve. RHEL has the equivalent: subscription-manager repos --enable codeready-builder-for-rhel-9-aarch64-rpms.

COPR — for personal projects #

Fedora’s COPR is a hub for user-built temporary repos. Tools that haven’t reached EPEL yet, or single-maintainer packages, live here.

using COPR
$ sudo dnf copr enable some-user/some-project
$ sudo dnf install some-package

Not recommended for production. If a maintainer disappears, security patches stop. Learning and experimentation only.

Things to mind when adding an external repo #

ItemWhy
Trusted repos onlyrpm installs as root. A malicious package is game over.
GPG signature verificationgpgcheck=1 should be on (default).
PriorityAn external repo overriding RHEL core packages is a problem.
Enable only when neededLeave enabled=0 and use --enablerepo ad hoc.

In production, keep just EPEL on, and resolve everything else from official RHEL repos when you can.

Enabling/disabling repos with subscription-manager #

On RHEL machines (not AlmaLinux/Rocky) there are extra repos beyond BaseOS / AppStream, depending on your subscription.

repos available to enable/disable
$ sudo subscription-manager repos --list | head -30
+----------------------------------------------------------+
    Available Repositories in /etc/yum.repos.d/redhat.repo
+----------------------------------------------------------+
Repo ID:   rhel-9-for-aarch64-supplementary-rpms
Repo Name: Red Hat Enterprise Linux 9 for ARM 64 - Supplementary (RPMs)
Repo URL:  ...
Enabled:   0

Repo ID:   codeready-builder-for-rhel-9-aarch64-rpms
Repo Name: Red Hat CodeReady Linux Builder for RHEL 9 ARM 64 (RPMs)
...
Enabled:   0

Enable a specific repo:

enable CRB
$ sudo subscription-manager repos \
    --enable codeready-builder-for-rhel-9-aarch64-rpms

$ sudo dnf repolist | grep code
codeready-builder-for-rhel-9-aarch64-rpms ...

EPEL has packages that depend on CRB, so you’ll often enable both together.

Common commands at a glance #

A reference of the commands used across this post. No need to memorize — come back when you get stuck.

CommandWhat it does
dnf install <pkg>Install a package + its dependencies
dnf remove <pkg>Remove a package + unused dependencies
dnf update [<pkg>]Update everything or a specific package
dnf check-updateShow if updates are available
dnf search <query>Search names and summaries
dnf info <pkg>Show detailed package info
dnf list installed/available/updatesLists
dnf repoquery -l <pkg>Files installed by a package
dnf repoquery --whatrequires <pkg>What depends on this package
dnf history / history info N / history undo NTransaction history and rollback
dnf module list <pkg>Module streams
dnf module enable <pkg>:<stream>Enable a module stream
dnf module install <pkg>:<stream>/<profile>Install from a module
dnf repolist [--all]List repos
dnf config-manager --set-enabled <repo>Enable a repo
subscription-manager repos --enable <repo>Enable a RHEL repo

Common pitfalls #

“The package isn’t found” #

If search returns nothing, it’s usually one of two things.

  1. A repo is disabled — dnf repolist to confirm. With AppStream off, application packages disappear.
  2. You need an external repo like EPEL — htop, ncdu, and friends aren’t in the official RHEL repos.

“The cache feels off” #

When stale metadata confuses dnf:

clear the cache
$ sudo dnf clean all
$ sudo dnf makecache

“Modules conflict” #

If dnf install postgresql-server errors out with something about a module being disabled, you haven’t enabled it. Check streams with dnf module list postgresql, then dnf module enable postgresql:16.

If two modules collide over the same package, run dnf module reset <pkg> to reset state and re-enable cleanly.

“EPEL is installed but dependencies don’t resolve” #

About 99% of the time, CRB (CodeReady Linux Builder) isn’t enabled. Use the subscription-manager repos --enable codeready-builder-... line above, or dnf config-manager --set-enabled crb on AlmaLinux/Rocky.

Wrap-up #

The picture from this post:

  • rpm (single file) / yum (older name) / dnf (current standard) — RHEL 9 unifies on dnf.
  • Daily life is mostly install / remove / update / search / info / list.
  • dnf history and history undo are the distinctly RHEL touch — transaction-level rollback.
  • BaseOS is the OS core (10-year stability), AppStream is applications (multiple versions).
  • modules lets you choose major versions of the same package — PG 13/15/16, etc.
  • EPEL is Fedora’s RHEL-compatible add-on repo — where htop, ncdu, etc. come from. Often paired with CRB.
  • For external repos: trusted only / GPG check on / in production keep it to EPEL.

Next — systemd #

Now you can install packages. The next thing to handle is how to start them, stop them, and have them come up automatically on boot — that’s systemd’s job.

#4 Intro to systemd — services, targets, journalctl covers how systemd holds down PID 1 across the whole system, the systemctl start / stop / enable / status family of commands, what targets (multi-user / graphical / rescue) mean, writing a first .service unit by hand and bringing it up, and using journalctl to read every service’s logs from one place.

X