in Parallel on Linux
Anyone hosting several client projects on one server eventually runs into different PHP version requirements, since Magento, WordPress, and legacy applications rarely demand the same version. Versioned FPM packages, update-alternatives, and cleanly separated pools per vhost let you run multiple PHP versions side by side without conflicts and safely in production.
Table of Contents
- 1. Why a server needs multiple PHP versions
- 2. Installing versioned packages: php8.1-fpm alongside php8.3-fpm
- 3. update-alternatives: how the PHP CLI default is managed
- 4. PHP-FPM pools: a dedicated process per version
- 5. Binding an Nginx vhost per project to the right pool
- 6. Safely switching the CLI version per project
- 7. Maintaining extensions and php.ini per version
- 8. Magento projects: mapping version requirements concretely
- 9. Switching methods in direct comparison
- 10. Summary
- 11. FAQ
1. Why a server needs multiple PHP versions
Agencies and freelancers hosting several client projects on a shared server almost inevitably run into the problem of differing PHP version requirements. A Magento 2.4.6 store requires PHP 8.1 or 8.2, a freshly set up Magento 2.4.8 requires PHP 8.3, and alongside it there might be an older WordPress project that throws errors on PHP 8.3 because of outdated plugins. Running a single global PHP interpreter in this everyday scenario means either constant compatibility problems or a risky downgrade that endangers other projects.
The solution is not virtual machines or containers for every single project, but the native ability of Debian and Ubuntu based distributions to maintain several PHP versions as independent, side by side installable packages. Each version gets its own FPM daemon, its own php.ini, and its own set of extensions, without the versions overwriting each other. This approach is more resource efficient than full containers per project and can be fully implemented with the standard tools of apt and systemd.
2. Installing versioned packages: php8.1-fpm alongside php8.3-fpm
The foundation for parallel PHP versions is the PPA maintained by Ondřej Surý, which provides versioned PHP packages for Ubuntu and Debian. Unlike the default repository, which offers only a single PHP version per distribution release, the PPA delivers php8.1, php8.2, php8.3, and further versions that can all be installed simultaneously, each with its own name prefix. The php8.1-fpm package installs a completely independent FPM daemon that runs alongside php8.3-fpm, each with its own systemd service and its own Unix socket.
It is important to install all required extensions for each version individually, since apt treats packages such as php8.1-mysql and php8.3-mysql as entirely separate packages. A common beginner mistake is installing extensions for only one version and only noticing missing modules for the second interpreter in production, when a Magento installation fails with Class GD not found or a similar error. A short comparison script running dpkg -l | grep php8.1 against dpkg -l | grep php8.3 reliably reveals missing packages.
# Add the Ondrej Sury PPA for versioned PHP packages (Ubuntu/Debian)
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update
# Install PHP 8.1 and PHP 8.3 side by side, each with its own FPM daemon
sudo apt install -y php8.1-fpm php8.1-cli php8.1-mysql php8.1-xml \
php8.1-mbstring php8.1-curl php8.1-zip php8.1-gd php8.1-intl php8.1-bcmath
sudo apt install -y php8.3-fpm php8.3-cli php8.3-mysql php8.3-xml \
php8.3-mbstring php8.3-curl php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath
# Verify both FPM services are active independently
systemctl status php8.1-fpm --no-pager | head -n 5
systemctl status php8.3-fpm --no-pager | head -n 5
# List installed PHP CLI binaries
ls -la /usr/bin/php8.*
3. update-alternatives: how the PHP CLI default is managed
After installation, the binaries /usr/bin/php8.1 and /usr/bin/php8.3 exist side by side, but the plain php -v call in the shell has to point at exactly one of them. That is precisely what update-alternatives is for, a Debian mechanism that manages symbolic links with priorities and repoints a single generic name such as /usr/bin/php at one concrete version. Every installed PHP version automatically registers itself as an alternative during package setup, with a priority that roughly matches its version number.
The command sudo update-alternatives --config php opens an interactive menu listing every registered PHP version with its priority and lets the user type a number. For automation scripts where no interaction is possible, update-alternatives --set php /usr/bin/php8.3 works instead without any prompt. It is crucial to understand that this switch only affects the php CLI invocation in the shell and in cron jobs, it has no effect whatsoever on the FPM pools that the web server addresses via Unix sockets and that are configured completely independently.
# List all registered PHP alternatives with their priorities
sudo update-alternatives --list php
# Interactive selection menu
sudo update-alternatives --config php
# Selection Path Priority Status
# 0 /usr/bin/php8.3 83 auto mode
#* 1 /usr/bin/php8.1 81 manual mode
# 2 /usr/bin/php8.3 83
# Non-interactive switch, useful in provisioning scripts
sudo update-alternatives --set php /usr/bin/php8.1
# Confirm the active CLI version
php -v | head -n 1
# Same mechanism also applies to php-config and phpize
sudo update-alternatives --config php-config
sudo update-alternatives --config phpize
4. PHP-FPM pools: a dedicated process per version
PHP-FPM organizes worker processes into so called pools, each defined in its own configuration file under /etc/php/8.3/fpm/pool.d/. Every installed PHP version ships a default pool named www.conf by default, which already opens its own Unix socket at /run/php/php8.3-fpm.sock. For production multi-project hosting, though, the default pool is rarely sufficient, because every project needs its own resource limits, its own system user, and its own process management to avoid interfering with each other.
A clean setup creates a dedicated pool with a unique name for every project, for example shop-client-a.conf, with its own user, group, and socket path. The pm = dynamic directive combined with individually tuned values for pm.max_children, pm.start_servers, and pm.max_requests prevents a memory hungry project from crowding out another project's FPM workers beyond that project's own PHP memory limits. Every pool change requires a systemctl reload php8.3-fpm, which finishes serving in flight requests before the new configuration takes effect.
; /etc/php/8.3/fpm/pool.d/shop-client-a.conf
; Dedicated pool for a single Magento project on PHP 8.3
[shop-client-a]
user = shop-client-a
group = shop-client-a
; Unique socket path, referenced later in the Nginx vhost
listen = /run/php/php8.3-fpm-shop-client-a.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
; Per-pool php.ini overrides, isolated from other pools
php_admin_value[memory_limit] = 2G
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
php_admin_value[error_log] = /var/log/php/shop-client-a-error.log
php_flag[display_errors] = off
5. Binding an Nginx vhost per project to the right pool
The final building block is the connection between Nginx and the matching FPM socket. In the fastcgi_pass directive of the respective server block, you enter the exact socket path of the associated pool, not the version's default socket. This way, the Nginx configuration alone decides which PHP version and which resource pool serves a given project, independent of whichever version is currently set as the global CLI default via update-alternatives.
This separation is the decisive advantage over a single global PHP installation: updating the CLI default for maintenance scripts on the server affects not a single running vhost, because Nginx communicates exclusively through the hard wired socket path. To test whether a project is compatible with a new PHP version, you simply copy the vhost, change the socket path to point at the test pool, and test through an alternate hostname, without touching the production vhost at all.
# /etc/nginx/sites-available/shop-client-a.conf
server {
listen 443 ssl http2;
server_name shop-client-a.com;
root /var/www/shop-client-a/pub;
index index.php;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# Points directly at the dedicated pool socket for PHP 8.3
fastcgi_pass unix:/run/php/php8.3-fpm-shop-client-a.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 300;
}
}
# Reload after every vhost or pool change
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl reload php8.3-fpm
6. Safely switching the CLI version per project
For deploy scripts, Composer calls, and bin/magento commands within a single project, the global update-alternatives default is often too blunt, because multiple projects with different CLI requirements have to work on the same server at the same time. The safe path uses the direct, version specific binary instead of the generic php command: /usr/bin/php8.1 bin/magento setup:upgrade is guaranteed to invoke PHP 8.1, regardless of whichever version currently happens to be the global default.
For Composer, the same strategy is recommended, either via the COMPOSER_PHP_PATH environment variable or by calling the desired binary directly, for example /usr/bin/php8.3 /usr/local/bin/composer.phar install. In deploy scripts, define a variable such as PHP_BIN="/usr/bin/php8.3" at the top and reference it consistently for every subsequent call, instead of relying on whatever global default happens to be active at execution time. This makes the script reproducible, regardless of who runs it, when, or on which server.
#!/usr/bin/env bash
# deploy-shop-client-a.sh: pin PHP version explicitly for reproducible deploys
set -euo pipefail
readonly PHP_BIN="/usr/bin/php8.3"
readonly PROJECT_DIR="/var/www/shop-client-a"
readonly COMPOSER_BIN="/usr/local/bin/composer.phar"
cd "$PROJECT_DIR"
echo "[INFO] Using $($PHP_BIN -v | head -n 1)"
"$PHP_BIN" "$COMPOSER_BIN" install --no-dev --optimize-autoloader
"$PHP_BIN" bin/magento maintenance:enable
"$PHP_BIN" bin/magento setup:upgrade
"$PHP_BIN" bin/magento setup:di:compile
"$PHP_BIN" bin/magento setup:static-content:deploy de_DE en_US -f
"$PHP_BIN" bin/magento maintenance:disable
sudo systemctl reload php8.3-fpm
echo "[OK] Deploy finished with pinned PHP 8.3"
7. Maintaining extensions and php.ini per version
Every parallel installed PHP version has its own configuration directory, for example /etc/php/8.1/ and /etc/php/8.3/, each with separate subfolders for cli, fpm, and mods-available. Enabling an extension via phpenmod therefore has to happen for each version individually, there is no global enable. The command sudo phpenmod -v 8.3 redis enables the Redis extension specifically for PHP 8.3, while PHP 8.1 remains untouched until the same command is run there as well.
For project specific php.ini values such as memory_limit or max_execution_time, the pool configuration from section 4 is the preferred place, since it affects only that particular project and can be versioned together with the repository on every deploy. Global changes to the php.ini under /etc/php/8.3/fpm/php.ini, on the other hand, affect every pool of that version and should be reserved for settings that genuinely apply version wide, such as opcache.enable or timezone values.
A proven safeguard before any larger rollout is a diff comparison of installed extension packages between two versions, for example with dpkg -l | grep php8.1- against dpkg -l | grep php8.3-. Equally useful is php8.3 --ini | grep "Loaded Configuration", which shows exactly which php.ini file a given CLI binary actually loads, since with several parallel versions this is by no means always obvious and is a common source of support requests.
8. Magento projects: mapping version requirements concretely
With every minor release, Magento publishes clearly documented supported PHP ranges, and these ranges typically overlap only partially between consecutive Magento versions. Magento 2.4.6 supports PHP 8.1 and 8.2, Magento 2.4.7 extends that to PHP 8.3, and Magento 2.4.8 requires PHP 8.3 as a minimum while supporting newer patch releases too. Anyone maintaining several Magento stores at different patch levels ends up needing at least two PHP versions running in parallel on the same server almost by necessity.
Before any version switch, it is worth checking the composer.json of the respective Magento project, where the require.php directive specifies the exactly supported interval, for example ">=8.3.0 <8.4.0-0". A Composer update run against a non matching active CLI version immediately fails with a clear error message in such cases, rather than installing incompatible packages, which is one of the most reliable safeguards against accidental version mix ups in the deploy process.
9. Switching methods in direct comparison
The five most common pitfalls when running multiple PHP versions in parallel can be named clearly and are just as clearly avoided through established practices. The table below contrasts the risky default approach with the recommended, production safe approach for each of them.
| Task | Risky / Error-prone | Recommended approach | Benefit |
|---|---|---|---|
| CLI call in deploy scripts | php bin/magento ... | /usr/bin/php8.3 bin/magento ... | Independent of the global update-alternatives default |
| FPM mapping in the vhost | Shared default socket across all projects | Dedicated pool and socket per project | No mutual resource crowding out |
| Extension management | Only one version populated, second forgotten | phpenmod -v per version, diff check | Missing modules surface before the deploy |
| Switching the CLI default | Manually repointing symlinks by hand | update-alternatives --set php ... | Consistent, documented, scriptable |
| Checking version compatibility | Guessing the version or skimming a changelog | Evaluating require.php from composer.json | Composer stops cleanly on any mismatch |
In practice, most incidents involving the wrong PHP version originate from exactly these five spots: an ambiguous CLI call, a shared FPM socket, a forgotten extension, manually twisted symlinks, and ignored Composer constraints. Making these five points explicit and consistent in deploy scripts and server documentation reduces version related outages to a minimum, even when five or more Magento stores at different patch levels run on the same server at once.
Mironsoft
Server administration, PHP infrastructure and Magento hosting
Keeping multiple Magento stores on one server under control?
We set up clean multi-PHP environments with dedicated FPM pools per project and make sure every Magento version gets exactly the PHP version it actually needs, without interfering with the others.
Multi-PHP setup
Versioned FPM packages, clean pool separation and documented CLI defaults
Deploy automation
Version pinned deploy scripts that never assume a global PHP default
Magento migration
PHP version switches for Magento upgrades without downtime for other projects
10. Summary
Running multiple PHP versions in parallel on Linux is fully solvable with the standard tools of Debian and Ubuntu, without any container overhead. Versioned packages from the Ondrej Sury PPA install every PHP version as an independent FPM daemon with its own socket. update-alternatives controls only the global CLI default and has no effect on running web projects. Dedicated FPM pools per vhost provide clean resource separation, and using the direct, version specific binary in deploy scripts makes every workflow independent of whichever global default happens to be active.
The biggest practical lever is actively addressing each of the five typical failure sources, an ambiguous CLI call, a shared FPM socket, a forgotten extension, manually twisted symlinks, and ignored Composer constraints, in the deploy process instead of leaving them to chance. Once this structure is set up cleanly, you can run any number of Magento stores with different PHP requirements on the same server without a version switch for one project ever endangering another.
Managing Multiple PHP Versions in Parallel: The Essentials at a Glance
Packages per version
php8.1-fpm and php8.3-fpm run as fully independent daemons, extensions must be installed for each version separately.
update-alternatives
Controls only the global php CLI command, has no effect on FPM pools or running vhosts.
One pool per vhost
Its own socket, its own system user, its own resource limits per project prevent mutual interference.
Pin deploy scripts
/usr/bin/php8.3 instead of php in deploy scripts makes workflows reproducible and independent of the global default.