Linux Basics

Run Multiple PHP Versions Side by Side with PHP-FPM and Nginx on Debian and Ubuntu

Give each site its own PHP version with separate PHP-FPM pools and sockets, keep the CLI predictable, and avoid the traps of mixed-version servers.

Published
Reading time
8 min read

One application is certified for PHP 8.3, another is ready for 8.5, and you want to move them one at a time instead of all at once. PHP-FPM makes that possible: every PHP version runs as its own service, and every site can have its own pool, Unix user and socket. Nginx then decides which version serves which site. This guide walks through the setup on Debian and Ubuntu, and is honest about the trade-offs.

Before you start: where the extra versions come from

Debian and Ubuntu each ship one PHP branch per release: Debian 13 (trixie) has PHP 8.4, Debian 12 (bookworm) has PHP 8.2, Ubuntu 24.04 has PHP 8.3 and Ubuntu 22.04 has PHP 8.1. Those packages get security fixes from the distribution. Any other version has to come from a third-party repository; the widely used one for Debian is packages.sury.org, and the Ubuntu equivalent is the ondrej/php PPA. Keep three facts in mind:

  • Security updates for those packages come from the repository maintainer, not from the Debian or Ubuntu security teams.
  • The repository also builds the version your distribution ships, with higher version numbers, so after adding it your existing PHP packages are upgraded from it as well. apt policy php8.4-fpm shows which origin is in use.
  • The repository still carries end-of-life branches (5.6 to 8.1). Being installable does not make them supported. Upstream PHP currently supports 8.2 (security fixes until 31 December 2026), 8.3 (until 31 December 2027), 8.4 (until 31 December 2028) and 8.5 (until 31 December 2029).

Take a backup or snapshot of the server before changing package sources.

Step 1: Add the repository (Debian)

These are the commands published in the repository's README:

sudo apt-get update
sudo apt-get -y install lsb-release ca-certificates curl
sudo curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
sudo dpkg -i /tmp/debsuryorg-archive-keyring.deb
sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/debsuryorg-archive-keyring.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
sudo apt-get update

On Ubuntu 22.04 and 24.04, add the PPA with sudo add-apt-repository ppa:ondrej/php instead. Read its description page first: in October 2026 it says the PPA is being merged into packages.sury.org, which is already the recommended source for the newest Ubuntu release, and that migration instructions will follow.

Step 2: Install the second version

Install FPM, the CLI and the extensions your application needs. Extensions are per version: php8.3-intl does nothing for PHP 8.4.

sudo apt install php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring \
  php8.3-xml php8.3-curl php8.3-intl php8.3-zip php8.3-gd
systemctl status php8.3-fpm
ls -l /run/php/

Each version gets its own service (php8.3-fpm, php8.4-fpm), its own configuration tree under /etc/php/8.3/ and a default pool listening on /run/php/php8.3-fpm.sock.

Step 3: Give each site its own pool

The default www pool runs every site as www-data. A dedicated pool per site keeps one site's PHP code from reading another site's files and lets you size each site separately. Create a system user, then a pool file such as /etc/php/8.3/fpm/pool.d/shop.conf:

sudo useradd --system --user-group --home-dir /var/www/shop --shell /usr/sbin/nologin shop
[shop]
user = shop
group = shop
listen = /run/php/php8.3-fpm-shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 10s

The listen.owner and listen.group lines let Nginx (running as www-data) connect to the socket while PHP itself runs as shop. With pm = ondemand, workers start only when requests arrive and stop after the idle timeout, which matters when several PHP versions share a small VPS. Give the shop user write access only to the directories the application must write to, such as uploads and cache. Then test and restart:

sudo php-fpm8.3 -t
sudo systemctl restart php8.3-fpm
ls -l /run/php/php8.3-fpm-shop.sock

Step 4: Point Nginx at the right socket

Debian's Nginx package ships snippets/fastcgi-php.conf, which splits the path, returns 404 for PHP files that do not exist and includes the standard FastCGI parameters. Use it and name the versioned socket explicitly:

server {
    listen 80;
    server_name shop.example.com;
    root /var/www/shop/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm-shop.sock;
    }
}
sudo nginx -t && sudo systemctl reload nginx

Moving the site to PHP 8.5 later is a matter of creating the same pool under /etc/php/8.5/fpm/pool.d/, changing one fastcgi_pass line and reloading Nginx; rolling back is the reverse. Avoid the generic /run/php/php-fpm.sock: Debian manages it with update-alternatives and points it at the highest-priority FPM version that is running, so a site using it can switch PHP versions silently when you install a new one.

Step 5: Keep the command line predictable

The php command is also an alternative, and a newly installed higher version takes it over in automatic mode. Cron jobs, Composer and framework CLIs then run on a different version from the website. Either fix the default:

update-alternatives --display php
sudo update-alternatives --set php /usr/bin/php8.3

or, better on mixed servers, call the versioned binary explicitly wherever it matters:

/usr/bin/php8.3 /var/www/shop/artisan schedule:run
php8.3 "$(command -v composer)" install --no-dev

Step 6: Verify from a real request

Put a one-line file with a random name in the site's web root that prints PHP_VERSION and php_sapi_name(), open it once, and delete it. You should see the expected version and fpm-fcgi. Do not leave phpinfo() pages behind; they expose configuration and environment details.

Pitfalls

  • Patching only one version. Every installed branch needs updates. When PHP publishes a security release, check every php8.x package with apt policy.
  • Separate php.ini files. Settings live in /etc/php/<version>/fpm/php.ini per version; a limit raised for 8.3 is not raised for 8.5.
  • Memory. Each FPM service has its own master process and workers. Count pm.max_children across all pools against the RAM you have.
  • Leftover versions. When the last site leaves a version, list its packages with dpkg -l 'php8.3*' and purge them, so an unused and unpatched PHP does not linger.

What you should do

  1. Prefer your distribution's PHP when it meets your applications' requirements; add a third-party repository only when you need another branch.
  2. Use only branches that upstream still supports, and note their end dates.
  3. Create one FPM pool, user and versioned socket per site, and reference that socket in Nginx.
  4. Pin the CLI with update-alternatives --set or call php8.x explicitly in cron and deploy scripts.
  5. Include every installed PHP version in your monthly patch check.

On a Skyline Cloud VPS in Riyadh you have root SSH access for all of this; what runs inside the server is your responsibility, and Riyadh servers have no platform snapshots, so take your own backup before changing repositories.

Sources

SKYLINE Engineering

@skyline

The engineering team at SKYLINE Industrial Solutions. We publish field-tested guides drawn from real KSA and GCC deployments.

See author profile
SKYLINE engineering services

Need this implemented for you?

Reading is free — building it right takes a team. SKYLINE engineers ship Linux Basics for Aramco vendors, banks, hospitals and government agencies across Saudi Arabia. Talk to us before you start.

Aramco Approved Contractor ISO 9001 · ISO 27001 SAMA CSF aligned NCA ECC ready 247+ KSA clients

Comments

0 total · 0 threads
Be the first to leave a comment.