Install PHP & PHP-FPM (Ondřej Surý Repo) — Multi‑Version (5.6 → 8.4)

Target OS: Ubuntu/Debian family
Audience: Sysadmins/DevOps running Nginx or Apache with multiple PHP versions side‑by‑side.


Why use the Ondřej Surý repository?

  • Multiple PHP branches in parallel. Official Ubuntu/Debian repos typically ship one current PHP per release. Ondřej’s repo exposes many versions (legacy 5.6, 7.x, 8.x) side‑by‑side (separate packages and sockets).
  • Security & backports. Popular builds with timely rebuilds, patches, and fixes for older branches that you may still need to run for legacy apps (while you plan upgrades).
  • Consistent package naming. Predictable names across versions (phpX.Y-…) so automation is simpler.
  • Community standard. De‑facto upstream used by a large portion of the PHP on Ubuntu/Debian ecosystem for years.

⚠️ Caveat: PHP 5.6 and some 7.x branches are EOL. Only run them for legacy workloads in isolated hosts/containers, with WAF and strict network policies. Plan migrations to supported PHP versions ASAP.


Repository setup

Ubuntu (preferred: PPA)

# Prereqs
sudo apt update
sudo apt install -y software-properties-common ca-certificates lsb-release apt-transport-https

# Add the PPA
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update

Debian (bookworm/bullseye etc.)

# Prereqs
sudo apt update
sudo apt install -y ca-certificates lsb-release apt-transport-https curl gnupg

# Add the official Sury APT repository
curl -fsSL https://packages.sury.org/php/apt.gpg | sudo gpg --dearmor -o /usr/share/keyrings/sury-php.gpg
echo "deb [signed-by=/usr/share/keyrings/sury-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" \
  | sudo tee /etc/apt/sources.list.d/sury-php.list
sudo apt update

Tip: To avoid accidental mixing with the OS’s PHP, optionally pin Sury packages higher:

/etc/apt/preferences.d/php-sury

Package: php*
Pin: origin packages.sury.org
Pin-Priority: 700

Installing multiple PHP versions (CLI + FPM)

Below installs PHP 5.6, 7.4, 8.1, 8.2, 8.3, 8.4 (add/remove versions as needed).

# Choose your versions
VERSIONS="5.6 7.4 8.1 8.2 8.3 8.4"

# Core packages per version
for v in $VERSIONS; do
  sudo apt install -y php${v} php${v}-fpm php${v}-cli php${v}-common php${v}-opcache php${v}-curl php${v}-mbstring php${v}-xml php${v}-zip
done

Service names (systemd):

  • php5.6-fpm, php7.4-fpm, php8.1-fpm, php8.2-fpm, php8.3-fpm, php8.4-fpm
  • Sockets (default): /run/php/phpX.Y-fpm.sock

Start/enable examples:

sudo systemctl enable --now php7.4-fpm php8.1-fpm php8.2-fpm php8.3-fpm php8.4-fpm
# Start legacy only when needed
sudo systemctl enable --now php5.6-fpm

Switching default CLI version (update-alternatives)

# Register installed binaries with alternatives (usually auto, but safe to force)
for v in 5.6 7.4 8.1 8.2 8.3 8.4; do
  sudo update-alternatives --install /usr/bin/php php /usr/bin/php${v} $((100+${v/./}))
  sudo update-alternatives --install /usr/bin/phar phar /usr/bin/phar${v} $((100+${v/./})) || true
  sudo update-alternatives --install /usr/bin/phar.phar phar.phar /usr/bin/phar.phar${v} $((100+${v/./})) || true
done

# Choose default interactively
sudo update-alternatives --config php

# Check
php -v

Nginx integration (per‑site PHP version)

Example server block using PHP‑FPM 8.2 socket. Duplicate per site and change to the required version.

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

  index index.php index.html;

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

  location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    # Point to the desired PHP-FPM version:
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
  }

  location ~ /\.ht {
    deny all;
  }
}

Reload Nginx after enabling the matching PHP‑FPM service:

sudo nginx -t && sudo systemctl reload nginx

Using TCP instead of sockets? Edit pool listen to 127.0.0.1:9082 (see pool config below) and set fastcgi_pass 127.0.0.1:9082;


Apache integration

Two main patterns (choose one per vhost):

1) mod_php (simpler, but one PHP per Apache MPM; not side‑by‑side friendly):

   sudo apt install -y libapache2-mod-php8.2
   sudo a2dismod php7.4 php8.1 php8.3 php8.4
   sudo a2enmod php8.2
   sudo systemctl reload apache2

2) php-fpm via proxy_fcgi (recommended; per‑vhost versioning):

   sudo a2enmod proxy_fcgi setenvif
   sudo a2enconf php8.2-fpm
   # Or configure per-vhost:
   # <FilesMatch \.php$>
   #   SetHandler "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost/"
   # </FilesMatch>
   sudo systemctl reload apache2

