dnf and rpm
Topic 37

dnf and rpm

Package ManagementRed Hat Family

rpm is the low-level package format and tool of the Red Hat family — RHEL, Fedora, CentOS Stream, Rocky, AlmaLinux — exactly mirroring the role dpkg plays on Debian. An .rpm file is a cpio archive of files plus a header carrying metadata: name, version, release, architecture, dependencies, scriptlets, and cryptographic signatures. rpm installs and removes that single file and records it in a local database, but it does not resolve dependencies or fetch anything from the network. Hand it a package whose requirements are missing and it refuses with an unmet-dependency error and stops.

dnf is the dependency resolver and repository client layered on top — the Red Hat-family counterpart to apt. It reads repository metadata, runs a SAT solver to compute a consistent transaction, downloads every needed .rpm, and hands the whole set to the RPM library to apply atomically. The operational rule is the same one that governs apt versus dpkg: drive everyday work through dnf so dependencies resolve, and drop to rpm only to query the database or inspect a file you already have on disk.

The Two Layers

rpm operates on packages and the local RPM database under /var/lib/rpm (now a single SQLite file on current releases, previously Berkeley DB). It answers questions about what is installed and verifies installed files against their recorded checksums, but it has no concept of a repository. dnf sits above it, owns the repository configuration in /etc/yum.repos.d/, caches metadata under /var/cache/dnf, and turns a request like "install nginx" into the full set of packages that satisfies it.

The split matters when something goes wrong. If dnf cannot find a package, the problem is repository configuration or metadata. If an install fails partway with a scriptlet error or a file conflict, that is the RPM layer reporting, and rpm queries are how you diagnose it. Reaching for rpm -i to install a real package is almost always a mistake, because it bypasses the solver and leaves you to satisfy dependencies by hand.

Installing, Removing, and Updating

Everyday dnf verbs read close to their apt equivalents, with one important difference: dnf refreshes repository metadata automatically when the cache is stale, so there is no mandatory apt update step before installing. dnf upgrade covers both "upgrade these packages" and "upgrade everything", and unlike older yum it removes packages pulled in only as dependencies once nothing needs them.

# refresh and install — dnf resolves dependencies and downloads .rpms
sudo dnf install nginx

# upgrade every package to the latest in the enabled repos
sudo dnf upgrade

# remove a package and any now-unneeded dependencies
sudo dnf remove nginx

# search the repos and show package details
dnf search "web server"
dnf info nginx

When you already hold a downloaded .rpm, install it with dnf install ./package.rpm rather than rpm -i package.rpm. The dnf path still runs the solver against the enabled repositories, so any dependencies the local file needs are fetched automatically — the same reasoning that favors apt install ./file.deb over dpkg -i on Debian.

Querying the RPM Database

rpm in query mode (-q) is the fast, offline way to interrogate installed state — it never touches the network and never changes anything. This is where rpm earns its place even on a system you administer entirely through dnf: finding which package owns a file, listing what a package installed, or reading the changelog of an installed version.

# which package owns this file on disk?
rpm -qf /usr/sbin/nginx

# list every file a package installed
rpm -ql nginx

# inspect a downloaded .rpm without installing it
rpm -qpi ./nginx-1.24.0-1.el9.x86_64.rpm

# verify installed files against recorded checksums, mode, owner
rpm -V nginx

rpm -V compares the live filesystem against the metadata recorded at install time and prints a flag string for anything that drifted — 5 for a changed checksum, S for size, T for mtime, M for mode. It is a cheap integrity tripwire: an unexpected checksum change on a binary under /usr/sbin is worth investigating, while a changed config under /etc marked c is normal and expected.

GPG Signatures and Trust

Every package in a Red Hat-family repository is GPG-signed, and the signature lives in the RPM header rather than alongside the file as apt does with repository-level signing. dnf checks each package against the keys you have imported before applying the transaction; an unsigned or mismatched package is refused unless you explicitly disable the check. Importing a vendor's key with rpm --import is the act that says "I trust packages signed by this key", and it is the one step you must not skip when adding a third-party repository.

The trade-off mirrors the rest of the package system: signatures only protect you against tampering with packages from sources you have already chosen to trust. Adding a careless third-party repository and importing its key widens your trust boundary just as much as it does on Debian — the cryptography is sound, but it cannot vouch for whoever holds the key.

Modules, Streams, and Distro Differences

RHEL 8 and 9 layer modular content on top of plain packages: a module like nodejs ships several streams (for example 18 and 20), and enabling one stream pins which version of that application you receive across upgrades. This solves the real problem of wanting a newer runtime than the base release froze, but it adds state — dnf module enable nodejs:20 — that a plain dnf install does not surface, and forgetting it has happened is a common source of "why am I stuck on this version" confusion. Fedora has largely retired modularity, so the same command set behaves differently across the family.

