dnf and rpm
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.
- Installing a real package with
rpm -i package.rpminstead ofdnf install ./package.rpm—rpmdoes 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 --forceto push past a conflict — it installs the files but leaves the RPM database describing a system that cannot actually run, and every laterdnftransaction inherits the broken state. - Adding a third-party repo and skipping
rpm --importof its GPG key, then disabling signature checks with--nogpgcheckto 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
/etcand then being surprised by an.rpmnewfile after an upgrade — RPM preserves your changed config and writes the package's new version alongside it, and silently ignoring.rpmnew/.rpmsavefiles lets config drift accumulate. - Forgetting that a module stream is enabled — a pinned stream caps the version you can install, so
dnf upgradeappears to refuse a newer release for no visible reason until you checkdnf module list --enabled. - Running
dnf clean allreflexively to "fix" a problem — it only discards cached metadata, forces a slow re-download, and almost never addresses the actual repository or dependency error.
- Drive every install, removal, and upgrade through
dnf; reserverpmfor query mode (-q) and verification (-V), never for installing real packages. - Install local
.rpmfiles withdnf install ./file.rpmso the solver fetches their dependencies from the enabled repositories instead of failing on missing requirements. - Import every repository's GPG key with
rpm --importand leavegpgcheck=1on; treat--nogpgcheckas a red flag, not a workaround. - Use
rpm -qfto find the owning package before touching any file under/usr, andrpm -qlto see what a package placed where, before assuming you can safely delete it. - Review and merge
.rpmnewfiles after every upgrade with a tool likerpmconf -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 —dnfrecords every transaction and can reverse it as a unit. - Check
dnf module list --enabledbefore troubleshooting a version that refuses to advance, since a pinned stream, not a missing package, is the usual cause.
apt over dpkg, the exact same resolver-over-format split with .deb packagesArch — pacman, a single tool covering both the format and dependency resolutionopenSUSE — zypper over the same rpm format, a sibling resolver to dnfKnowledge Check
You have a downloaded .rpm whose dependencies are not yet installed. Why prefer dnf install ./file.rpm over rpm -i file.rpm?
dnfruns the solver against the enabled repositories to fetch the missing dependencies, whilerpminstalls the one file and fails on unmet requirementsrpm -icannot install a local file at all, only packages it pulls directly from a configured repository over the network after resolving their names against the indexdnfskips 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
dnfrecords the transaction in the RPM database for later rollback whilerpmwrites 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/nginxfor 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
ExecStartpath on disk dnfalways installs a service in a masked state that needs a full reboot to clear beforesystemctlwill let you start the daemon at all- The package name is wrong; on RHEL the web server package is
apache2, nothttpd
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
nodejsto 18; you have to switch the stream before the newer version is offered - The 20 package is unsigned, so
dnfsilently 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 --rebuilddbbefore the version 20 build becomes visible to the solver again - Cached metadata is stale, and only a
dnf clean allfollowed by a refresh exposes the newer build
You got correct