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 nginxlocks 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.
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, usednf 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:
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.