CentOS

How To Install PHP 8.4 on CentOS Stream 10 / 9

This guide installs PHP 8.4 on CentOS Stream 10 and CentOS Stream 9 from Remi’s modular repository. The distro’s own php package is 8.3 on Stream 10 and 8.0 on Stream 9, and both of those already carry the PHP2Shell fix. That matters because php.net’s 21 October advisory told Remi users to switch to distribution packages, and Remi shipped its fixed builds on 22 October. The guide covers the repository setup, the PHP2Shell check and serving PHP through Nginx or Apache with PHP-FPM. The install, the PHP2Shell check and the Nginx setup below were run on fresh CentOS Stream 10 and CentOS Stream 9 systems in October 2026 with PHP 8.4.27 from Remi, and the Apache steps on CentOS Stream 10.

Original content from computingforgeeks.com - post 161397

PHP is a general-purpose server-side scripting language used for making powerful, dynamic, and interactive websites. It was initially created by a Canadian developer by the name of Rasmus Lerdorf in 1993 but was released to the general public in 1995. PHP is now maintained by the PHP Group. Since then several versions have been released as described on the ChangeLog

PHP 8.4 was released on November 21, 2024, and brings the language changes below. It gets active support until 31 December 2026 and security fixes until 31 December 2028, according to the PHP supported versions page.

I will briefly look at the features that come with PHP 8.4. PHP 8.4 is designed for better performance, better syntax, and improved type safety.

Features PHP 8.4

Some of the features released with PHP 8.4 are as briefly described below. The examples used in this blog are from PHP 8.4.0 Release notes.

  • Property Hooks: Property hooks eliminate the need to write docblock comments that could become out of sync by supporting computed properties that are natively understood by IDEs and static analysis tools. Additionally, they make it possible to reliably pre- or post-process values without having to verify that the class contains a matching getter or setter.
  • Asymmetric Visibility: In order to expose a property’s value without permitting alteration from outside of a class, boilerplate getter methods are no longer necessary because the scope to write to and read a property can now be controlled individually.
# In earlier versions
class PhpVersion
{
    private string $version = '8.3';

    public function getVersion(): string
    {
        return $this->version;
    }

    public function increment(): void
    {
        [$major, $minor] = explode('.', $this->version);
        $minor++;
        $this->version = "{$major}.{$minor}";
    }
}

With asymmetric visibility the getter goes away, because the property is public to read and private to write:

# In PHP 8.4 versions
class PhpVersion
{
    public private(set) string $version = '8.4';

    public function increment(): void
    {
        [$major, $minor] = explode('.', $this->version);
        $minor++;
        $this->version = "{$major}.{$minor}";
    }
}
  • #[\Deprecated] Attribute : The new #[\Deprecated] attribute makes PHP’s existing deprecation mechanism available to user-defined functions, methods, and class constants.
#In earlier PHP versions
class PhpVersion
{
    /**
     * @deprecated 8.3 use PhpVersion::getVersion() instead
     */
    public function getPhpVersion(): string
    {
        return $this->getVersion();
    }

    public function getVersion(): string
    {
        return '8.3';
    }
}

$phpVersion = new PhpVersion();
// No indication that the method is deprecated.
echo $phpVersion->getPhpVersion();

PHP 8.4 moves that notice into an attribute, so the engine itself raises the deprecation when the method is called:

#In PHP 8.4 versions
class PhpVersion
{
    #[\Deprecated(
        message: "use PhpVersion::getVersion() instead",
        since: "8.4",
    )]
    public function getPhpVersion(): string
    {
        return $this->getVersion();
    }

    public function getVersion(): string
    {
        return '8.4';
    }
}

$phpVersion = new PhpVersion();
// Deprecated: Method PhpVersion::getPhpVersion() is deprecated since 8.4, use PhpVersion::getVersion() instead
echo $phpVersion->getPhpVersion();
  • New ext-dom features and HTML5 support: The new DOM API provides a number of features to make dealing with documents easier, fixes a number of long-standing compliance flaws in the behavior of the DOM capability, and supports processing HTML5 documents in a standards-compliant manner.
