Guides/LinuxLinux/Linux Package Management: apt, dnf, and yum

Linux Package Management: apt, dnf, and yum

Install, update, pin, and remove software the right way across Debian and RHEL systems. apt vs dnf/yum, repositories, version pinning for reproducible builds, querying what owns a file, and when to build from source.


Installing software on Linux is easy until it is not: a deploy pulls a newer package than CI tested, two servers end up on different versions, or you need a tool that is not in any repo. Understanding the package manager - not just install, but pinning, repositories, and querying - is what keeps your servers reproducible instead of slowly drifting apart.

Linux has two big package families. Know which one you are on and the rest maps over.

The two families

  • Debian / Ubuntu use .deb packages, the apt front-end, and dpkg underneath.
  • RHEL / Fedora / Amazon Linux / Rocky use .rpm packages, the dnf front-end (yum is the older name, now an alias), and rpm underneath.

The commands are different words for the same ideas, so here is the map you will use constantly:

Task Debian/Ubuntu (apt) RHEL/Fedora (dnf)
Refresh package lists apt update (dnf refreshes automatically)
Upgrade everything apt upgrade dnf upgrade
Install a package apt install nginx dnf install nginx
Remove a package apt remove nginx dnf remove nginx
Search apt search nginx dnf search nginx
Show package info apt show nginx dnf info nginx
List installed apt list --installed dnf list installed
What provides a file dpkg -S /path dnf provides /path
Which package owns a cmd dpkg -S $(which nginx) rpm -qf $(which nginx)

The one gotcha that bites everyone on Debian: apt update and apt upgrade are different things. update only refreshes the local list of what is available; upgrade actually installs newer versions. On a fresh box you almost always run sudo apt update first, or install will fail to find packages or grab stale ones. dnf refreshes its metadata on its own, so there is no separate update step.

The everyday flow

# Debian / Ubuntu
sudo apt update                 # refresh the catalog first
sudo apt install -y nginx       # -y to skip the prompt (for scripts)
sudo apt remove nginx           # remove the package, keep config
sudo apt purge nginx            # remove the package AND its config
sudo apt autoremove             # clean up orphaned dependencies

# RHEL / Fedora
sudo dnf install -y nginx
sudo dnf remove nginx
sudo dnf autoremove

remove vs purge on Debian is worth knowing: remove leaves /etc config behind (handy if you are reinstalling), purge wipes it too. When a reinstall keeps picking up old config you thought you deleted, purge is why.

Repositories: where packages come from

A package manager only installs what its configured repositories know about. Repos are defined in:

  • Debian/Ubuntu: /etc/apt/sources.list and /etc/apt/sources.list.d/*.list
  • RHEL/Fedora: /etc/yum.repos.d/*.repo

To install software that is not in the base distro - Docker, the latest Node, a vendor tool - you add that vendor's repository, then install normally. The pattern is always: add the repo's signing key (so packages are verified), add the repo definition, refresh, install.

# Adding a third-party repo is vendor-specific, but the shape is always:
# 1. add the GPG key   2. add the repo   3. apt update   4. apt install

Two practical notes. First, never pipe a random curl ... | sudo bash installer without reading it - it runs as root and can add repos and packages you did not intend. Second, the GPG key matters: it is how the package manager verifies a package was not tampered with. A repo with no key, or [trusted=yes], disables that check - avoid it.

Version pinning: the key to reproducible servers

This is where package management becomes a DevOps skill. By default install grabs the newest version, which means the package you tested in CI may not be the package that lands in production a week later. For anything that matters, pin the version.

# Install a SPECIFIC version
sudo apt install nginx=1.24.0-1ubuntu1     # Debian: name=version
sudo dnf install nginx-1.24.0              # RHEL: name-version

# See what versions are available to pin to
apt list -a nginx                          # all versions in the repos
dnf --showduplicates list nginx

You can also stop a package from being upgraded at all - useful when a newer kernel or library breaks something and you need to freeze it:

sudo apt-mark hold nginx       # Debian: never upgrade nginx
sudo apt-mark unhold nginx     # release the hold
# RHEL: add "exclude=nginx*" to /etc/dnf/dnf.conf, or use versionlock

The same principle is why a Dockerfile should say apt-get install -y nginx=1.24.0-1ubuntu1 rather than bare nginx: pinning is what makes a build today produce the same image as a build next month. Unpinned installs are the quiet source of "it worked on my machine."

Querying: what is installed and what owns this file

When you are debugging, you often need to go the other direction - from a file or command back to the package:

# Debian / Ubuntu
dpkg -l | grep nginx              # is it installed, and what version?
dpkg -L nginx                     # every file this package installed
dpkg -S /usr/sbin/nginx           # which package owns this file?

# RHEL / Fedora
rpm -qa | grep nginx              # installed packages matching nginx
rpm -ql nginx                     # files in the package
rpm -qf /usr/sbin/nginx           # which package owns this file

"Which package put this file here, and what version is it?" comes up constantly when a config is in an unexpected place or a binary behaves oddly. dpkg -S and rpm -qf answer it in one line.

The lower level: dpkg and rpm

apt and dnf resolve dependencies and talk to repos; dpkg and rpm operate on a single package file directly, with no dependency resolution. You reach for them to install a downloaded .deb/.rpm:

sudo dpkg -i ./some-tool.deb      # install a local .deb (then `apt -f install` to fix deps)
sudo rpm -i ./some-tool.rpm       # install a local .rpm

The catch: because they do not resolve dependencies, a dpkg -i can leave the package half-installed if a dependency is missing. The fix on Debian is sudo apt -f install, which pulls the missing dependencies. For this reason, prefer apt install ./file.deb / dnf install ./file.rpm (the front-ends accept a local file and DO resolve dependencies).

Snap, Flatpak, and language package managers

Two more layers you will meet:

  • Snap and Flatpak bundle an app with its dependencies in a sandbox. They sidestep system dependency conflicts but are heavier and slower to start. Common for desktop apps and some CLIs; on servers, prefer the native package when one exists.
  • Language managers (pip, npm, cargo, go install) install per-language tools and libraries. Keep these separate in your head from the OS package manager: a Python library belongs in pip/a virtualenv, not apt, and mixing them (installing Python libs with apt and pip both) is a reliable way to create version conflicts.

Building from source: the last resort

Sometimes the version you need is not packaged anywhere. The classic build dance:

./configure        # detect the system and set build options
make               # compile
sudo make install  # install (usually into /usr/local)

Reach for this only when you must, and know the trade-off: a source build is invisible to the package manager. It will not show in dpkg -l, will not get security updates, and make install scatters files with no clean uninstall. If you go this route, prefer installing into /usr/local (kept separate from package-managed files), and better still, use checkinstall or package it yourself so the system still tracks it. For most needs, a vendor repo or a pinned package is the right answer; source builds are for the rare tool that has no other option.

The shape of it

Know which family you are on, pin versions for anything that matters (this is the real DevOps lesson), add vendor repos with their signing keys to get software beyond the base distro, and use dpkg -S / rpm -qf to trace a file back to its package when debugging. Reach for dpkg/rpm only for local files, and build from source only when nothing else will do. Get that right and your servers stay reproducible instead of drifting into snowflakes.