Nginx and multiple PHP versions on Debian and Ubuntu

Mascote do LinuxPro ligando o nginx a três PHP-FPM, placa 3D do hexágono verde na parede

Debian 12 ships PHP 8.2. Ubuntu 24.04, 8.3. Debian 13, 8.4. Ubuntu 26.04, 8.5. A real host needs all three at once: the old WordPress, the new Laravel, the cron CLI. The distro repository does not allow it. The one from Ondřej Surý does. Nginx comes from nginx.org — stable, pinned, without mixing in distro modules.

The landscape in September 2026

PHP in the official lifecycle:

  • 8.5 — active until Dec/2027, security until Dec/2029 (released 20 Nov 2025)
  • 8.4 — active until Dec/2026, security until Dec/2028
  • 8.3 — security only, until Dec/2027
  • 8.2 — security until 31 Dec 2026
  • 8.1 died on 31 Dec 2025 — do not install it on a new machine

Sury co-installs php8.3-fpm, php8.4-fpm and php8.5-fpm on the same box. Each has its own socket in /run/php/. Nginx chooses the socket in the vhost.

Sury's nginx repository closed in April 2026. PHP remains at packages.sury.org/php. Nginx: distro or nginx.org. Do not mix the two binaries.

Nginx from nginx.org

Debian 12 (bookworm), 13 (trixie); Ubuntu 24.04 (noble), 26.04 (resolute). amd64 and arm64. Key fingerprint: 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 (expires 24 May 2027).

Debian:

sudo apt install -y curl gnupg2 ca-certificates lsb-release debian-archive-keyring
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor 
  | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
gpg --dry-run --quiet --no-keyring --import --import-options import-show 
  /usr/share/keyrings/nginx-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] 
https://nginx.org/packages/debian $(lsb_release -cs) nginx" 
  | sudo tee /etc/apt/sources.list.d/nginx.list

Ubuntu — only the keyring and URL change:

sudo apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor 
  | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] 
https://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" 
  | sudo tee /etc/apt/sources.list.d/nginx.list

Pin it so apt does not revert to the distro's nginx, and install stable (even number: 1.30.x in 2026). Mainline is .../mainline/debian or .../mainline/ubuntu.

printf '%sn' 'Package: *' 'Pin: origin nginx.org' 'Pin: release o=nginx' 'Pin-Priority: 900' 
  | sudo tee /etc/apt/preferences.d/99nginx
sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx
nginx -v

The nginx.org package uses the user nginx and /etc/nginx/conf.d/ — it does not have sites-available. The distro package uses www-data and the Debian layout. Choose one and stick with it. systemd handles the service in both cases.

Want the distro's nginx, with sites-enabled and nginx-extras? Skip this section and run sudo apt install nginx. The rest of PHP is the same; only the socket owner changes.

PHP: Sury's DPA

Official recipe from packages.sury.org/php/README.txt — valid for Debian 12/13 and Ubuntu 24.04/26.04:

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

Ubuntu-only shortcut (Launchpad PPA from the same Ondřej):

sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update

Do not enable both at once. One PHP source is enough.

Three FPMs side by side + typical WordPress/Laravel extensions:

sudo apt install -y 
  php8.3-fpm php8.3-cli php8.3-mysql php8.3-xml php8.3-curl php8.3-mbstring php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath 
  php8.4-fpm php8.4-cli php8.4-mysql php8.4-xml php8.4-curl php8.4-mbstring php8.4-zip php8.4-gd php8.4-intl php8.4-bcmath 
  php8.5-fpm php8.5-cli php8.5-mysql php8.5-xml php8.5-curl php8.5-mbstring php8.5-zip php8.5-gd php8.5-intl php8.5-bcmath
sudo systemctl enable --now php8.3-fpm php8.4-fpm php8.5-fpm
ls /run/php/
# php8.3-fpm.sock  php8.4-fpm.sock  php8.5-fpm.sock

Default CLI (what php without a number calls):

sudo update-alternatives --config php
php -v
php8.5 -v

Align the socket with nginx

Diagrama: navegadores chegam no nginx, que roteia para três pools de PHP-FPM no mesmo host

With nginx.org, the worker runs as the user nginx. The default FPM pool is www-data. Without alignment, you get 502. In each pool:

sudo sed -i 's/^listen.owner.*/listen.owner = nginx/' /etc/php/8.4/fpm/pool.d/www.conf
sudo sed -i 's/^listen.group.*/listen.group = nginx/' /etc/php/8.4/fpm/pool.d/www.conf
# repita 8.3 e 8.5
sudo systemctl restart php8.3-fpm php8.4-fpm php8.5-fpm

If nginx is from the distro, leave www-data and skip the sed.

Example vhost — three sites, three PHP versions. With nginx.org, the file goes in /etc/nginx/conf.d/:

server {
    listen 80;
    server_name wp.exemplo.com.br;
    root /var/www/wp.exemplo.com.br;
    index index.php index.html;
    location / { try_files $uri $uri/ /index.php?$args; }
    location ~ .php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}
server {
    listen 80;
    server_name app.exemplo.com.br;
    root /var/www/app.exemplo.com.br/public;
    index index.php;
    location / { try_files $uri $uri/ /index.php?$query_string; }
    location ~ .php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.5-fpm.sock;
    }
}
sudo mkdir -p /var/www/wp.exemplo.com.br /var/www/app.exemplo.com.br/public
echo '<?php phpinfo();' | sudo tee /var/www/wp.exemplo.com.br/index.php
sudo nginx -t && sudo systemctl reload nginx

Check in phpinfo() the PHP Version and the path to php.ini (/etc/php/8.3/fpm/php.ini, etc.). Each major version has its own ini — changing 8.4 does not affect 8.5.

Firewall and what not to do

sudo ufw allow 22,80,443/tcp
sudo ufw enable

Do not install libapache2-mod-php* on this box. Do not point two identical server_name to different sockets and expect a miracle. Do not deploy PHP 8.1 “because the plugin requires it”: Sury still packages it, but upstream support has ended. 8.2 only lasts until the end of 2026.

A panel with nginx+PHP already set up: ISPConfig. This post covers the bare machine, without a panel.

Wrap-up

One pin for nginx.org, one Sury DPA, three FPMs, three sockets, three server. Apt updates the point release; the vhost selects the major version. When 8.2 loses security support, remove the package — 8.5 in the other vhost will not even notice.

Nginx: nginx.org/en/linux_packages.html · Sury PHP: packages.sury.org/php · PHP lifecycle: php.net/supported-versions.