# In earlier PHP versions
$dom = new DOMDocument();
$dom->loadHTML(
    <<<'HTML'
        <main>
            <article>PHP 8.4 is a feature-rich release!</article>
            <article class="featured">PHP 8.4 adds new DOM classes that are spec-compliant, keeping the old ones for compatibility.</article>
        </main>
        HTML,
    LIBXML_NOERROR,
);

$xpath = new DOMXPath($dom);
$node = $xpath->query(".//main/article[not(following-sibling::*)]")[0];
$classes = explode(" ", $node->className); // Simplified
var_dump(in_array("featured", $classes)); // bool(true)

# In PHP 8.4
$dom = Dom\HTMLDocument::createFromString(
    <<<'HTML'
        <main>
            <article>PHP 8.4 is a feature-rich release!</article>
            <article class="featured">PHP 8.4 adds new DOM classes that are spec-compliant, keeping the old ones for compatibility.</article>
        </main>
        HTML,
    LIBXML_NOERROR,
);

$node = $dom->querySelector('main > article:last-child');
var_dump($node->classList->contains("featured")); // bool(true)
  • Object API for BCMath: When working with arbitrary precision numbers, the new BcMath\Number object allows for standard mathematical operators and object-oriented usage. These objects can be used in string contexts like echo $num since they implement the Stringable interface and are immutable.
#In earlier PHP versions
$num1 = '0.12345';
$num2 = 2;
$result = bcadd($num1, $num2, 5);

echo $result; // '2.12345'
var_dump(bccomp($num1, $num2) > 0); // false

#In PHP Version 8.4
use BcMath\Number;

$num1 = new Number('0.12345');
$num2 = new Number('2');
$result = $num1 + $num2;

echo $result; // '2.12345'
var_dump($num1 > $num2); // false
#In earlier PHP versions
$animal = null;
foreach (['dog', 'cat', 'cow', 'duck', 'goose'] as $value) {
    if (str_starts_with($value, 'c')) {
        $animal = $value;
        break;
    }
}

var_dump($animal); // string(3) "cat"

#In PHP 8.4
$animal = array_find(
    ['dog', 'cat', 'cow', 'duck', 'goose'],
    static fn (string $value): bool => str_starts_with($value, 'c'),
);

var_dump($animal); // string(3) "cat"
  • new MyClass()->method() without parentheses: Properties and methods of a newly instantiated object can now be accessed without wrapping the new expression in parentheses.
#In earlier PHP versions
class PhpVersion
{
    public function getVersion(): string
    {
        return 'PHP 8.3';
    }
}

var_dump((new PhpVersion())->getVersion());

#In PHP 8.4
class PhpVersion
{
    public function getVersion(): string
    {
        return 'PHP 8.4';
    }
}

var_dump(new PhpVersion()->getVersion());

Those examples run on any 8.4 build. The rest of this guide gets one onto CentOS Stream.

How To Install PHP 8.4 on CentOS Stream 10 / CentOS Stream 9

The same commands work on both releases. Where the repository packages differ between Stream 10 and Stream 9, rpm -E %rhel fills in the major version for you, so you do not have to edit any URL.

Step 1: Update CentOS Stream system.

Confirm your system version first, because the repository packages in Step 2 must match the major release.

cat /etc/os-release

On a CentOS Stream 10 box the release fields read like this (Stream 9 prints VERSION_ID="9" and PLATFORM_ID="platform:el9"):

NAME="CentOS Stream"
VERSION="10 (Coughlan)"
RELEASE_TYPE=stable
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="10"
PLATFORM_ID="platform:el10"
PRETTY_NAME="CentOS Stream 10 (Coughlan)"
ANSI_COLOR="0;31"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:centos:centos:10"
HOME_URL="https://centos.org/"
VENDOR_NAME="CentOS"
VENDOR_URL="https://centos.org/"
BUG_REPORT_URL="https://issues.redhat.com/"
REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux 10"
REDHAT_SUPPORT_PRODUCT_VERSION="CentOS Stream"

Update your system packages and reboot so the box runs the newest kernel and libraries before you add third-party repositories:

sudo dnf update -y && sudo reboot

After the reboot, log back in and carry on with the repositories.

Step 2: Add EPEL & REMI repository

