The Problem with a Single PHP Version#
Hosting a mix of WordPress blogs, legacy Joomla sites, and modern Laravel apps on one server creates a compatibility nightmare. Forcing PHP 8.3 on everyone breaks older applications like Magento 1.9. Locking everyone to PHP 7.4 for safety means modern sites miss critical performance gains and security patches.
The professional solution is per-site PHP version isolation. This isn't just about compatibility—it's fundamental to resource control and security. Assigning each site its own PHP-FPM pool lets you:
- Enforce memory and process limits per site, preventing one runaway script from crippling the entire server.
- Select the optimal PHP version for each application's requirements.
- Isolate security contexts, making it straightforward to trace resource abuse to a specific account.
This guide walks through configuring Nginx with multiple PHP-FPM pools, each running a different PHP version. We'll use Ubuntu 22.04 LTS as our base, but the principles apply to any modern Linux distribution.
Step 1: Install Multiple PHP Versions#
First, add the Ondřej Surý PPA, which maintains current, secure PHP packages for Ubuntu.
sudo add-apt-repository ppa:ondrej/php
sudo apt updateInstall the PHP versions you need. For this example, we'll install PHP 7.4, 8.1, and 8.3, along with the FPM module and common extensions.
sudo apt install php7.4-fpm php8.1-fpm php8.3-fpm
sudo apt install php7.4-mysql php8.1-mysql php8.3-mysql php7.4-curl php8.1-curl php8.3-curl php7.4-gd php8.1-gd php8.3-gd php7.4-mbstring php8.1-mbstring php8.3-mbstring php7.4-xml php8.1-xml php8.3-xml php7.4-zip php8.1-zip php8.3-zipEach version's FPM service starts automatically. Verify they're running:
systemctl status php7.4-fpm
systemctl status php8.1-fpm
systemctl status php8.3-fpmStep 2: Create Dedicated FPM Pools#
Each PHP version installs a default www.conf pool in /etc/php/VERSION/fpm/pool.d/. We need custom pools for each site or account. Let's create a pool for client-a.com running PHP 8.1.
sudo nano /etc/php/8.1/fpm/pool.d/client-a.confPaste this configuration. The key is the listen directive—each pool needs a unique socket file.
[client-a]
user = client-a
group = client-a
listen = /run/php/php8.1-fpm-client-a.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 5
pm.max_requests = 500
php_admin_value[error_log] = /var/log/php/client-a-error.log
php_admin_flag[log_errors] = onRepeat for other sites. For client-b.com on PHP 7.4, create /etc/php/7.4/fpm/pool.d/client-b.conf with socket path /run/php/php7.4-fpm-client-b.sock.
Critical: Create the corresponding system user for each site (sudo useradd -r -s /bin/false client-a). This is the foundation of process isolation. Tools like Salieno Core automate this user and pool provisioning when an account is created, but the manual method gives you full control.
Restart the relevant FPM services to load new pools:
sudo systemctl restart php8.1-fpm
sudo systemctl restart php7.4-fpmStep 3: Configure Nginx to Use the Correct Socket#
Tell Nginx which PHP-FPM socket to use for each site. In your Nginx site configuration (e.g., /etc/nginx/sites-available/client-a.com), point the fastcgi_pass directive to the specific socket.
server {
listen 80;
server_name client-a.com www.client-a.com;
root /var/www/client-a.com/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm-client-a.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}For client-b.com, change the fastcgi_pass line to:
fastcgi_pass unix:/run/php/php7.4-fpm-client-b.sock;Test your Nginx configuration and reload:
sudo nginx -t
sudo systemctl reload nginxStep 4: Verify and Test#
Create a simple info.php file in each site's document root:
<?php phpinfo();Visit http://client-a.com/info.php and http://client-b.com/info.php. You should see the correct PHP version reported for each. Check the "Loaded Configuration File" and "Server API" lines to confirm FPM is active.
For a practical test, create a file that uses a function deprecated or removed in a newer PHP version. For example, each() was removed in PHP 8.0. If your PHP 8.1 site throws a fatal error while your PHP 7.4 site works, isolation is confirmed.
Step 5: Manage and Monitor Your Pools#
With multiple FPM processes running, monitoring becomes crucial. Key commands:
- Check pool status:
sudo php-fpm8.1 -tt(for PHP 8.1) shows detailed configuration parsing, including all pools. - View active connections: Use
sudo ss -xp | grep phpto see which sockets have active connections. - Monitor processes:
ps aux | grep php-fpmshows all FPM master and worker processes. The pool name is in the process list. - Read logs: Check
/var/log/php/client-a-error.logfor application errors and/var/log/php8.1-fpm.logfor FPM service logs.
To change a site's PHP version: 1) Create a new pool config for the target PHP version, 2) Update the Nginx fastcgi_pass directive, and 3) Reload Nginx. Disable the old pool by renaming its .conf file and restarting the old FPM service.
Conclusion: Isolation is Non-Negotiable#
Per-site PHP-FPM pools are foundational for any serious shared hosting environment. They move you from a "one-size-fits-all" model to a professional, multi-tenant architecture where performance and compatibility are managed per customer. While the manual setup is straightforward, it becomes tedious at scale—this is where integrated platforms like Salieno Core automate user, pool, and Nginx configuration provisioning into a single click. Whether you automate it or not, the principle remains: isolate your PHP environments to protect your server and your customers.
0 comments
Loading comments…