All About Package Managers in Linux: DNF, RPM, APT, Pacman, and the Long Road to Here
1. The Agony of Dependency Hell
The first time I hit real dependency hell, I wasn't even trying to do anything exotic. I just wanted a graphics viewer that could open a specific image format my digital camera used, back when I was running an early distro off a stack of install CDs. The tarball unpacked fine. ./configure complained about a missing header file. I found the library that provided it, downloaded its tarball, and its ./configure complained about a different missing header file. Three libraries deep, I was compiling something with a name I don't remember, for a dependency of a dependency of an image viewer, and I had genuinely forgotten what I was originally trying to install. That spiral has a name — Dependency Hell — and if you started using Linux before the mid-90s binary package formats existed, you know exactly the feeling I'm describing.
It's worth walking through how bad it actually was, because a lot of people who started with apt install or dnf install have no idea what problem those tools were solving.
2. Compiling Everything by Hand, and Why Uninstalling Was Basically Impossible
Before online repositories and standardized binary packages existed, software came as compressed C source tarballs. Installing something — a browser, an IRC client, whatever — meant a manual build pipeline every single time:
1. Unpack the tarball: tar -xzvf application-1.0.tar.gz
2. Run the configure script, which probed your system for compilers, headers, and architecture flags: ./configure
3. Compile everything with make, invoking gcc across however many source files the project had
4. Copy the resulting binaries into system paths with sudo make install
If a single header or shared library was missing — libpng.h, libz.so.1, anything — the build died with a compiler error that told you almost nothing useful. You'd go find the source for the missing library, discover it needed something else, and keep descending. And even once you got something installed, there was no clean way to remove it. make install scattered binaries into /usr/local/bin, libraries into /usr/local/lib, and config files across /etc with zero record of what it had just done. Uninstalling meant manually hunting your filesystem for stray files and hoping you didn't miss anything, or break something else that had started depending on the same library.
"Dependency Hell wasn't just an inconvenience; it was the single biggest barrier stopping Linux from becoming a viable operating system for everyday software engineers."
3. Two Teams, Two Answers: RPM and .deb
By the mid-90s, two separate engineering efforts arrived at roughly the same idea from different directions. Ian Murdock's Debian project built the .deb format together with the low-level dpkg tool starting around 1994–95. Over at Red Hat, Marc Ewing and Erik Troan built RPM, drawing on earlier internal tools like pm, with the RPM project itself dated to 1997.
Both formats did the same fundamental thing: stop shipping raw source code, and ship pre-compiled binaries bundled with real metadata instead of hoping the user's system happened to have the right compiler and headers.
A .deb file, if you crack it open, is just a UNIX ar archive holding three pieces: debian-binary (the format version), control.tar.xz (metadata, dependency lists, and maintainer scripts like preinst and postinst), and data.tar.xz (the actual payload, mapped to real filesystem paths). An .rpm file is structurally different but conceptually similar — a lead header, a signature block for PGP verification, a header of key-value metadata tags (things like Requires: libc.so.6(GLIBC_2.34)), and a compressed CPIO archive holding the payload.
You can actually go poke at these formats yourself, which I still find oddly satisfying:
# Debian / Ubuntu — inspecting a .deb with ar and dpkg-deb
ar t package.deb # debian-binary, control.tar.xz, data.tar.xz
dpkg-deb -I package.deb # control metadata, maintainer info, dependencies
dpkg-deb -c package.deb # every file path packed inside the payload
# Red Hat / Fedora — inspecting an .rpm with rpm and cpio
rpm -qip package.rpm # header tags and version, without installing anything
rpm -qlp package.rpm # target file paths inside the payload
rpm2cpio package.rpm | cpio -t # extract the raw binary payload tree directly4. Automating the Hunt: APT, and Eventually a Real SAT Solver
Here's the catch, though: neither rpm -i nor dpkg -i could reach out to the network and fetch a missing dependency for you. If foo.rpm needed bar.rpm, running rpm -i foo.rpm just printed an error and stopped. You still had to go find bar.rpm yourself, usually by trawling FTP mirrors, and install it first, by hand, every time. The binary format solved installing and cleanly removing software. It did nothing for the actual hunting-down-dependencies problem — that was still entirely your job.
Debian closed that gap in 1998 with apt-get. APT layered remote mirror indexing and automated dependency resolution on top of dpkg. Run apt-get install nginx, and APT pulls repository manifests, works out the full dependency graph, fetches whatever's missing over HTTP, and hands it all to dpkg in the right install order. That's the moment package management on Debian-family systems stopped being manual archaeology.
Red Hat's ecosystem got there by a slightly rougher road. Early on, people leaned on third-party scripts like up2date. Then Seth Vidal, working as a systems administrator for Duke University's Physics department, built YUM (Yellowdog Updater, Modified) as a rewrite of an earlier, slower Yellowdog-Linux-specific tool — this happened somewhere around 2002, depending on which release you count as "the start." YUM did for RPM roughly what APT had done for .deb.
The problem was that YUM aged badly at scale. Its Python-based dependency resolution and XML metadata parsing turned into a real bottleneck once enterprise systems had tens of thousands of packages to reason about — installs that should take seconds started taking noticeably longer, and the memory overhead climbed with it. Fedora officially retired YUM in favor of DNF (Dandified YUM) in 2015. The core change was swapping YUM's older resolution logic for libsolv, a C library built jointly by openSUSE and Red Hat that turns "figure out which package versions satisfy all these constraints" into a Boolean Satisfiability problem and hands it to a fast SAT solver. What used to be a slow, occasionally wrong dependency calculation became something that resolves in milliseconds and is provably correct given the constraints. More recently, dnf5 — a full rewrite in C++ — folded DNF and PackageKit into one lightweight daemon, dropped the Python runtime dependency entirely, and added instant tab completion along with full transaction rollback history.
That rollback feature is one of my favorite quality-of-life upgrades in this whole timeline, and it's worth actually seeing in action:
# Fedora / RHEL (DNF5)
sudo dnf install nginx -y # SAT-solver dependency resolution, not guesswork
sudo dnf history # every transaction the system remembers
sudo dnf history undo 42 # cleanly roll back transaction #42
dnf provides */libssl.so # find which package owns a specific library file
# Debian / Ubuntu (APT)
sudo apt update && sudo apt install nginx -y
apt-cache policy nginx # compare version priority across mirrors
dpkg -S /usr/bin/nginx # find which installed package owns a binary5. Arch Takes a Different Philosophy Entirely
I've used dnf history undo to walk back a bad update at an inconvenient hour more than once, and every time it feels a little bit like a cheat code compared to the "reinstall everything and hope" era.
While Debian and Red Hat were building increasingly sophisticated dependency resolvers, Judd Vinet took Arch Linux in a deliberately opposite direction starting in 2002. Where Debian and Red Hat often split software into separate -dev and -doc sub-packages, Arch leans hard into Keep It Simple — fewer abstractions, less magic, more trust placed in the user to understand what they're installing.
Pacman ships binaries as .pkg.tar.zst archives, compressed with Meta's Zstandard algorithm specifically because it decompresses fast. But the feature that actually defines the Arch experience for most people is the Arch User Repository — a massive, community-maintained index of PKGBUILD scripts that describe how to build software directly from upstream source, often the same day a project ships a new release. Pair that with an AUR helper like yay or paru, and you can pull in software that hasn't made it into any official repository yet with a single command.
sudo pacman -Syu # sync repositories, upgrade the whole rolling system
pacman -Qo /usr/bin/nginx # find which package owns this executable
yay -S visual-studio-code-bin # build and install straight from the AUR6. When Shared Libraries Themselves Become the Problem
Because Arch is a rolling release, pacman -Syu is effectively your only upgrade command, forever — there's no "Arch 12 to Arch 13" migration to plan around, just a continuous stream of updates. That's either liberating or slightly terrifying depending on your relationship with stability, and honestly it can be both in the same week.
Traditional package managers keep libraries in shared system locations like /usr/lib. That works fine until Application A needs OpenSSL 1.1 and Application B insists on OpenSSL 3.0, and a routine system upgrade suddenly breaks one of them — the Linux world's own flavor of DLL Hell, arriving decades later than you'd expect.
The response has been a shift toward isolating applications from the host system entirely, or making builds reproducible enough that this conflict can't happen in the first place:
• Flatpak, built by Alexander Larsson, uses OSTree — essentially Git applied to operating-system binary trees — combined with bubblewrap sandboxes to keep desktop GUI apps isolated from whatever else is on your system.
• Snap, Canonical's answer to the same problem, packages applications as read-only squashfs images mounted as loop devices, with AppArmor handling the security boundary.
• Nix, which came out of Eelco Dolstra's doctoral research in the early 2000s, takes the most radical approach: every package lives in its own content-addressed hash directory under /nix/store — something like /nix/store/c68a48b3...-nginx-1.24.0 — so two different versions of the same library can coexist without ever colliding. The upside is genuinely reproducible builds and instant, atomic rollbacks. The downside is a learning curve that's steeper than anything else on this list.
7. Putting It Side by Side
If you're jumping between distros regularly, having the everyday commands lined up next to each other saves a lot of "wait, is it -S or -i" moments:
=======================================================================================================================
TASK DEBIAN/UBUNTU (APT) FEDORA/RHEL (DNF5) ARCH (PACMAN) NIX
=======================================================================================================================
Install a package sudo apt install <pkg> sudo dnf install <pkg> sudo pacman -S <pkg> nix-env -iA nixpkgs.<pkg>
Remove a package sudo apt remove <pkg> sudo dnf remove <pkg> sudo pacman -R <pkg> nix-env -e <pkg>
Search repositories apt search <query> dnf search <query> pacman -Ss <query> nix-env -qaP <query>
Upgrade the system sudo apt update && upgrade sudo dnf upgrade sudo pacman -Syu nixos-rebuild switch
Find file owner dpkg -S /path/to/file dnf provides */file pacman -Qo /path/to/file nix-store --query
View transaction history cat /var/log/dpkg.log sudo dnf history cat /var/log/pacman.log nix-env --list-generations
Roll back an update manual reinstall sudo dnf history undo <N> manual downgrade nixos-rebuild switch --rollback
Package format .deb (ar + tar.xz) .rpm (lead + cpio) .pkg.tar.zst /nix/store/<hash>-<name>
Dependency resolution APT DAG resolver libsolv SAT solver Pacman solver Pure functional graph8. A Rough Timeline and Final Thoughts
A rough timeline, if you want the dates in one place:
• Early 1990s — the tarball era: ./configure && make && make install, and dependency hell as an unavoidable rite of passage.
• Mid-1990s — Debian's dpkg and the .deb format arrive, giving Linux its first real structured binary packages.
• 1997 — Red Hat's RPM format ships, built by Marc Ewing and Erik Troan.
• 1998 — Debian's apt-get adds automated, network-aware dependency resolution on top of dpkg.
• Early 2000s — Judd Vinet launches Arch Linux and pacman; around the same period, Seth Vidal builds YUM at Duke University for RPM-based systems.
• Early 2000s — Eelco Dolstra publishes the PhD thesis that introduces Nix and declarative package management.
• 2015 — Fedora replaces YUM with DNF, powered by the libsolv SAT solver.
• 2016 — Flatpak 1.0 and Canonical's Snap both ship, bringing containerized desktop packaging to a mainstream audience.
• 2024 onward — dnf5, a C++ rewrite, becomes the standard high-speed RPM tooling across Fedora and its enterprise relatives.
None of these tools replaced the ones before them so much as they responded to what the previous generation got wrong. Tarballs solved "how do I distribute software" but ignored dependencies entirely. RPM and .deb solved installation and clean removal but couldn't fetch anything over a network. APT and YUM solved dependency fetching but left performance and correctness problems on the table as systems scaled up. DNF's SAT solver solved correctness and speed. And Flatpak, Snap, and Nix are all, in their own very different ways, responses to the fact that even a perfect dependency resolver can't stop two applications from wanting incompatible versions of the same shared library.
Knowing this history isn't just trivia. The moment you're debugging a broken CI pipeline, or trying to figure out why a Docker base image behaves differently than your laptop, understanding why each of these tools exists — not just its command syntax — is usually what gets you to the actual answer faster.