PHP 8.4 is not the default php on either CentOS Stream release, so we will pull it from Remi’s repository. Remi’s release package depends on EPEL, and both have to match your major release. Older copies of this guide installed the EL9 EPEL package and remi-release-8.rpm, which fails on Stream 10 with nothing provides epel-release = 8 needed by remi-release-8.10-2.el8.remi.noarch. Enable EPEL and the CodeReady Builder (CRB) repository first.

sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-$(rpm -E %rhel).noarch.rpm
sudo crb enable

The crb helper comes with epel-release and prints CRB repo is enabled and named: crb. Note that it takes enable, not install. Now add Remi.

sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-$(rpm -E %rhel).rpm

Once you have enabled EPEL and REMI repositories, install the yum-utils to manage yum repositories.

sudo dnf -y install yum-utils

List the enabled repositories to confirm all four new ones are in place:

dnf repolist

On Stream 10 you should see crb, epel, remi-modular and remi-safe next to the stock repositories:

repo id          repo name
appstream        CentOS Stream 10 - AppStream
baseos           CentOS Stream 10 - BaseOS
crb              CentOS Stream 10 - CRB
epel             Extra Packages for Enterprise Linux 10 - x86_64
extras-common    CentOS Stream 10 - Extras packages
remi-modular     Remi's Modular repository for Enterprise Linux 10 - x86_64
remi-safe        Safe Remi's RPM repository for Enterprise Linux 10 - x86_64

Enable the PHP 8.4 stream from Remi. The reset clears any stream that is already enabled, such as the AppStream php:8.3 stream on Stream 9, which would otherwise block the switch.

sudo dnf module reset -y php
sudo dnf module enable -y php:remi-8.4

Nothing is installed yet. Enabling the stream only tells dnf which PHP branch to pick from Remi.

Step 3: Install PHP 8.4 on CentOS Stream 10 / CentOS Stream 9

With the stream enabled, install the module’s default profile. It pulls in the CLI, PHP-FPM, mbstring and XML packages in one transaction.

sudo dnf module install -y php:remi-8.4

Every PHP package in the transaction comes from remi-modular with a module_php.8.4 release tag:

Installing group/module packages:
 php-cli          x86_64 8.4.27-1.module_php.8.4.el10.remi   remi-modular 2.3 M
 php-common       x86_64 8.4.27-1.module_php.8.4.el10.remi   remi-modular 768 k
 php-fpm          x86_64 8.4.27-1.module_php.8.4.el10.remi   remi-modular 2.0 M
 php-mbstring     x86_64 8.4.27-1.module_php.8.4.el10.remi   remi-modular 531 k
 php-xml          x86_64 8.4.27-1.module_php.8.4.el10.remi   remi-modular 699 k
Installing dependencies:
 oniguruma5php    x86_64 6.9.10-1.el10.remi                  remi-safe    217 k
....
Transaction Summary
Install  10 Packages
Total download size: 6.8 M
Installed size: 30 M

During the first install from Remi, dnf imports Remi’s GPG key (Importing GPG key 0x2A0948E4 on Stream 10). Without -y you will be asked to confirm it, so type y and press Enter.

Run a full update once more so nothing on the box lags behind the new repositories.

sudo dnf update -y

Confirm the version of PHP installed in your system.

php -v

Both Stream 10 and Stream 9 printed the same Remi build:

PHP 8.4.27 (cli) (built: Oct 19 2026 14:38:59) (NTS gcc x86_64)
Copyright (c) The PHP Group
Built by Remi's RPM repository <https://rpms.remirepo.net/> #StandWithUkraine #NoAI
Zend Engine v4.4.27, Copyright (c) Zend Technologies

The version line is what the PHP2Shell check in the next section compares against the fixed releases.

Check PHP 8.4 for PHP2Shell (CVE-2026-109618, CVE-2026-109619)

PHP2Shell is the name for CVE-2026-109618 and CVE-2026-109619, two remote code execution bugs in PHP itself that PHP’s 20 October pre-announcement rated CVSS v4.0 9.2. The PHP security advisory of 21 October fixed them with one release per supported branch, and it rates Apache with mod_php as the highest risk. These are the first fixed versions:

  • PHP 8.5.12 for the 8.5 branch
  • PHP 8.4.27 for the 8.4 branch
  • PHP 8.3.36 for the 8.3 branch
  • PHP 8.2.35 for the 8.2 branch

