Run multiple PHP versions on one server (5.6 to 8.4), per site
A legacy client site needs PHP 7.4; your new app wants 8.4. You don't need two servers — one machine can run every PHP version at once, isolated per site. Here's how multi-PHP actually works, the traps to avoid, and how to make switching a site's version a one-click job.
Anyone who hosts more than one PHP site eventually hits the version wall. An older client site runs a theme or plugin that breaks on anything past PHP 7.4. The app you're building today wants 8.3 or 8.4 for its typed properties and performance. A third project is stuck on some 5.6-era code nobody has budget to modernise. The instinct is to spin up a separate server per era — but that's wasteful and unnecessary. One Linux box can run every PHP version at once, with each site pinned to exactly the one it needs. Here's how that works, and how to keep it clean.
Why multi-PHP matters
- Legacy and modern, side by side. You don't have to force an old site onto a new PHP (and watch it white-screen) or hold a new app back on an old one. Each gets its correct version.
- Safe upgrades. You can move a site from 8.1 to 8.3 on its own, test it, and roll back — without touching the twelve other sites on the box.
- One server, many eras. No paying for extra VPSes just to segregate PHP versions. The cost of hosting a 5.6 site next to an 8.4 site is basically zero.
The mechanism that makes this possible is PHP-FPM (FastCGI Process Manager), and understanding it is the whole game.
How running many PHP versions works
Modern Linux distros let you install multiple PHP versions in parallel. On Ubuntu/Debian the common route is the ondrej/php PPA, which packages php5.6-fpm, php7.4-fpm, php8.1-fpm, … php8.4-fpm as separate services that coexist happily.
Each installed version runs its own FPM master, and each master can run multiple pools. A pool is an isolated worker process group with its own config: which user it runs as, its resource limits, and — crucially — its own listening socket. So the picture is:
php7.4-fpm → pool: siteA → /run/php/siteA.sock (runs as user siteA)
php8.4-fpm → pool: siteB → /run/php/siteB.sock (runs as user siteB)
Your web server (Nginx) doesn't execute PHP itself — it hands .php requests to a socket over FastCGI. So "what PHP version does this site use?" is answered by one line in the site's Nginx config: which socket it forwards to.
# Site A on PHP 7.4
location ~ \.php$ {
fastcgi_pass unix:/run/php/siteA.sock;
include fastcgi_params;
}
Point that fastcgi_pass at the 7.4 pool's socket and the site runs 7.4; point it at the 8.4 pool's socket and it runs 8.4. Switching a site's PHP version is really just re-pointing its socket and reloading Nginx. Everything else — files, database — is untouched.
Isolation: the part people skip
Running one pool per site isn't only about versions; it's also how you keep tenants apart. Two rules do the heavy lifting:
- One Linux user per pool. Run each site's FPM pool as its own dedicated user (not a shared
www-data). Then a compromised plugin on one site can't read another site's files or database credentials — the OS permissions stop it. open_basedirper pool. Setopen_basedirin each pool's config to pin its PHP to that site's document root (plus its temp dir). Even a buggy or malicious script then physically cannot read outside its own directory.
Get these two right and "many PHP versions on one box" also means "many safely isolated sites on one box." (More on that layered isolation in hosting multiple client sites on one VPS.)
The traps to watch for
- CLI PHP is a different version. The
phpyou get on the command line (for Composer, WP-CLI, cron) is the system default — often not the version a given site's FPM pool uses. Run version-specific binaries (php7.4,php8.4) or set the per-site default so your cron jobs don't run under the wrong version. - Extensions are per version.
php-redis,php-imagick,php-gdand friends must be installed for each PHP version separately (php8.4-redis,php7.4-redis). A site that "lost" an extension after a version switch usually just needs that extension package for the new version. - Each pool costs memory. Every pool holds worker processes in RAM. A dozen busy pools add up — size
pm.max_childrenper pool and watch memory, rather than letting each pool assume it owns the whole box. - Reload, don't forget. After changing a pool or a socket, reload the FPM service and Nginx. A renewed config that isn't reloaded is a config that isn't live.
Doing this by hand is fiddly — make it one click
None of the above is hard in isolation. But wiring it correctly for every site — install the right FPM version, create a pool as the site's user with the right open_basedir and limits, generate a socket, repoint Nginx, reload both services, keep CLI and extensions aligned — is a lot of fiddly, error-prone steps to repeat by hand each time you add a site or change a version.
That's exactly what a control panel should absorb. ShadowPanel manages the full multi-PHP stack for you:
- PHP 5.6 through 8.4, installed in parallel and ready per site.
- Switch a site's version from a dropdown — the panel regenerates that site's isolated FPM pool, repoints the Nginx socket, and reloads both services. No SSH, no config editing.
- Isolation applied by default — every pool runs as the site's own Linux user with
open_basedirenforced. - Extensions and per-site config handled through the panel, so a version switch doesn't quietly drop what the site needs.
You pick the version; the panel does the plumbing — safely, the same way every time.
The bottom line
You don't need a server per PHP era. One machine, PHP-FPM, and one isolated pool per site let you run 5.6 next to 8.4 with each site on exactly the version it needs — and, done right, each site sealed off from the others. The concepts are simple; the per-site wiring is tedious and easy to get subtly wrong. ShadowPanel turns "switch this site's PHP version" into a dropdown, with the isolation baked in.
curl -fsSL https://shadowpanel.de/install.sh | bash