dnf Module Streams on RHEL 8 and 9: Install Multiple App Versions the Right Way
π― Key Takeaways
- dnf Module Streams on RHEL 8 and 9: Install Multiple App Versions the Right Way
- What Is AppStream and Why Red Hat Introduced Module Streams
- BaseOS vs AppStream: What Goes Where
- dnf module list: See Available Modules and Streams
- dnf module info: Understand What a Stream Provides
π Table of Contents
- dnf Module Streams on RHEL 8 and 9: Install Multiple App Versions the Right Way
- What Is AppStream and Why Red Hat Introduced Module Streams
- BaseOS vs AppStream: What Goes Where
- dnf module list: See Available Modules and Streams
- dnf module info: Understand What a Stream Provides
- Installing a Specific Stream
- Understanding Module Profiles
- Real Scenario 1: Install PHP 8.2 Instead of Default 8.1
- Real Scenario 2: Run Python 3.11 Alongside System Python
- Real Scenario 3: PostgreSQL 15 vs Default Version
- Switching Between Streams
- Resetting a Module Stream
- When Modules Conflict With Each Other
- Disabling a Module Stream
- Node.js Module Streams
- Checking Module Status
- Common Beginner Mistakes
- Conclusion
dnf Module Streams on RHEL 8 and 9: Install Multiple App Versions the Right Way
One of the most frequently misunderstood features of RHEL 8 and RHEL 9 is the AppStream module system. Admins coming from Ubuntu or Debian are accustomed to PPAs and alternative package repositories for installing non-default versions of software. On RHEL, Red Hat built a more structured approach directly into the package manager: DNF module streams.
π Table of Contents
- dnf Module Streams on RHEL 8 and 9: Install Multiple App Versions the Right Way
- What Is AppStream and Why Red Hat Introduced Module Streams
- BaseOS vs AppStream: What Goes Where
- BaseOS
- AppStream
- dnf module list: See Available Modules and Streams
- dnf module info: Understand What a Stream Provides
- Installing a Specific Stream
- Method 1: Enable the Stream Then Install
- Method 2: Install and Enable in One Command
- Understanding Module Profiles
- Real Scenario 1: Install PHP 8.2 Instead of Default 8.1
- Real Scenario 2: Run Python 3.11 Alongside System Python
- Real Scenario 3: PostgreSQL 15 vs Default Version
- Switching Between Streams
- Resetting a Module Stream
- When Modules Conflict With Each Other
- Disabling a Module Stream
- Node.js Module Streams
- Checking Module Status
- Common Beginner Mistakes
- Conclusion
If you have ever tried to install PHP 8.2 on RHEL 9 and gotten an older version, or struggled to run multiple Python versions, this guide explains exactly why that happens and how the module system is designed to solve it. More importantly, it shows you how to use module streams correctly so you get the exact version you need without breaking your system or fighting dependency conflicts.
What Is AppStream and Why Red Hat Introduced Module Streams
RHEL has a ten-year support lifecycle. This creates a fundamental tension: the operating system needs to be stable and unchanging, but applications like PHP, Node.js, and Python release new major versions regularly and have much shorter support windows.
Before RHEL 8, Red Hat’s solution was Software Collections (SCL) β an add-on repository that installed additional versions of applications into /opt/rh/ and required a special scl enable command to activate them. It worked, but it was clunky and required every script and service definition to know about SCL.
AppStream module streams are Red Hat’s cleaner replacement. They allow multiple versions of a package to coexist in the same repository, organized into named streams, with one stream selected as the default at any time. When you enable a specific stream, all dnf install commands for that application automatically get packages from that stream β no special wrapper commands required.
The core design principle: RHEL’s BaseOS packages are stable and long-lived. AppStream packages can have shorter lifecycles, with different streams representing different supported versions.
BaseOS vs AppStream: What Goes Where
Understanding which repo a package comes from matters because it tells you about the package’s support lifecycle and stability expectations.
BaseOS
BaseOS contains the core operating system components that form the stable foundation of RHEL. These packages are supported for the full 10-year lifecycle of the RHEL major version without major version updates. Examples: kernel, glibc, systemd, OpenSSL, bash, Python 3 (the system Python), OpenSSH.
AppStream
AppStream contains user-space applications and development tools that have more rapid release cycles. AppStream packages are organized into modules with streams representing different versions. Examples: PHP 7.4, PHP 8.0, PHP 8.2, Node.js 18, Node.js 20, PostgreSQL 13, PostgreSQL 15, MariaDB 10.5, MariaDB 10.11.
# Verify both repos are enabled
$ dnf repolist
repo id repo name
rhel-9-for-x86_64-appstream-rpms Red Hat Enterprise Linux 9 for x86_64 - AppStream
rhel-9-for-x86_64-baseos-rpms Red Hat Enterprise Linux 9 for x86_64 - BaseOS
dnf module list: See Available Modules and Streams
# List all available modules
$ dnf module list
Red Hat Enterprise Linux 9 for x86_64 - AppStream (RPMs)
Name Stream Profiles Summary
mariadb 10.5 client, galera, server MariaDB Module
mariadb 10.11 [d] client, galera, server MariaDB Module
maven 3.8 [d] common Java project management tool
nodejs 18 [d] common, development, Javascript runtime
minimal, s2i
nodejs 20 common, development, Javascript runtime
minimal, s2i
php 8.1 [d] common, devel, minimal PHP scripting language
php 8.2 common, devel, minimal PHP scripting language
postgresql 13 client, server PostgreSQL server and client module
postgresql 15 [d] client, server PostgreSQL server and client module
postgresql 16 client, server PostgreSQL server and client module
python311 3.11 [d] default Python 3.11
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled
The hint at the bottom explains the symbols:
- [d] β this is the default stream for this module. If you install the package without specifying a stream, this is what you get.
- [e] β this stream is currently enabled on your system.
- [x] β this stream is disabled.
- [i] β packages from this module are installed.
dnf module info: Understand What a Stream Provides
# Get detailed information about a module and its streams
$ dnf module info php
Name : php
Stream : 8.1 [d][a]
Version : 9010020230214152437.rhel9.1
Context : rhel9
Architecture : x86_64
Profiles : common [d], devel, minimal
Default profiles : common
Repo : rhel-9-for-x86_64-appstream-rpms
Summary : PHP scripting language
Description : PHP is an HTML-embedded scripting language. PHP attempts to make
it easy for developers to write dynamically generated webpages.
Artifacts : apcu-panel-0:5.1.21-1.module+el9.1.0+13933+e44d7e05.noarch
: php-0:8.1.14-1.module+el9.1.0+17883+48b9a97d.x86_64
: php-bcmath-0:8.1.14-1.module+...x86_64
: php-cli-0:8.1.14-1.module+...x86_64
: php-common-0:8.1.14-1.module+...x86_64
: php-devel-0:8.1.14-1.module+...x86_64
: php-fpm-0:8.1.14-1.module+...x86_64
# Get info for a specific stream
$ dnf module info php:8.2
Installing a Specific Stream
Method 1: Enable the Stream Then Install
# Step 1: Enable the specific stream
$ dnf module enable php:8.2 -y
Dependencies resolved.
================================================================================
Package Architecture Version Repository Size
================================================================================
Enabling module streams:
php 8.2
Transaction Summary
================================================================================
Complete!
# Step 2: Install PHP
$ dnf install php php-fpm php-cli -y
# Verify the installed version
$ php --version
PHP 8.2.15 (cli) (built: Jan 16 2024 13:04:52) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.15, Copyright (c) Zend Technologies
Method 2: Install and Enable in One Command
# The @ syntax enables the stream and installs the default profile
$ dnf module install php:8.2 -y
# Or with a specific profile
$ dnf module install php:8.2/common -y
Understanding Module Profiles
Within each stream, there are profiles β predefined sets of packages for different use cases. Installing a profile installs a curated group of related packages for that purpose.
# Common PHP profiles
# common β installs php, php-cli, php-fpm, and common extensions
# devel β installs everything in common plus headers and debugging tools
# minimal β installs only the core php package
# Install the devel profile to get headers for extension compilation
$ dnf module install php:8.2/devel -y
# For PostgreSQL, the profiles are:
# client β only the psql client and libraries
# server β the server and client
$ dnf module install postgresql:15/server -y
Real Scenario 1: Install PHP 8.2 Instead of Default 8.1
# Check what is currently installed
$ php --version 2>/dev/null || echo "PHP not installed"
PHP not installed
# Check the default stream
$ dnf module list php
Name Stream Profiles Summary
php 8.1 [d] common [d], devel, min PHP scripting language
php 8.2 common [d], devel, min PHP scripting language
# Enable PHP 8.2 and install
$ dnf module enable php:8.2 -y
$ dnf install php php-fpm php-mysqlnd php-mbstring php-xml php-json -y
# Verify
$ php --version
PHP 8.2.15 (cli) (built: Jan 16 2024 13:04:52) (NTS)
$ php -m | head -20
[PHP Modules]
bcmath
calendar
Core
ctype
curl
date
exif
fileinfo
filter
ftp
gettext
gmp
hash
iconv
json
libxml
# Start and enable php-fpm
$ systemctl enable --now php-fpm
Real Scenario 2: Run Python 3.11 Alongside System Python
On RHEL 9, the system Python is Python 3.9 from BaseOS. This is the Python that system tools depend on and should not be replaced. If you need Python 3.11 for your applications, the module system installs it alongside the system Python without replacing it.
# Check system Python
$ python3 --version
Python 3.9.18
# Check available Python modules
$ dnf module list python*
Name Stream Profiles Summary
python311 3.11 [d] default Python 3.11
# Enable and install Python 3.11
$ dnf module enable python311:3.11 -y
$ dnf install python3.11 -y
# Python 3.11 is now available as python3.11 β system python3 is unchanged
$ python3 --version
Python 3.9.18
$ python3.11 --version
Python 3.11.7
# Create a virtual environment with Python 3.11 for your application
$ python3.11 -m venv /opt/myapp/venv
$ source /opt/myapp/venv/bin/activate
(venv) $ python --version
Python 3.11.7
This is the critical point: do not replace the system Python. RHEL’s system tools β dnf, ansible, and others β depend on the system Python. Installing Python 3.11 through the module system gives you the version you need without touching the system Python.
Real Scenario 3: PostgreSQL 15 vs Default Version
# Check available PostgreSQL streams
$ dnf module list postgresql
Name Stream Profiles Summary
postgresql 13 client, server PostgreSQL server
postgresql 15 [d] client, server PostgreSQL server
postgresql 16 client, server PostgreSQL server
# Install PostgreSQL 16 (non-default stream)
$ dnf module enable postgresql:16 -y
$ dnf module install postgresql:16/server -y
# Initialize the database
$ postgresql-setup --initdb
$ systemctl enable --now postgresql
# Verify version
$ psql --version
psql (PostgreSQL) 16.1
$ sudo -u postgres psql -c "SELECT version();"
version
----------------------------------------------------------------------------------------------------------------------
PostgreSQL 16.1 on x86_64-redhat-linux-gnu, compiled by gcc (GCC) 11.4.1 20231218 (Red Hat 11.4.1-3), 64-bit
(1 row)
Switching Between Streams
Switching between streams is possible but requires care. You must reset the current stream, enable the new stream, and then update the installed packages. This is not a trivial operation if you have data or configurations tied to the current version.
# Current situation: PHP 8.1 is installed and active
$ php --version
PHP 8.1.27
# Step 1: Remove the currently installed packages
$ dnf remove php php-fpm php-cli php-common -y
# Step 2: Reset the module (clears the stream selection)
$ dnf module reset php -y
Dependencies resolved.
Resetting modules:
php
Complete!
# Step 3: Enable the new stream
$ dnf module enable php:8.2 -y
# Step 4: Install the packages from the new stream
$ dnf install php php-fpm php-cli -y
# Step 5: Verify
$ php --version
PHP 8.2.15
Important: Before switching PHP or database streams on a production server, always test in a staging environment. Major version upgrades can require configuration changes, deprecated function removal, and database schema migrations that are not handled automatically.
Resetting a Module Stream
# Reset a module completely (removes stream selection and all package installs)
$ dnf module reset php -y
# After reset, the module is back to unselected state
$ dnf module list php
Name Stream Profiles Summary
php 8.1 [d] common [d], devel, min PHP scripting language
php 8.2 common [d], devel, min PHP scripting language
When Modules Conflict With Each Other
DNF module streams enforce stream compatibility rules. If you try to enable two streams that conflict, dnf will refuse.
# Attempting to enable two conflicting streams
$ dnf module enable php:8.1 -y
Complete!
$ dnf module enable php:8.2 -y
Error: It is not possible to switch enabled streams of a module unless explicitly enabled via configuration option module_stream_switch.
Error: Problems in request:
missing groups or modules: php:8.2
# Fix: reset first, then enable the new stream
$ dnf module reset php -y
$ dnf module enable php:8.2 -y
The error message is clear: you cannot switch streams while another stream is enabled. Always reset before switching.
Disabling a Module Stream
# Disable a module stream (prevents packages from this stream being installed)
$ dnf module disable php:8.1 -y
# This prevents accidentally installing packages from the old stream
# Verify disabled status
$ dnf module list php
Name Stream Profiles Summary
php 8.1 [x] common [d], devel, min PHP scripting language
php 8.2 [e] common [d], devel, min PHP scripting language
Node.js Module Streams
# Check available Node.js streams
$ dnf module list nodejs
Name Stream Profiles Summary
nodejs 18 [d] common, development, minimal, s2i JavaScript runtime
nodejs 20 common, development, minimal, s2i JavaScript runtime
# Install Node.js 20
$ dnf module enable nodejs:20 -y
$ dnf install nodejs -y
$ node --version
v20.11.0
$ npm --version
10.2.4
Checking Module Status
# See which modules are currently enabled or installed on your system
$ dnf module list --enabled
Red Hat Enterprise Linux 9 for x86_64 - AppStream (RPMs)
Name Stream Profiles Summary
nodejs 20 [e] common, development JavaScript runtime
php 8.2 [e] common [d] PHP scripting language
postgresql 16 [e] server PostgreSQL server
# See installed modules
$ dnf module list --installed
Common Beginner Mistakes
- Installing a package without enabling the right stream first: If you run
dnf install phpwithout enabling a stream, you get the default stream version. Always enable the stream explicitly for non-default versions. - Trying to install two competing streams simultaneously: You cannot have PHP 8.1 and PHP 8.2 from module streams on the same system at the same time. Use containers if you need multiple major versions simultaneously.
- Replacing the system Python: Never use the module system to replace
/usr/bin/python3. Install the new version alongside it and use virtual environments. - Switching streams without resetting first: Always run
dnf module resetbefore enabling a different stream. Trying to enable a stream while another is active results in an error. - Forgetting to install packages after enabling a stream: Enabling a stream does not install any packages. It just selects which version to use. You still need to run
dnf install. - Not checking available streams before installing: Always run
dnf module list <name>first to see what streams are available. Do not assume the latest version is the default.
Conclusion
DNF module streams are one of RHEL’s most practical features for server administrators who need specific versions of application stacks. The pattern is always the same: list available streams, enable the stream you need, install the packages, and verify. For switching versions, always reset the module first. The module system solves the version flexibility problem that previously required PPAs on Ubuntu or SCL on RHEL 7 β in a cleaner, more integrated way that plays well with DNF’s dependency resolver and Red Hat’s support model.
Was this article helpful?
About Ramesh Sundararamaiah
Red Hat Certified Architect
Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.