The same advisory urged anyone relying on rpms.remirepo.net to switch to distribution packages or build from source, because “The repo will not have updates today.” Remi’s fixed builds arrived a day later, on 22 October, and php:remi-8.4 now ships 8.4.27, so a fresh install from Step 3 is patched and can carry on to Step 4 once the check below passes. If you would rather not depend on a volunteer-run repository for security fixes, the distro packages that carry the fix are php 8.3.36 from the Stream 10 AppStream and the default php 8.0.30-9.el9 on Stream 9. Stream 10 also has separate php8.4 packages, but the newest php8.4-common our box could see was 8.4.24-2.el10. That version is below 8.4.27 and its changelog showed no CVE entry, so it cannot be confirmed as fixed.

Remi’s changelog only says “Update to 8.4.27” and does not name the CVEs, so the package version is the check.

rpm -q php-common

Version 8.4.27 or later is fixed. Our Stream 10 box returned the el10 build below, and Stream 9 returned the same version with an el9 tag:

php-common-8.4.27-1.module_php.8.4.el10.remi.x86_64

On an existing server that still reports 8.4.26 or older, upgrade the PHP packages and restart PHP-FPM. The package restarted PHP-FPM on its own in our test, but restart anyway and confirm the start time, along with any long-running PHP CLI workers such as queue consumers. Nginx and Apache in Step 5 both hand PHP to PHP-FPM, so php-fpm is the service that has to come back on the new code. If you installed the php package (the Apache module) and switched Apache to the prefork MPM, httpd runs PHP itself, so restart httpd as well.

sudo dnf upgrade -y "php*"
sudo systemctl restart php-fpm
systemctl show -p ActiveEnterTimestamp php-fpm
sudo dnf history info last

The PHP-FPM start time has to be the same second as the transaction’s End time or later. We rolled our Stream 10 box back to 8.4.26 to test this, and the upgrade to 8.4.27 ended in the same second PHP-FPM came back up (output trimmed):

ActiveEnterTimestamp=Thu 2026-10-22 16:13:24 UTC
Transaction ID : 8
Begin time     : Thu Oct 22 16:13:23 2026
Begin rpmdb    : 8f2a529f7fd300066ff13712be90cb06d1c825c2ece15bac0c2aa5662421f7d4
End time       : Thu Oct 22 16:13:24 2026 (1 seconds)
Command Line   : upgrade -y php*

CentOS Stream 9 has a trap. Its default php (8.0.30-9.el9) is fixed and names both CVEs in its changelog, but the AppStream module streams php:8.1, php:8.2 and php:8.3 are stale at 8.1.27, 8.2.28 and 8.3.19 and have no fixed build at all. On a box using one of those streams, dnf upgrade "php*" prints “Nothing to do.” while PHP stays vulnerable. Stream 9 itself has a limited life too: the CentOS project expects it to end with RHEL 9 full support, estimated for 2027. If rpm -q php-common shows a module_el9 release, move to Remi’s stream for the same branch with the repositories from Step 2.

sudo dnf module reset -y php
sudo dnf module enable -y php:remi-8.3
sudo dnf distro-sync -y "php*"
sudo systemctl restart php-fpm

That took our Stream 9 box from 8.3.19 to php-common-8.3.36-1.module_php.8.3.el9.remi, and the page served through Nginx reported 8.3.36. Pick php:remi-8.3 or php:remi-8.4. Remi’s 8.2 and 8.1 streams are fixed as well, but PHP 8.2 gets security fixes only until 31 December 2026 and PHP 8.1 reached end of life on 31 December 2025, so 8.1 now lives on Remi’s own backports. Our PHP 8.3 guide for EL9 covers that branch in more detail.

To stay on distribution packages instead, run the reset and the distro-sync without the enable line. That took the same box to 8.0.30-9.el9, the fixed default stream, but only if your code still runs on PHP 8.0, and Stream 9 AppStream has no php-imap package for it, so an app that needs IMAP has to stay on Remi. If distro-sync stops with problem with installed package, an extension you added has no build in the target stream (php-imap was the one on our box): remove it, run the switch, then install it again if the new stream has it.