The deeper divergence from Debian is philosophical. dnf ships no equivalent of Debian's postinst service-enable convention: installing a package on RHEL does not start or enable its service, so you manage that explicitly through systemctl. And the package names differ — -devel instead of -dev, httpd instead of apache2, chrony defaults instead of systemd-timesyncd — so muscle memory from Ubuntu does not transfer cleanly even where the underlying software is identical.

Common Mistakes
  • Installing a real package with rpm -i package.rpm instead of dnf install ./package.rpmrpm does not resolve dependencies, so you hit an unmet-requirement wall and end up satisfying them by hand or forcing a broken install.
  • Using rpm -i --nodeps --force to push past a conflict — it installs the files but leaves the RPM database describing a system that cannot actually run, and every later dnf transaction inherits the broken state.
  • Adding a third-party repo and skipping rpm --import of its GPG key, then disabling signature checks with --nogpgcheck to make the install go through — you have turned off the one defense against a tampered package.
  • Expecting a freshly installed service to be running — RHEL does not auto-enable services on install the way Debian conventionally does, so the daemon sits stopped until you run systemctl enable --now.
  • Editing a config under /etc and then being surprised by an .rpmnew file after an upgrade — RPM preserves your changed config and writes the package's new version alongside it, and silently ignoring .rpmnew/.rpmsave files lets config drift accumulate.
  • Forgetting that a module stream is enabled — a pinned stream caps the version you can install, so dnf upgrade appears to refuse a newer release for no visible reason until you check dnf module list --enabled.
  • Running dnf clean all reflexively to "fix" a problem — it only discards cached metadata, forces a slow re-download, and almost never addresses the actual repository or dependency error.
Best Practices
  • Drive every install, removal, and upgrade through dnf; reserve rpm for query mode (-q) and verification (-V), never for installing real packages.
  • Install local .rpm files with dnf install ./file.rpm so the solver fetches their dependencies from the enabled repositories instead of failing on missing requirements.
  • Import every repository's GPG key with rpm --import and leave gpgcheck=1 on; treat --nogpgcheck as a red flag, not a workaround.
  • Use rpm -qf to find the owning package before touching any file under /usr, and rpm -ql to see what a package placed where, before assuming you can safely delete it.
  • Review and merge .rpmnew files after every upgrade with a tool like rpmconf -a, so package-shipped config changes do not silently diverge from yours.
  • Roll back a bad transaction with dnf history undo <id> rather than reinstalling by hand — dnf records every transaction and can reverse it as a unit.
  • Check dnf module list --enabled before troubleshooting a version that refuses to advance, since a pinned stream, not a missing package, is the usual cause.
Comparable toolsDebian/Ubuntuapt over dpkg, the exact same resolver-over-format split with .deb packagesArchpacman, a single tool covering both the format and dependency resolutionopenSUSEzypper over the same rpm format, a sibling resolver to dnf

Knowledge Check

You have a downloaded .rpm whose dependencies are not yet installed. Why prefer dnf install ./file.rpm over rpm -i file.rpm?

  • dnf runs the solver against the enabled repositories to fetch the missing dependencies, while rpm installs the one file and fails on unmet requirements
  • rpm -i cannot install a local file at all, only packages it pulls directly from a configured repository over the network after resolving their names against the index
  • dnf skips the GPG signature check on a local file, which is what lets the install go through even though the package was never indexed by an enabled repository
  • Both behave identically here, but dnf records the transaction in the RPM database for later rollback while rpm writes nothing and leaves no history to undo

What does rpm -V nginx actually do?

  • Compares the installed files against the checksums, size, mode, and owner recorded at install time and flags anything that drifted — offline, changing nothing
  • Checks the enabled repositories for a newer build of nginx and reports back whether an upgrade is available, comparing the installed version against the latest one each mirror advertises
  • Verifies the GPG signature on the installed nginx package against the imported repository key before allowing the daemon to start under systemd
  • Validates the nginx configuration files under /etc/nginx for syntax errors and reports the offending line before you trigger a service restart

After dnf install httpd on RHEL, the service is not running. Why is this expected rather than a bug?

  • RHEL does not auto-enable or start services on install — you start it explicitly with systemctl enable --now httpd
  • The package installed but a post-install scriptlet error silently left the service binary missing from the unit's ExecStart path on disk
  • dnf always installs a service in a masked state that needs a full reboot to clear before systemctl will let you start the daemon at all
  • The package name is wrong; on RHEL the web server package is apache2, not httpd

A dnf upgrade refuses to move nodejs past version 18 even though 20 exists in the repos. What is the most likely cause?

  • An enabled module stream pins nodejs to 18; you have to switch the stream before the newer version is offered
  • The 20 package is unsigned, so dnf silently skips it midway through the upgrade transaction and quietly settles on the newest signed build it can still trust
  • The RPM database is corrupt and has to be rebuilt with rpm --rebuilddb before the version 20 build becomes visible to the solver again
  • Cached metadata is stale, and only a dnf clean all followed by a refresh exposes the newer build

You got correct