Understanding DNF Modules and Module Streams on Fedora

DNF modules let you install and switch between different major versions of software on Fedora — such as Node.js 18 vs 20 — without manually managing conflicting package dependencies.

The version mismatch that breaks your build

You upgraded your project to Node.js 20, but your local machine still runs version 18. You run sudo dnf install nodejs and the package manager refuses to upgrade, citing a dependency conflict. You are stuck on an old runtime because Fedora ships one default version per release cycle. The solution is not to fight the package manager. The solution is to tell it which version track you actually want.

How module streams actually work

DNF modules solve the flat repository problem. Instead of treating every package as an independent unit, Fedora groups related software into logical collections. Each collection contains streams. A stream is a version track that moves independently of the default repository state. Think of it like a railway switch. The default track points to the version Fedora tested for that specific release. Other tracks run parallel. You can flip the switch, and every package on that track updates together.

Profiles are pre-selected subsets of packages within a stream. The default profile installs the runtime and core libraries. The development profile pulls in headers, debug symbols, and build tools. This separation keeps your system lean when you only need to execute software, not compile it. Fedora maintains these modules in the AppStream repository. The package manager treats the entire module as a single dependency unit. When you enable a stream, you lock the stack to a specific upstream release. All packages in that stream share the same version baseline.

Run dnf module list before you install anything. Knowing what is available prevents half the dependency headaches.

Finding and enabling a stream

Here is how to see every module available in your current Fedora release.

# Show all modules, streams, and profiles in the enabled repos
dnf module list

# Filter the output to a specific module name
dnf module list nodejs

The output uses bracketed markers to show state. [d] marks the default stream. [e] marks an enabled stream. [i] marks an installed profile. If you see [d] next to nodejs:18, Fedora will install version 18 unless you explicitly change the selection. You must enable a stream before you can install its packages. Enabling does not install anything. It only tells DNF which track to follow during dependency resolution.

Here is how to lock your system to a specific version track.

# Enable the 20.x stream for nodejs without installing yet
sudo dnf module enable nodejs:20

# Install the default profile from the newly enabled stream
sudo dnf install nodejs

You can combine these steps into a single transaction if you prefer fewer commands. The module install command enables the stream and pulls the default profile in one pass.

# Enable the stream and install the default profile simultaneously
sudo dnf module install nodejs:20/default

Stick to the default profile unless you specifically need development headers. Extra build packages accumulate quickly and rarely get used after the first compile.

Switching versions without breaking dependencies

Switching streams requires clearing the current lock first. DNF will not allow two streams of the same module to be active at once. You must reset the module to unlock it, enable the new stream, and then sync your installed packages to match the new track.

Here is the safe sequence for moving between major versions.

# Clear the current stream selection without removing packages
sudo dnf module reset nodejs

# Enable the target stream you actually want
sudo dnf module enable nodejs:22

# Replace installed packages with versions from the new stream
sudo dnf distro-sync

The distro-sync command is different from dnf upgrade. Upgrade only moves packages forward. Distro-sync moves packages forward or backward to match the enabled repository state. This is necessary when switching streams because the new track may require older library versions or newer shared objects. Always run distro-sync after changing a module stream. Skipping it leaves your system in a half-updated state that breaks on the next transaction.

Check the module state after the sync completes. Verify the stream matches your intent before you restart services.

Verifying your active stack

Here is how to confirm which stream and profile your system is currently tracking.

# Display the enabled stream, active profiles, and package list
dnf module info nodejs

The output shows the stream version, the enabled profiles, and every package included in that selection. Cross-reference this with the actual binary version on your system.

# Check the runtime version that matches the enabled stream
node --version

# Verify the package manager sees the correct stream
dnf repoquery --installed --qf "%{name} %{version} %{release}" nodejs

The repoquery command bypasses the module abstraction and shows the exact RPM metadata. This is useful when troubleshooting mismatched binaries. If the binary version does not match the stream you enabled, your shell might be caching an old path. Clear your environment or open a new terminal.

Run dnf module info before every major project change. Track your stack the same way you track git branches.

Common pitfalls and dependency conflicts

Module streams lock dependencies tightly. If you install a package outside the module that conflicts with the stream, DNF will abort the transaction. You will see a hard failure that looks like this.

Error: Transaction test error:
  package nodejs-1:20.11.0-2.fc39.x86_64 conflicts with nodejs provided by nodejs-1:18.19.0-1.fc39.x86_64

The conflict is intentional. DNF refuses to mix two different runtimes because they ship overlapping binaries and shared libraries. The fix is to reset the module first. Never use --skip-broken or --best to force a module switch. Those flags bypass the stream lock and leave your system with orphaned packages.

Another common issue involves third-party repositories. Some external repos ship their own versions of module packages. If you add a repository that provides nodejs without respecting the module stream, DNF will complain about duplicate packages. Keep third-party repos separate from module-managed software. Use language-specific version managers for isolated projects.

Run dnf module reset before you debug a conflict. Clear the lock, re-enable the correct stream, and let DNF resolve the rest.

Choosing the right packaging method

Use DNF modules when you need a specific major version of a language runtime or database and want full system integration. Use COPR repositories when you need a patched kernel module or a bleeding-edge package that Fedora has not yet adopted. Use Flatpak when you want sandboxed desktop applications that do not interfere with system libraries. Use language-specific version managers like nvm or pyenv when you need multiple isolated runtimes for different projects on the same machine. Stay on the default DNF repository when you only need the standard version and want zero maintenance overhead.

Pick the tool that matches your isolation requirements. System-wide runtimes belong in modules. Project-specific runtimes belong in user-space managers.

Where to go next