If you cannot switch today, PHP’s advisory lists measures that reduce exposure but, in its words, “do not make an installation safe”. For CVE-2026-109618 it recommends memory_limit=-1, set where applications cannot override it such as php_admin_value in the FPM pool, and a low post_max_size: 8M without file uploads or 32M with them, keeping upload_max_filesize below it. For CVE-2026-109619 it recommends session.upload_progress.enabled=0 in php.ini or the pool (not ini_set() or .user.ini), then restarting every worker. An unlimited memory limit lets one request take all RAM, so watch memory and remove these lines once you are patched. For a site without uploads, append them to the pool and restart PHP-FPM.

echo 'php_admin_value[memory_limit] = -1' | sudo tee -a /etc/php-fpm.d/www.conf
echo 'php_admin_flag[session.upload_progress.enabled] = off' | sudo tee -a /etc/php-fpm.d/www.conf
echo 'php_admin_value[post_max_size] = 8M' | sudo tee -a /etc/php-fpm.d/www.conf
echo 'php_admin_value[upload_max_filesize] = 4M' | sudo tee -a /etc/php-fpm.d/www.conf
sudo systemctl restart php-fpm

For a site that takes uploads, use the advisory’s 32M ceiling instead of the two 8M and 4M lines:

echo 'php_admin_value[post_max_size] = 32M' | sudo tee -a /etc/php-fpm.d/www.conf
echo 'php_admin_value[upload_max_filesize] = 16M' | sudo tee -a /etc/php-fpm.d/www.conf
sudo systemctl restart php-fpm

The advisory also says to check that no other pool, directory or application setting puts a finite memory_limit back, so look through every file in /etc/php-fpm.d/ (one per pool) and your app’s own config. Then check the values through the web server, not the CLI, because pool settings only apply to FPM requests. The probe below uses the document root and domain of the Step 5 Nginx example, so replace /var/www/mywebsiteapp.com and mywebsiteapp.com with your own. On an Apache-only server, write it to /var/www/html/probe.php and request http://localhost/probe.php without the Host header.

echo '<?php foreach (["memory_limit", "session.upload_progress.enabled", "post_max_size", "upload_max_filesize"] as $k) echo $k, "=", var_export(ini_get($k), true), "\n"; echo PHP_VERSION, " ", PHP_SAPI, "\n";' | sudo tee /var/www/mywebsiteapp.com/probe.php
curl -s -H "Host: mywebsiteapp.com" http://127.0.0.1/probe.php

On the stale 8.3.19 stream all four settings came back as set through Nginx and PHP-FPM:

memory_limit='-1'
session.upload_progress.enabled='0'
post_max_size='8M'
upload_max_filesize='4M'
8.3.19 fpm-fcgi

Delete the probe right away, because it tells anyone who finds it your exact PHP build.

sudo rm -f /var/www/mywebsiteapp.com/probe.php

As an extra measure of our own, not one from PHP’s advisory, you can refuse multipart/form-data requests at the web server. That breaks every file upload on the site, so use it only on apps that never take uploads. On Nginx, put this inside server { } in the Step 5 site file, then run sudo nginx -t and sudo systemctl reload nginx. The rule has to sit in every server block that serves the app, including the port 443 block certbot adds.

    if ($http_content_type ~* "multipart/form-data") {
        return 403;
    }

A multipart POST to our Stream 9 box then got 403, while form-encoded POSTs and plain GETs still got 200. On Apache, create a separate file for the same rule. It applies to the main server only, because rewrite configurations are not inherited by virtual hosts, so copy the three lines into every <VirtualHost> that serves the app (including the :443 one), or add RewriteOptions InheritDown after RewriteEngine On.

sudo vim /etc/httpd/conf.d/php2shell-block.conf

Add these three lines, then run sudo apachectl configtest and sudo systemctl reload httpd:

RewriteEngine On
RewriteCond %{HTTP:Content-Type} multipart/form-data [NC]
RewriteRule ^ - [F]

