RHEL Basics #3: dnf and Package Management — repo, modules, AppStream
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:
- #1 What is RHEL — From Fedora to RHEL, plus AlmaLinux and Rocky Linux
- #2 Setup — Installing RHEL 9, Subscription Manager, first login
- #3 dnf and package management — repo, modules, AppStream ← this post
- #4 Intro to systemd — services, targets, journalctl
- #5 Users / groups / permissions — UID/GID, sudo, ACL
- #6 Filesystem basics — XFS, mount, /etc/fstab
- #7 Basic security — firewalld, SSH hardening
rpm / yum / dnf — one-line summary #
Three words people get confused about, sorted out in a single table:
| Tool | What it handles | Description |
|---|---|---|
| rpm | A single .rpm file | No automatic dependency resolution. Used occasionally to install a .rpm you have on hand. |
| yum | Repos + dependencies | The standard on RHEL 5–7. From RHEL 8 onward it’s effectively a nickname for dnf. |
| dnf | Repos + dependencies + modules | The 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.
$ which yum
/usr/bin/yum
$ ls -l /usr/bin/yum
lrwxrwxrwx. 1 root root 5 ... /usr/bin/yum -> dnf-3Daily dnf commands #
Starting with the ones you’ll meet first.
Search — search / info / list
#
$ 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.
$ dnf list installed | head # installed packages
$ dnf list available nginx* # installable packages
$ dnf list updates # packages with updatesInstall — 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.
$ sudo dnf install ./some-package.rpmEven a single downloaded .rpm resolves dependencies properly through dnf install. It beats rpm -i almost every time.
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 removetries 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
#
$ sudo dnf check-update # show available updates only (no changes)
$ sudo dnf update # bring everything to latest
$ sudo dnf update nginx # update one packageupdate 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.
$ 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 providesdnf 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.
$ 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$ 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
...$ sudo dnf history undo 12 # revert transaction 12
$ sudo dnf history redo 12 # apply it againThe 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:
$ 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 - BaseOSThere’s a reason for the split.
BaseOS — the unchanging OS core #
kernel / glibc / systemd / coreutils / openssl / NetworkManager
↑ the parts an OS needs to act like an OSMajor 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 #
PostgreSQL / Python / Node.js / nginx / Apache / Redis / Ruby / PHP / ...
↑ applications / languages / DBs / servers you pick and installThis 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.
$ 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 moduleThree 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.
$ sudo dnf module enable postgresql:16
$ sudo dnf module install postgresql:16/serverpostgresql: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).
$ dnf module info postgresql:16
$ sudo dnf module disable postgresql # disable all streams (before removal)
$ sudo dnf module reset postgresql # reset enable / disable stateWhich 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.
$ 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.
$ sudo dnf install htop ncduAlmaLinux / Rocky users — the
dnfflow forepel-releasediffers slightly. You usually need to enable the CodeReady Linux Builder (CRB) repo first viasudo dnf config-manager --set-enabled crbso 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.
$ sudo dnf copr enable some-user/some-project
$ sudo dnf install some-packageNot recommended for production. If a maintainer disappears, security patches stop. Learning and experimentation only.
Things to mind when adding an external repo #
| Item | Why |
|---|---|
| Trusted repos only | rpm installs as root. A malicious package is game over. |
| GPG signature verification | gpgcheck=1 should be on (default). |
| Priority | An external repo overriding RHEL core packages is a problem. |
| Enable only when needed | Leave 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.
$ 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: 0Enable a specific repo:
$ 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.
| Command | What 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-update | Show if updates are available |
dnf search <query> | Search names and summaries |
dnf info <pkg> | Show detailed package info |
dnf list installed/available/updates | Lists |
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 N | Transaction 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.
- A repo is disabled —
dnf repolistto confirm. With AppStream off, application packages disappear. - 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:
$ 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 historyandhistory undoare 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.