Skip to content

DNF — Street-Level Ops

Real-world patterns and debugging techniques for dnf in production.

Quick Diagnosis Commands

# What repos are active and where do they point?
dnf repolist -v

# Any updates pending? (exit code 100 = yes, 0 = no)
dnf check-update

> **Under the hood:** `dnf check-update` returns exit code 100 when updates are available and 0 when the system is current. This non-standard exit code (not 1) is intentional for scripting  `set -e` won't kill your script on "updates available." But it trips up CI pipelines that treat any non-zero exit as failure.

# What security advisories apply to this system?
dnf updateinfo list security

# Which package provides a missing file?
dnf provides '*/libssl.so.3'

# What depends on this package? (will anything break if I remove it?)
dnf repoquery --whatrequires openssl-libs --installed

Common Scenarios

Scenario 1: Security-Only Patching During a Change Window

You have a 2-hour maintenance window. You need to apply security patches only — no feature updates, no version bumps.

# Preview what security updates are available
dnf updateinfo list security --available

# See advisory details for a specific CVE
dnf updateinfo info RHSA-2025:1234

# Apply security updates only
dnf update --security -y

# Apply only critical/important severity
dnf update --security --sec-severity=Critical --sec-severity=Important -y

# Verify what changed
dnf history info last

Fleet pattern: Wrap this in Ansible with dnf module and security: yes. Log the transaction ID for rollback capability.

Scenario 2: Pinning Packages with Versionlock

Production runs nginx-1.24.0-3.el9. You need to prevent dnf update from bumping it to 1.26 during routine patching.

# Install the plugin (RHEL 8/9)
dnf install dnf-plugin-versionlock

# Lock the current version
dnf versionlock add nginx

# List all locks
dnf versionlock list

# Remove a lock when you're ready to upgrade
dnf versionlock delete nginx

# Clear all locks (careful!)
dnf versionlock clear

The lock file lives at /etc/dnf/plugins/versionlock.list. It's plain text — you can manage it with Ansible templates or commit it to your config management repo.

Gotcha: dnf versionlock add nginx locks to the exact currently-installed NEVRA (name-epoch-version-release-arch). If you later want to allow a minor update, you must delete and re-add the lock. It does not support wildcards like "lock to 1.24.*".

Fleet pattern: Keep versionlock.list in version control. Push updates via Ansible. This gives you an audit trail of what was locked, when, and by whom.

Scenario 3: Unattended Updates with DNF Automatic

dnf-automatic handles unattended updates — useful for non-critical servers or for downloading-but-not-applying updates to reduce patch window time.

# Install
dnf install dnf-automatic

# Config file
cat /etc/dnf/automatic.conf

Key config sections in /etc/dnf/automatic.conf:

[commands]
# What to do: download-only, apply, or notify-only
apply_updates = yes
# Which updates: default, security
upgrade_type = security

[emitters]
# How to notify: stdio, email, motd
emit_via = stdio, motd

[email]
email_from = root@host.example.com
email_to = ops@example.com
email_host = smtp.example.com

Timer units:

# Enable the timer (runs daily by default)
systemctl enable --now dnf-automatic-install.timer

# Different timer profiles:
# dnf-automatic-download.timer   — download only
# dnf-automatic-install.timer    — download + install
# dnf-automatic-notifyonly.timer  — just notify

# Check when it last ran
systemctl status dnf-automatic-install.timer
journalctl -u dnf-automatic-install.service --since today

Scenario 4: Offline / Disconnected Repository Mirroring

Air-gapped or bandwidth-constrained environments need local repo mirrors.

# Mirror a repo to local disk
dnf reposync --repoid=baseos --download-metadata -p /srv/repos/

# Create/update repository metadata
createrepo_c /srv/repos/baseos/

# If the upstream repo uses modules, also sync module metadata
dnf reposync --repoid=appstream --download-metadata -p /srv/repos/

# Serve via httpd or nginx
# Point clients at: baseurl=http://repo-mirror.internal/baseos/

For delta syncs (bandwidth savings):

# Only sync new packages since last run
dnf reposync --repoid=baseos --download-metadata --newest-only -p /srv/repos/

# Use --delete to remove packages no longer in upstream
dnf reposync --repoid=baseos --download-metadata --delete -p /srv/repos/

Fleet pattern: Run reposync on a cron schedule on a bastion host. Internal servers point at the bastion. This also gives you a natural "staging gate" — you control when new packages become visible to production.

Scenario 5: Debugging Dependency Failures

A package install fails with dependency conflicts. Here's the diagnosis flow:

# What provides the missing dependency?
dnf repoquery --whatprovides 'libfoo.so.2()(64bit)'

# What does the failing package require?
dnf repoquery --requires nginx --resolve

# What repo is a specific package coming from?
dnf repoquery --info nginx

# Are there multiple versions available across repos?
dnf repoquery --showduplicates nginx

# Check if module filtering is hiding what you need
dnf module list | grep -i postgres
dnf module reset postgresql   # clears stream, makes all versions visible

# Force-verbose solver output
dnf install nginx --best --allowerasing -v 2>&1 | head -100

# Check for exclude= in repo configs that might hide packages
grep -r 'exclude=' /etc/yum.repos.d/ /etc/dnf/dnf.conf

Scenario 6: Rolling Back a Bad Update

You applied updates, and now the application is broken.

# Find the transaction that broke things
dnf history list --reverse | head -20

# See exactly what changed in that transaction
dnf history info 87

# Undo just that transaction
dnf history undo 87 -y

# Or roll back to the state before the update
dnf history rollback 86 -y

# Verify the rollback worked
rpm -q nginx   # check version
systemctl restart nginx

Important: Rollback works for package changes but won't revert config file changes made by %post scripts or by the packages themselves. Always back up /etc/ before major updates.

Debug clue: dnf history info <id> shows exactly which packages were installed, upgraded, or removed in a transaction. If a rollback fails with dependency conflicts, use dnf history undo <id> --allowerasing — but review what it proposes to remove first.

Operational Patterns

Pattern: Staging Gate with Snapshot Repos

Use dnf reposync to mirror repos at a point in time. Promote snapshots through environments:

Day 0: reposync → /srv/repos/2025-03-15/
Day 1: Dev servers point at 2025-03-15/
Day 3: Staging servers updated to 2025-03-15/
Day 7: Production servers updated to 2025-03-15/

Every environment gets the exact same packages. No "works in staging, not in prod" due to repo contents changing between deployments.

Pattern: Compliance Reporting

# List all installed packages with versions (for SBOM / audit)
rpm -qa --queryformat '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' | sort

# List packages with available security updates (compliance gap)
dnf updateinfo list security --available

# List all advisories applied to this system
dnf updateinfo list --installed

# Check which packages came from which repo
dnf list installed | awk '{print $3}' | sort | uniq -c | sort -rn

Pattern: Caching Proxy with Squid or Nginx

Instead of full mirrors, use a caching proxy for bandwidth savings:

# In /etc/dnf/dnf.conf
[main]
proxy=http://cache.internal:3128

The first server that requests a package downloads it through the proxy; subsequent servers get the cached copy. Works well for container build farms where many builds pull the same packages.

Pattern: DNF Download-Only for Pre-staging

Reduce maintenance window time by pre-downloading packages:

# Download but don't install
dnf update --downloadonly -y

# Packages are cached in /var/cache/dnf/
# During the window, install from cache (no network needed)
dnf update -y -C   # -C = cache only, no network

> **Remember:** Pre-staging with `--downloadonly` before the maintenance window cuts your actual downtime to just the install + restart phase. The download is often 80% of the wall-clock time in a patching window.