On Stream 10 with httpd, that gave the same result: 403 for multipart, 200 for form-encoded POSTs and GETs. CentOS Stream 8 and CentOS Linux 8 are end of life, so they get no OS updates at all. Remi still publishes fixed EL8 builds of PHP, but the system underneath stays unpatched, so move those servers to a maintained EL10 rebuild; our PHP 8.4 guide for Rocky Linux 10 and AlmaLinux 10 uses the same Remi steps.

Step 4: Install PHP 8.4 extensions

We will now install the PHP 8.4 extensions most applications ask for: database access through mysqlnd, image handling with GD, internationalization, ZIP archives, BCMath and IMAP, plus PEAR and the CGI binary. To install the extensions, execute the command below.

sudo dnf install -y php-{common,pear,cgi,curl,mbstring,gd,mysqlnd,gettext,bcmath,json,xml,fpm,intl,zip,imap}

Some of those names do not exist as separate packages any more. curl, gettext and json are built into php-common and php-cli on 8.4, Remi builds ZIP support from the PECL zip extension as php-pecl-zip, and IMAP, which was removed from the PHP core in 8.4, as php-pecl-imap; dnf resolves the old names to those packages. List the loaded modules to confirm they are all there.

php -m | grep -iE "^(curl|gd|mysqlnd|gettext|bcmath|json|intl|zip|imap|mbstring)$"

All ten extensions load:

bcmath
curl
gd
gettext
imap
intl
json
mbstring
mysqlnd
zip

If one is missing, install its package by name and run the check again.

Step 5: Using PHP with Nginx / Apache web server

With PHP 8.4 successfully installed, I will briefly describe how to use PHP with Nginx / Apache web servers. These are the two most commonly used web servers, and on CentOS Stream both hand PHP requests to PHP-FPM. Pick one: they both listen on port 80, so they cannot run side by side with their default configuration.

PHP 8.4 Usage with Nginx web server

PHP uses PHP-FPM (FastCGI Process Manager) to run PHP code in its own pool of worker processes. Nginx acts as a reverse proxy here: it serves static files itself and forwards requests for .php files to PHP-FPM over a Unix socket.

To install Nginx and FPM PHP extensions on your system, run the command:

sudo dnf install vim nginx php-fpm -y

php-fpm is already installed from Step 3, so on Stream 10 dnf adds Nginx, vim and their dependencies:

Installed:
  centos-logos-httpd-100.5-1.el10.noarch  logrotate-3.22.0-5.el10.x86_64
  nginx-2:1.26.3-15.el10.x86_64           nginx-core-2:1.26.3-15.el10.x86_64
  vim-common-2:9.1.083-25.el10.x86_64     vim-enhanced-2:9.1.083-25.el10.x86_64
  vim-filesystem-2:9.1.083-25.el10.noarch xxd-2:9.1.083-25.el10.x86_64

Complete!

Start and enable nginx and php-fpm services by executing the command below.

sudo systemctl enable --now nginx php-fpm

systemd creates one symlink per service:

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'.

Check Nginx status:

systemctl status nginx php-fpm

Both units should report active (running), and PHP-FPM’s status line reads “Ready to handle connections”:

● nginx.service - The nginx HTTP and reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: disabled)
    Drop-In: /etc/systemd/system/nginx.service.d
             └─php-fpm.conf
     Active: active (running) since Thu 2026-10-22 15:41:30 UTC; 7s ago
   Main PID: 848 (nginx)
      Tasks: 3 (limit: 6425)
     Memory: 3.3M (peak: 3.5M)

Oct 22 15:41:30 labhost nginx[844]: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
Oct 22 15:41:30 labhost nginx[844]: nginx: configuration file /etc/nginx/nginx.conf test is successful
Oct 22 15:41:30 labhost systemd[1]: Started nginx.service - The nginx HTTP and reverse proxy server.

● php-fpm.service - The PHP FastCGI Process Manager
     Loaded: loaded (/usr/lib/systemd/system/php-fpm.service; enabled; preset: disabled)
     Active: active (running) since Thu 2026-10-22 15:41:30 UTC; 7s ago
   Main PID: 842 (php-fpm)
     Status: "Ready to handle connections"
      Tasks: 6 (limit: 6425)
     Memory: 10.5M (peak: 10.7M)

