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 disableddnf repolist to confirm. With AppStream off, application packages disappear.
  2. You need an external repo like EPELhtop, 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