Rocky Linux, AlmaLinux and CentOS Stream ship PHP as module streams. This guide installs PHP 8.3 from Remi’s repository, which adds every PHP branch as a stream such as php:remi-8.3. Remi is one volunteer, though. When PHP released the PHP2Shell fixes on 21 October 2026, its advisory urged users of rpms.remirepo.net to switch to distribution packages or build from source, because the repository would not have updates that day. Remi’s fixed 8.3.36 build arrived on 22 October, and Rocky Linux 9’s own AppStream php:8.3 stream carries the same fix, so you have two patched routes to pick from.
This guide adds EPEL and Remi, installs PHP 8.3 with the extensions most web apps expect, checks the build against PHP2Shell, opens the firewall and wires PHP-FPM into Nginx or Apache. Every command below was run on fresh Rocky Linux 9 systems in October 2026, and Rocky Linux 8 ran the same install and both web server setups. We did not run AlmaLinux or CentOS hosts; their package status below comes from our package checks, not from this walkthrough.
Enable EPEL and Remi’s repositories
PHP 8.5 is the newest branch, but 8.3 still gets upstream security releases, and plenty of applications pin it. The 8.3 packages used here come from Remi’s third party RPM repository, which needs EPEL plus the CRB repository (PowerTools on EL8) first. On Rocky, AlmaLinux or CentOS Stream 9, install EPEL:
sudo dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
Then enable CRB with the crb helper that epel-release installs. It prints CRB repo is enabled and named: crb on Rocky Linux 9:
sudo crb enable
On an EL8 system (Rocky Linux 8 or AlmaLinux 8), install the EL8 EPEL package and enable PowerTools instead:
sudo dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
sudo dnf config-manager --set-enabled powertools
Now add Remi. On EL9, install the EL9 release package:
sudo dnf -y install https://rpms.remirepo.net/enterprise/remi-release-9.rpm
On EL8, use the EL8 package:
sudo dnf -y install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
Update the dnf cache after adding the repositories.
sudo dnf makecache -y
List the configured repositories to confirm they are active:
sudo dnf repolist
On Rocky Linux 9 you should see crb, epel, remi-modular and remi-safe next to the distro repositories:
repo id repo name
appstream Rocky Linux 9 - AppStream
baseos Rocky Linux 9 - BaseOS
crb Rocky Linux 9 - CRB
epel Extra Packages for Enterprise Linux 9 - x86_64
epel-cisco-openh264 Extra Packages for Enterprise Linux 9 openh264 (From Cisco) - x86_64
extras Rocky Linux 9 - Extras
remi-modular Remi's Modular repository for Enterprise Linux 9 - x86_64
remi-safe Safe Remi's RPM repository for Enterprise Linux 9 - x86_64
Reset the PHP module so no distro stream is left enabled. On a fresh system this prints “Nothing to do”, which is fine.
sudo dnf module reset php -y
The Remi repository guide covers the other streams and the non-modular SCL packages if you need more than one PHP version on the same host.
Install PHP 8.3 and extensions
Enable the PHP 8.3 Remi module and install its default profile:
sudo dnf module -y install php:remi-8.3
The profile brings in the CLI, PHP-FPM and a few core extensions, all from remi-modular:
Dependencies resolved.
================================================================================
Package Arch Version Repository Size
================================================================================
Installing group/module packages:
php-cli x86_64 8.3.36-1.module_php.8.3.el9.remi remi-modular 5.6 M
php-common x86_64 8.3.36-1.module_php.8.3.el9.remi remi-modular 1.3 M
php-fpm x86_64 8.3.36-1.module_php.8.3.el9.remi remi-modular 1.9 M
php-mbstring x86_64 8.3.36-1.module_php.8.3.el9.remi remi-modular 578 k
php-xml x86_64 8.3.36-1.module_php.8.3.el9.remi remi-modular 218 k
Installing dependencies:
httpd-filesystem noarch 2.4.62-13.el9_8.6 appstream 13 k
libedit x86_64 3.1-39.20210216cvs.el9 baseos 103 k
libxslt x86_64 1.1.34-14.el9_7.1 appstream 240 k
oniguruma5php x86_64 6.9.10-1.el9.remi remi-safe 222 k
Installing weak dependencies:
nginx-filesystem noarch 2:1.20.1-28.el9_8.6.rocky.0.1 appstream 11 k
Installing module profiles:
php/common
Enabling module streams:
php remi-8.3
Transaction Summary
================================================================================
Install 10 Packages
Install the php package for CLI use and the other key extensions. A few names in this list (json, curl, gettext, cgi) are built into php-common or php-cli on PHP 8, and dnf resolves them to those packages without complaint.
sudo dnf -y install php php-{common,pear,cgi,curl,mbstring,gd,mysqlnd,gettext,bcmath,json,xml,fpm,intl,zip,imap}
Check the PHP version to confirm the installation was successful.
php --version
Rocky Linux 9 and Rocky Linux 8 both reported the same Remi build:
PHP 8.3.36 (cli) (built: Oct 19 2026 14:00:34) (NTS gcc x86_64)
Copyright (c) The PHP Group
Zend Engine v4.3.36, Copyright (c) Zend Technologies
with Zend OPcache v8.3.36, Copyright (c), by Zend Technologies
You can also list the loaded modules.
php --modules
The list should include gd, imap, intl, mysqlnd, zip and Zend OPcache. A missing name means its package did not install, so rerun the dnf line above and read its errors.
Confirm PHP 8.3 is patched for PHP2Shell
PHP2Shell is two critical remote code execution bugs in PHP itself, CVE-2026-109618 and CVE-2026-109619, which php.net rated CVSS v4.0 9.2 in its 20 October pre-announcement. PHP fixed both on 21 October 2026 in 8.5.12, 8.4.27, 8.3.36 and 8.2.35 (PHP security advisory). The same advisory told rpms.remirepo.net users to switch to distribution packages or build from source, since Remi would not have updates on release day. Remi’s php:remi-8.3 stream for EL9 and EL8 shipped 8.3.36 on 22 October, so a fresh install from this guide is now patched. Query the package to prove it on your own server:
rpm -q php-common
Remi moves 8.3 to each upstream release, so the version is the check here: 8.3.36 or later is fixed, anything older is not. Remi’s changelog only says “Update to 8.3.36” and doesn’t name the CVE IDs, so grepping the changelog for them prints nothing even on a patched box. Our Rocky Linux 9 system returned:
php-common-8.3.36-1.module_php.8.3.el9.remi.x86_64
On a fresh install you can stop here and move on to the web server. The rest of this section is for an existing server that still runs an older 8.3 build. Upgrade every PHP package in one transaction. If dnf says there’s nothing to do while rpm -q still shows an older build, your mirror is behind: run sudo dnf clean metadata and try again.
sudo dnf -y upgrade "php*"
Restart PHP-FPM so the workers load the new code. In our test the php-fpm package restarted the service during the upgrade (its main PID changed), but restart it anyway and confirm, because a worker that still runs the old build is still exploitable. Apache in this guide hands PHP to PHP-FPM, so httpd does not need a restart. If you moved Apache to the prefork MPM to load mod_php, run sudo systemctl restart httpd as well. Restart long-running PHP CLI processes too, such as queue workers under supervisor.
sudo systemctl restart php-fpm
Prove the running service is newer than the upgrade. Read when the transaction ended first:
sudo dnf history info last | head -5
The upgrade on our test server finished at 16:03:37:
Transaction ID : 11
Begin time : Thu Oct 22 16:03:35 2026
Begin rpmdb : 41ae26be89749cf0772ab1c4c37a039f3d3ca59c57f2c356647f87133f1e031d
End time : Thu Oct 22 16:03:37 2026 (2 seconds)
End rpmdb : 07af98dba34f7e557ac5c8bf49b1bd8a06c414ba32bd8776027e7be29a7cc02d
last only means the PHP upgrade when that upgrade actually changed packages. If dnf printed “Nothing to do”, the last transaction is something else, so find the newest one that touched PHP with sudo dnf history list php-common and pass its ID to dnf history info. Then ask systemd when php-fpm last entered the active state:
systemctl show -p ActiveEnterTimestamp php-fpm
The timestamp must be later than the End time above. Here it is, so every FPM worker runs 8.3.36. If the time is older, the restart did not happen.
ActiveEnterTimestamp=Thu 2026-10-22 16:03:48 UTC
If you’d rather follow the distribution channel the advisory points to, Rocky Linux 9’s AppStream has fixed builds too: php:8.3 at 8.3.36, php:8.2 at 8.2.35, and the default non-modular PHP at 8.0.30-8.el9_8.1. Its php:8.1 stream had no fix on 22 October 2026 (still 8.1.32). CentOS Stream 9’s 8.1, 8.2 and 8.3 streams are stale and unfixed, while AlmaLinux 9 and 8 have fixed builds in every stream. On Rocky Linux 8, the AppStream php:8.2 stream we installed was 8.2.35 (fixed); we did not check its other streams. To move to the patched AppStream 8.3 stream, use dnf module switch-to, which swaps every PHP package in one transaction. The AppStream stream is named php:8.3 and Remi’s is php:remi-8.3, so the command below picks AppStream even with Remi’s repositories enabled:
sudo dnf -y module switch-to php:8.3
On a server built from this guide the switch stops with an error, because php-imap and php-sodium came from Remi and AppStream doesn’t build them:
Error:
Problem 1: package php-imap-8.3.36-1.module_php.8.3.el9.remi.x86_64 from @System requires php-common(x86-64) = 8.3.36-1.module_php.8.3.el9.remi, but none of the providers can be installed
Problem 2: package php-sodium-8.3.36-1.module_php.8.3.el9.remi.x86_64 from @System requires php-common(x86-64) = 8.3.36-1.module_php.8.3.el9.remi, but none of the providers can be installed
Remove those two extensions first, then rerun the switch. Your applications lose the imap_* and sodium_* functions, so check that nothing on the server calls them:
sudo dnf -y remove php-imap php-sodium
After the removal the same switch-to command went through on our Rocky Linux 9 box with Remi still enabled, and it also took a separate test box from AppStream’s 8.1.32. Either way, rpm -q php-common then printed the Red Hat rebase:
php-common-8.3.36-1.module+el9.8.0+40396+4e680bb7.x86_64
Like Remi’s, that changelog says “rebase to 8.3.36” without CVE IDs, so the version is the check. For the Remi stream, the same command takes php:remi-8.3. Restart php-fpm and repeat the timestamp check afterwards. A jump from 8.1 to 8.3 can break old application code, so test the app first. If you can’t move yet, the PHP2Shell mitigation guide shows the interim PHP-FPM and web server settings we tested.
CentOS Linux 8 is end of life, and its repositories no longer work at all: mirrorlist.centos.org no longer resolves and mirror.centos.org holds only a pointer to the vault archive, so dnf on a stock CentOS 8 box can’t install anything from this guide. Remi’s EL8 build of 8.3.36 is fixed (we installed it on Rocky Linux 8), but a CentOS 8 server around it stays unpatched, so plan the move to a supported release and follow the PHP guide for Rocky Linux 10 and AlmaLinux 10.
Using PHP with Nginx or Apache
If you are using Nginx or Apache virtual hosts you can configure them to use PHP. Both talk to the same PHP-FPM pool, and both bind port 80, so run one of them on a given server.
1) With Nginx web server
You will require PHP-FPM (FastCGI Process Manager) to listen for incoming PHP requests and execute them.
sudo dnf install nginx php-fpm vim -y
Start and enable the services.
sudo systemctl enable --now nginx php-fpm
systemd creates the two symlinks that start both services at boot:
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /usr/lib/systemd/system/nginx.service.
Created symlink /etc/systemd/system/multi-user.target.wants/php-fpm.service → /usr/lib/systemd/system/php-fpm.service.
Leave the PHP-FPM pool as Remi ships it. The default www pool runs as apache and listens on a Unix socket whose ACL already lets the nginx user connect. Don’t change the pool user to nginx: the session directory /var/lib/php/session belongs to group apache, and when we switched the user in our test, session_start() stopped working. Confirm the defaults:
grep -E '^(user|group|listen|listen.acl_users) ' /etc/php-fpm.d/www.conf
Rocky Linux 9 and 8 both print the same four lines, and the socket path is the one Nginx points at below:
user = apache
group = apache
listen = /run/php-fpm/www.sock
listen.acl_users = apache,nginx
Create a server block for your site in its own file under /etc/nginx/conf.d/, which the main nginx.conf includes inside its http block:
sudo vim /etc/nginx/conf.d/myapp.conf
Add the following, with your own domain and document root:
server {
listen 80;
listen [::]:80;
server_name myapp.example.com;
root /var/www/myapp;
index index.php index.html;
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php-fpm/www.sock;
}
}
Two lines in that block fix failures we hit with the older version of this config. Without fastcgi_param SCRIPT_FILENAME, every PHP request returns 404 and the Nginx error log says FastCGI sent in stderr: "Primary script unknown", because the EL fastcgi_params file does not set it. Without listen [::]:80, a request to localhost arrives over IPv6 and lands on the distro’s default server, which returns the stock “page is not found” error on Rocky Linux 8.
Check the configuration syntax.
sudo nginx -t
Both lines should end in “ok” and “successful”:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Create the document root and reload Nginx so it picks up the new server block:
sudo mkdir -p /var/www/myapp
sudo systemctl reload nginx
For a full stack with MariaDB and Let’s Encrypt on top of this, the LEMP stack walkthrough picks up from here.
2) With Apache web server
Apache on EL9 and EL8 runs the event MPM, and /etc/httpd/conf.d/php.conf hands .php files to PHP-FPM over /run/php-fpm/www.sock. The php package does install mod_php, but on Rocky Linux 9 /etc/httpd/conf.modules.d/20-php.conf only loads it under the prefork MPM, so with the defaults PHP-FPM must be running for Apache to serve PHP.
If the Apache httpd package is not installed, install it as below.
sudo dnf -y install httpd php-fpm
Nginx and Apache both bind port 80, so disable Nginx before you start httpd. When we started httpd with Nginx still running, it failed with (98)Address already in use: AH00072: make_sock: could not bind to address 0.0.0.0:80.
sudo systemctl disable --now nginx
Now start and enable httpd together with PHP-FPM.
sudo systemctl enable --now httpd php-fpm
Both services should report active:
systemctl is-active httpd php-fpm
Anything other than two active lines means one of them failed to start, and journalctl -u httpd or journalctl -u php-fpm shows why:
active
active
The LAMP setup on Rocky Linux 9 adds MariaDB and virtual hosts on top of this Apache install.
Open HTTP and HTTPS in firewalld
Rocky Linux and AlmaLinux run firewalld, which blocks ports 80 and 443 until you allow them, so a browser on another machine can’t reach the site yet. If sudo firewall-cmd answers command not found, firewalld isn’t in use on that server, so open ports 80 and 443 in your provider’s security group instead, or install firewalld with sudo dnf -y install firewalld and sudo systemctl enable --now firewalld as we did. Add both services permanently and reload the rules:
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload
Each command prints success. List the allowed services to confirm http and https are there:
sudo firewall-cmd --list-services
Our Rocky Linux 9 server showed the defaults plus the two new entries:
cockpit dhcpv6-client http https ssh
If the list lacks them after a reload, the first command failed, so rerun it and read the error.
Testing PHP 8.3 installation
Create a test PHP file in the Apache web root /var/www/html/ to check that PHP works. A plain sudo echo ... > file fails with “Permission denied” for a normal user, because the redirect runs in your own shell, so pipe the line through sudo tee. With Nginx, write the file to /var/www/myapp/ instead (the root of your server block).
echo "<?php phpinfo(); ?>" | sudo tee /var/www/html/test.php
With Apache, open the test page at http://10.0.1.50/test.php, using your server’s IP address. With Nginx, use the server_name domain, such as http://myapp.example.com/test.php, because a request that doesn’t match it can land on the stock default server (on our Rocky Linux 9 box the bare IP still reached the vhost, since conf.d loads before the stock server). If that name doesn’t resolve yet, add it to your local hosts file or test from the server with curl -H 'Host: myapp.example.com' http://127.0.0.1/test.php. On our Rocky Linux 9 box the page title read PHP 8.3.36 - phpinfo() and Server API showed FPM/FastCGI. The screenshot below is from the original setup in January 2024 and shows PHP 8.3.2, a build that is vulnerable to PHP2Shell; yours should show 8.3.36 or later.

We can confirm PHP is working as anticipated. Delete the test script now, because phpinfo() tells an attacker exactly which build and extensions you run.
sudo rm -f /var/www/html/test.php /var/www/myapp/test.php
Here is a list of books to read on PHP and web technologies.
For more in depth information about PHP visit the php.net documentation.