Multiple PHP versions on AlmaLinux and Rocky Linux, one FPM pool per site
Run PHP 8.4 and 8.2 side by side with Apache and Remi's packages: each site gets its own PHP version, its own Linux user and its own FPM pool.
A hosting server rarely gets to pick one PHP version. A client’s old shop needs 8.2, while new WordPress sites should run 8.4. The distribution ships one PHP at a time, so this tutorial uses Remi’s repository, which installs each version under its own prefix so they never collide.
You will end up with two sites, each running as its own Linux user in its own PHP-FPM pool:
| Site | PHP | Runs as |
|---|---|---|
site1.example.com | 8.4 | site1 |
site2.example.com | 8.2 | site2 |
A pool per site is what keeps one site’s code from reading another site’s files. It also means one site that runs away with its PHP processes does not starve the others.
Add the EPEL and Remi repositories
Remi’s packages depend on EPEL, and EPEL depends on CodeReady Builder (crb), which is
off by default.
dnf -y install dnf-plugins-corednf config-manager --set-enabled crbdnf -y install epel-releasednf -y install "https://rpms.remirepo.net/enterprise/remi-release-$(rpm -E %rhel).rpm"$(rpm -E %rhel) expands to your major version (9 or 10), so the same line works on both.
Install Apache and both PHP versions
Remi names each version’s packages phpXY-php-*. Install FPM and a few common
extensions for each version you want:
dnf -y install httpddnf -y install php84-php-fpm php84-php-opcache php84-php-mysqlnd php84-php-processdnf -y install php82-php-fpm php82-php-opcache php82-php-mysqlnd php82-php-processEach version lives in its own tree. Its configuration is under /etc/opt/remi/php84/,
its sockets under /var/opt/remi/php84/run/, and it runs as its own service,
php84-php-fpm. Nothing here replaces the system php, and you can add php83 or
php85 the same way later.
Create a user and a document root per site
Each site gets its own Linux user. Its files belong to that user and to the apache
group, so Apache can serve static files, and nobody else can read them:
for site in site1 site2; do useradd --system --shell /sbin/nologin "$site" mkdir -p "/var/www/$site/public" chown -R "$site:apache" "/var/www/$site" chmod 750 "/var/www/$site"doneDirectories under /var/www already carry the SELinux label Apache is allowed to read,
so nothing needs relabelling.
Put a test page in each site that reports which PHP version runs it, and as which user:
for site in site1 site2; do cat > "/var/www/$site/public/index.php" <<'EOF'<?phpecho PHP_VERSION, ' ', posix_getpwuid(posix_geteuid())['name'], "\n";EOF chown "$site:apache" "/var/www/$site/public/index.php"doneGive each site its own FPM pool
A pool is a group of PHP processes with its own user, its own socket and its own limits.
Create one for site1 under PHP 8.4:
cat > /etc/opt/remi/php84/php-fpm.d/site1.conf <<'EOF'[site1]user = site1group = site1listen = /var/opt/remi/php84/run/php-fpm/site1.socklisten.owner = apachelisten.group = apachelisten.mode = 0660
pm = ondemandpm.max_children = 5pm.process_idle_timeout = 10s
php_admin_value[open_basedir] = /var/www/site1:/tmpEOFand one for site2 under PHP 8.2:
cat > /etc/opt/remi/php82/php-fpm.d/site2.conf <<'EOF'[site2]user = site2group = site2listen = /var/opt/remi/php82/run/php-fpm/site2.socklisten.owner = apachelisten.group = apachelisten.mode = 0660
pm = ondemandpm.max_children = 5pm.process_idle_timeout = 10s
php_admin_value[open_basedir] = /var/www/site2:/tmpEOFWhat the settings do:
user/group— the PHP code of this site runs as this user, and only this user. File permissions do the isolating from here.listen.*— only Apache may connect to the socket.pm = ondemand— no processes at all until a request arrives, and idle ones exit after 10 seconds. On a server with many small sites this is the setting that saves the most memory.pm.max_childrencaps how many requests run at once, per site.open_basedir— a second fence inside PHP itself: even a file the user could read is off limits outside these paths.
Each version also ships a default www pool. It is not used here, so disable it, then
start both services:
mv /etc/opt/remi/php84/php-fpm.d/www.conf /etc/opt/remi/php84/php-fpm.d/www.conf.disabledmv /etc/opt/remi/php82/php-fpm.d/www.conf /etc/opt/remi/php82/php-fpm.d/www.conf.disabledsystemctl enable --now php84-php-fpm php82-php-fpmPoint each Apache virtual host at its pool
The SetHandler line is where a site picks its PHP version: it hands .php files to
that site’s socket.
cat > /etc/httpd/conf.d/site1.conf <<'EOF'<VirtualHost *:80> ServerName site1.example.com DocumentRoot /var/www/site1/public <Directory /var/www/site1/public> Require all granted AllowOverride All </Directory> <FilesMatch "\.php$"> SetHandler "proxy:unix:/var/opt/remi/php84/run/php-fpm/site1.sock|fcgi://localhost" </FilesMatch> DirectoryIndex index.php index.html</VirtualHost>EOF
cat > /etc/httpd/conf.d/site2.conf <<'EOF'<VirtualHost *:80> ServerName site2.example.com DocumentRoot /var/www/site2/public <Directory /var/www/site2/public> Require all granted AllowOverride All </Directory> <FilesMatch "\.php$"> SetHandler "proxy:unix:/var/opt/remi/php82/run/php-fpm/site2.sock|fcgi://localhost" </FilesMatch> DirectoryIndex index.php index.html</VirtualHost>EOF
apachectl configtestsystemctl enable --now httpdTo move a site to another PHP version later, change the version in its SetHandler
path, move its pool file into that version’s php-fpm.d, and reload both services.
Check that each site runs its own PHP, as its own user
Until DNS points the names at this server, ask Apache directly with a Host header:
curl -fsS -H 'Host: site1.example.com' http://127.0.0.1/ | grep -E '^8\.4\.[0-9]+ site1$'curl -fsS -H 'Host: site2.example.com' http://127.0.0.1/ | grep -E '^8\.2\.[0-9]+ site2$'Each line prints the version and the user, for example 8.4.26 site1.
Now check the isolation. Have site2 try to read site1’s code:
cat > /var/www/site2/public/peek.php <<'EOF'<?phpecho @file_get_contents('/var/www/site1/public/index.php') === false ? "blocked\n" : "LEAKED\n";EOFchown site2:apache /var/www/site2/public/peek.phpcurl -fsS -H 'Host: site2.example.com' http://127.0.0.1/peek.php | grep -x blockedrm /var/www/site2/public/peek.phpblocked is the answer you want. Two separate fences stop it: open_basedir refuses the
path, and even without it, site2 is neither the owner of /var/www/site1 nor in its
group.
The CorePanel way
CorePanel builds this structure for every hosting account and keeps it up to date.
Each account is its own Linux user with its own on-demand FPM pool, and PHP 8.4, 8.3, 8.2
and 7.4 come installed side by side. The version is chosen per account when you create
it, and changed later under Edit account → PHP version: CorePanel rewrites the pool
on the new version and re-points every site of the account in one reload. A single site
can also run a version of its own, from the PHP column of the account’s domains, and
your customers can pick it from their own panel. open_basedir,
process limits and a private temporary directory per account are set the same way.
See How PHP runs and Isolation and limits.