PHP‑FPM pool tuning (per version)

Each version ships a default pool: /etc/php/X.Y/fpm/pool.d/www.conf

Key directives to review:

; -- Listener (socket or TCP) --
listen = /run/php/php8.2-fpm.sock
;listen = 127.0.0.1:9082

; -- Ownership/permissions for Nginx/Apache --
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

; -- Process manager --
pm = dynamic          ; options: static | dynamic | ondemand
pm.max_children = 20  ; set based on RAM and app concurrency
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6

; -- Timeouts & slow log --
request_terminate_timeout = 120s
request_slowlog_timeout = 5s
slowlog = /var/log/php8.2-fpm/slow.log

; -- Security hardening --
php_admin_value[expose_php] = 0
php_admin_value[session.cookie_httponly] = 1
php_admin_value[session.cookie_secure] = 1
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec,parse_ini_file,show_source

Apply changes:

sudo systemctl restart php8.2-fpm

Sizing rule‑of‑thumb:
pm.max_children ≈ (Available RAM for PHP) / (Avg PHP process MB)
Measure via ps, smem, or /proc after steady state load.


Per‑version php.ini overrides & extensions

  • Global INI: /etc/php/X.Y/fpm/php.ini and /etc/php/X.Y/cli/php.ini
  • Drop‑in overrides: /etc/php/X.Y/fpm/conf.d/*.ini
  • Common extensions: phpX.Y-mysql, phpX.Y-imagick, phpX.Y-gd, phpX.Y-intl, phpX.Y-bcmath, etc.

Example enabling Opcache/JIT (for PHP ≥ 8.0):

; /etc/php/8.2/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=1
opcache.revalidate_freq=2
; JIT (PHP 8+)
opcache.jit=1255
opcache.jit_buffer_size=128M

Restart FPM after changes.


Installing specific versions quickly

# Example: only 7.4 and 8.2 stacks
sudo apt install -y php7.4 php7.4-fpm php7.4-cli php7.4-{mysql,gd,xml,mbstring,zip,curl,bcmath,intl}
sudo apt install -y php8.2 php8.2-fpm php8.2-cli php8.2-{mysql,gd,xml,mbstring,zip,curl,bcmath,intl}

Check binaries/sockets:

php -v
systemctl status php7.4-fpm php8.2-fpm
ss -lpn | grep php-fpm

Running truly legacy PHP 5.6 safely

  • Install only where required: sudo apt install -y php5.6 php5.6-fpm ...
  • Isolate: separate VM/container; restrictive security groups; no public ingress if possible.
  • Put Nginx/OpenResty with WAF (e.g., ModSecurity/OWASP CRS) in front.
  • Disable dangerous functions; set open_basedir; use read‑only FS if possible.
  • Plan migration: run dual‑stack (old + new) behind a traffic splitter to gradually cut over.

Nginx per‑site pool example (clean isolation)

Create a dedicated pool per vhost for resource isolation:

/etc/php/8.2/fpm/pool.d/example.conf

[example_com]
user = www-data
group = www-data
listen = /run/php/php-8.2-example.sock
pm = ondemand
pm.max_children = 30
pm.process_idle_timeout = 10s
; Higher limits for this site only
php_admin_value[memory_limit] = 512M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M

Nginx vhost:

location ~ \.php$ {
  include snippets/fastcgi-php.conf;
  fastcgi_pass unix:/run/php/php-8.2-example.sock;
}

Reload:

sudo systemctl reload php8.2-fpm
sudo systemctl reload nginx

Apache per‑vhost (php-fpm) example

<VirtualHost *:80>
  ServerName example.com
  DocumentRoot /var/www/example.com/public

  <Directory /var/www/example.com/public>
    AllowOverride All
    Require all granted
  </Directory>

  <FilesMatch \.php$>
    SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost/"
  </FilesMatch>
</VirtualHost>

Best practices checklist

  • Keep versions minimal. Install only the branches your apps need. Remove unused:
    sudo apt purge 'php5.6*' (example).
  • Pin & freeze wisely. Use apt preferences or apt-mark hold for critical stacks to avoid surprise upgrades.
  • Separate pools per app. Per‑pool limits (pm.*, memory_limit) prevent noisy‑neighbor issues.
  • Harden FPM. Disable dangerous functions, expose_php=0, set session.cookie_secure=1 (when behind HTTPS), use session.use_strict_mode=1.
  • Observe. Enable FPM status & ping pages (firewalled):
    ; pool.d/*.conf
    pm.status_path = /status
    ping.path = /ping
    ping.response = pong

    Nginx location (internal or IP‑restricted):

    location ~ ^/(status|ping)$ {
    allow 127.0.0.1; deny all;
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    }
  • Logs & slowlog. Use request_slowlog_timeout and analyze bottlenecks.
  • Opcache. Tune memory and validation; verify with php -i | grep opcache.
  • Resource limits. Use systemd overrides if needed:
    sudo systemctl edit php8.2-fpm
    [Service]
    MemoryMax=1G
    TasksMax=256
  • Zero‑downtime restarts. Use reload for Nginx/Apache and restart PHP‑FPM version by version; drain traffic if using LB.
  • Automate. Keep IaC scripts (Ansible/Terraform/Cloud‑Init) to standardize packages and configs.
  • Backups. Version‑control /etc/php/, Nginx/Apache vhosts, and deploy from code.

Troubleshooting quick wins

  • 502 Bad Gateway: FPM not running or wrong socket path → systemctl status phpX.Y-fpm, fix fastcgi_pass.
  • Permission denied on socket: Ensure listen.owner/group/mode allow the web server user.
  • Mixed versions: phpinfo() shows different version than expected → check vhost handler (proxy_fcgi vs mod_php), and FPM pool used.
  • Extensions missing: Install the phpX.Y-<ext> for the target version (CLI and FPM use separate INI dirs).
  • High memory usage: Reduce pm.max_children, lower memory_limit, profile code paths, enable Opcache.

Uninstalling a version cleanly

sudo systemctl stop php7.4-fpm
sudo apt purge -y 'php7.4*'
sudo apt autoremove -y
sudo nginx -t && sudo systemctl reload nginx

Quick copy‑paste: install all common versions

# Ubuntu: ensure ppa:ondrej/php is added; Debian: ensure packages.sury.org/php is added
sudo apt update

for v in 5.6 7.4 8.1 8.2 8.3 8.4; do
  sudo apt install -y php${v} php${v}-fpm php${v}-cli php${v}-common php${v}-opcache php${v}-curl php${v}-mbstring php${v}-xml php${v}-zip
done

Happy hosting!


🔒 Best Security Recommendations (PHP & PHP-FPM)

When running PHP in production, security hardening is as important as performance. Below are best practices:

  1. Run least privilege

    • Always run PHP-FPM pools under a non-root user (e.g., www-data).
    • Use per-site pools with separate users if apps differ in trust level.
  2. Disable dangerous functions
    In pool configs or php.ini:

    disable_functions = exec,passthru,shell_exec,system,proc_open,popen,phpinfo,curl_exec,curl_multi_exec,parse_ini_file,show_source

    Add/remove depending on app needs.

  3. Hide PHP version

    expose_php = Off

    Prevents leaking PHP version in headers.

  4. Limit file operations

    • Use open_basedir to restrict PHP’s file access:
      open_basedir = /var/www/example.com:/tmp/
    • Ensure upload/tmp directories are not executable.
  5. Session hardening

    session.cookie_httponly = 1
    session.cookie_secure = 1      ; requires HTTPS
    session.use_strict_mode = 1
    session.cookie_samesite = Strict
  6. Restrict FPM status & ping pages
    Only expose to localhost or monitoring IPs. Never expose /status or /ping publicly.

  7. Error handling

    display_errors = Off
    log_errors = On
    error_log = /var/log/php/error.log

    Prevents leaking sensitive data to users.

  8. Resource control

    • Use memory_limit wisely (e.g., 256M per pool).
    • Limit request sizes:
      post_max_size = 16M
      upload_max_filesize = 16M
      max_execution_time = 30
  9. Logging & monitoring

    • Enable slowlog for long requests.
    • Centralize logs with ELK/Graylog/Wazuh.
    • Use Prometheus exporters or FPM status for metrics.
  10. Regular patching

    • Use Ondřej’s repo for latest security updates.
    • Automate security upgrades (unattended-upgrades) for PHP packages.
  11. Isolation & containers

    • Use Docker/LXC/VM for legacy PHP (5.6/7.x).
    • Do not mix high-risk legacy apps with modern apps in the same host.
  12. Web server WAF

    • Protect upstream PHP apps with ModSecurity + OWASP CRS or a reverse proxy WAF (e.g., Nginx with NAXSI, Cloudflare, etc.).
  13. TLS everywhere

    • Always serve apps over HTTPS (Let’s Encrypt auto-renewal recommended).
    • Enforce Strict-Transport-Security headers.
  14. File permissions

    • Webroot owned by root, writable dirs owned by app user:
      sudo chown -R root:root /var/www/example.com
      sudo chown -R www-data:www-data /var/www/example.com/storage
    • Files 644, directories 755 (or stricter).
  15. Systemd sandboxing (extra hardening)
    /etc/systemd/system/php8.2-fpm.service.d/override.conf

    [Service]
    NoNewPrivileges=true
    PrivateTmp=true
    ProtectSystem=full
    ProtectHome=true
    RestrictSUIDSGID=true

    Then reload systemd: sudo systemctl daemon-reexec