The stock PHP-FPM pool already listens on a Unix socket that Nginx is allowed to use, so it needs no edits. Earlier versions of this guide switched the pool user to nginx, but the PHP session directory /var/lib/php/session belongs to group apache with mode 770, so sessions break under any other user. Check the pool settings instead.

grep -E "^(user|group|listen|listen\.acl_users)\s*=" /etc/php-fpm.d/www.conf

The pool runs as apache, and listen.acl_users gives the nginx user access to the socket:

user = apache
group = apache
listen = /run/php-fpm/www.sock
listen.acl_users = apache,nginx

Now create a server block for your site in its own file under /etc/nginx/conf.d/, which the main nginx.conf includes.

sudo vim /etc/nginx/conf.d/mywebsiteapp.com.conf

Add the following, replacing mywebsiteapp.com with your domain. The SCRIPT_FILENAME line matters: the stock /etc/nginx/fastcgi_params does not set it, and without it the Nginx error log shows FastCGI sent in stderr: "Primary script unknown" and Nginx returns 404 “File not found.” for every PHP page:

server {
    listen       80;
    server_name  mywebsiteapp.com;
    root         /var/www/mywebsiteapp.com;
    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;
    }
}

Create the document root with a small test page, then validate the configuration and reload Nginx.

sudo mkdir -p /var/www/mywebsiteapp.com
echo '<?php echo PHP_VERSION, " ", PHP_SAPI, "\n";' | sudo tee /var/www/mywebsiteapp.com/index.php
sudo nginx -t
sudo systemctl reload nginx

The syntax test has to pass before the reload:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Request the page with your domain in the Host header, so you can test before DNS points at the server.

curl -s -H "Host: mywebsiteapp.com" http://127.0.0.1/index.php

fpm-fcgi confirms that PHP-FPM ran the script, not the CLI:

8.4.27 fpm-fcgi

A 404 with “File not found.” at this point means the SCRIPT_FILENAME line or the root path is wrong.

To reach the site from other machines, open port 80 in firewalld. Minimal CentOS Stream installs and cloud images may not have firewalld at all (firewall-cmd: command not found), so install it first in that case. Before you start a freshly installed firewalld, check your SSH port: on our Stream 10 box the default public zone allowed only ssh (port 22), cockpit and dhcpv6-client, and firewalld applies that zone the moment it starts. If SSH listens on another port, say 2222, run sudo firewall-offline-cmd --add-port=2222/tcp after the install and before the start, or you lock yourself out. Cloud servers also filter traffic at the provider’s security group, so open port 80 there too.

sudo dnf install -y firewalld
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --reload

Both firewall-cmd calls print success, and sudo firewall-cmd --zone=public --list-services then includes http.

PHP 8.4 Usage with Apache web server

On CentOS Stream, Apache does not run PHP inside its own processes. The php-fpm package drops /etc/httpd/conf.d/php.conf, which passes .php requests to the same PHP-FPM socket through mod_proxy_fcgi, and this guide never installs the php package (the Apache module). To install the Apache httpd, run the command below.

sudo dnf install -y httpd

Stop and disable Nginx if you set it up above, because both web servers bind port 80 and httpd will not start while Nginx holds it.

sudo systemctl disable --now nginx

systemd confirms Nginx no longer starts at boot:

Removed '/etc/systemd/system/multi-user.target.wants/nginx.service'.

Start and enable the httpd service by the following command.

sudo systemctl enable --now httpd

The output shows the boot symlink for httpd:

Created symlink '/etc/systemd/system/multi-user.target.wants/httpd.service' → '/usr/lib/systemd/system/httpd.service'.

The Apache configuration lives in /etc/httpd/conf and /etc/httpd/conf.d. The website data resides in /var/www/html but can be changed if you wished to.

The primary Apache configuration file is /etc/httpd/conf/httpd.conf. Its defaults already listen on port 80 and serve /var/www/html, which is all this test needs, so check the two lines rather than edit them.

grep -nE "^(DocumentRoot|Listen)" /etc/httpd/conf/httpd.conf

Change DocumentRoot here only if your site lives somewhere else:

47:Listen 80
124:DocumentRoot "/var/www/html"

Create a file in /var/www/html called info.php for test purposes.

sudo vim /var/www/html/info.php

Add the following code.

<?php
phpinfo();
?>

Save and exit from the editor.

Open port 80 through the firewall if you did not already do it in the Nginx section. Install firewalld first, as shown there, if firewall-cmd is missing.

sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --reload

Manage the Apache service by running the following commands:

sudo systemctl start httpd
sudo systemctl restart php-fpm
sudo systemctl restart httpd

Check the status of the httpd service:

systemctl status httpd.service

The php-fpm.conf drop-in makes httpd pull in PHP-FPM (Wants=php-fpm.service), and the status line confirms Apache is listening on port 80:

● httpd.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; preset: disabled)
    Drop-In: /etc/systemd/system/httpd.service.d
             └─php-fpm.conf
     Active: active (running) since Thu 2026-10-22 15:42:30 UTC; 229ms ago
       Docs: man:httpd.service(8)
   Main PID: 1346 (httpd)
     Status: "Started, listening on: port 80"
      Tasks: 177 (limit: 6425)
     Memory: 14.1M (peak: 14.5M)

To test your server, open http://localhost/info.php on the server itself or http://10.0.1.50/info.php from another machine, using your server’s IP address. If your server is properly set, the PHP information page loads.

PHP 8.4 phpinfo page served by Apache on CentOS Stream
phpinfo page captured in December 2024 on PHP 8.4.2, an older build without the PHP2Shell fix; a patched server reports 8.4.27 or later.

Alternatively, query the page from your terminal and pull out the version and server API.

curl -s http://localhost/info.php | grep -oE "PHP Version [0-9.]+|FPM/FastCGI" | sort -u

Apache passed the request to PHP-FPM, which reports the patched build:

FPM/FastCGI
PHP Version 8.4.27

You may wish to analyze the logs to know what is happening to your site.

sudo grep info.php /var/log/httpd/access_log

Each request shows up with its status code and size:

127.0.0.1 - - [22/Oct/2026:15:42:30 +0000] "GET /info.php HTTP/1.1" 200 76181 "-" "curl/8.12.1"

Delete info.php once you are done testing. A forgotten phpinfo page shows anyone the exact PHP build and module list, which is the first thing scanners look for after a disclosure like PHP2Shell.

sudo rm -f /var/www/html/info.php

Keep a copy of the probe or index page only on sites that need it, and remove it from production once the checks pass.

Wrap up

PHP 8.4 is now running on CentOS Stream 10 or CentOS Stream 9 from Remi, checked against PHP2Shell and served through Nginx or Apache with PHP-FPM. Rerun rpm -q php-common after every update, because the package version is the only reliable sign that a security release landed. If your servers run on SELinux enforcing, leave OPcache JIT off: with JIT on, PHP-FPM needs setsebool -P httpd_execmem 1, which loosens SELinux for every web server process. Our SELinux troubleshooting guide helps when a denial breaks PHP-FPM.

See more guides:

Keep reading

Patch PHP2Shell on RHEL, Rocky Linux and AlmaLinux 10 / 9 / 8 AlmaLinux Patch PHP2Shell on RHEL, Rocky Linux and AlmaLinux 10 / 9 / 8 Install GitHub CLI (gh) on Linux, macOS, and Windows Programming Install GitHub CLI (gh) on Linux, macOS, and Windows Customize KDE Plasma Desktop with Themes|Modules|Extensions AlmaLinux Customize KDE Plasma Desktop with Themes|Modules|Extensions Best RHCSA Books for 2026: RHEL 10 (EX200) and RHCE Study Guides RHEl Best RHCSA Books for 2026: RHEL 10 (EX200) and RHCE Study Guides Best Monitor Arms for a Multi-Monitor Developer Desk Programming Best Monitor Arms for a Multi-Monitor Developer Desk Install AWX (Ansible Tower) on RHEL 10 / Rocky Linux 10 / AlmaLinux 10 Automation Install AWX (Ansible Tower) on RHEL 10 / Rocky Linux 10 / AlmaLinux 10

Leave a Comment

Press ESC to close