Target OS: Ubuntu/Debian family
Audience: Sysadmins/DevOps running Nginx or Apache with multiple PHP versions side‑by‑side.
phpX.Y-…
) so automation is simpler.⚠️ 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.
# 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
# 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
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
/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
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
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 setfastcgi_pass 127.0.0.1:9082;
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
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 viaps
,smem
, or/proc
after steady state load.
php.ini
overrides & extensions/etc/php/X.Y/fpm/php.ini
and /etc/php/X.Y/cli/php.ini
/etc/php/X.Y/fpm/conf.d/*.ini
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.
# 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
sudo apt install -y php5.6 php5.6-fpm ...
open_basedir
; use read‑only FS if possible.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
<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>
sudo apt purge 'php5.6*'
(example).apt preferences
or apt-mark hold
for critical stacks to avoid surprise upgrades.pm.*
, memory_limit
) prevent noisy‑neighbor issues.expose_php=0
, set session.cookie_secure=1
(when behind HTTPS), use session.use_strict_mode=1
.; 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;
}
request_slowlog_timeout
and analyze bottlenecks.php -i | grep opcache
.systemd
overrides if needed:sudo systemctl edit php8.2-fpm
→
[Service]
MemoryMax=1G
TasksMax=256
reload
for Nginx/Apache and restart
PHP‑FPM version by version; drain traffic if using LB./etc/php/
, Nginx/Apache vhosts, and deploy from code.systemctl status phpX.Y-fpm
, fix fastcgi_pass
.listen.owner/group/mode
allow the web server user.phpinfo()
shows different version than expected → check vhost handler (proxy_fcgi
vs mod_php), and FPM pool used.phpX.Y-<ext>
for the target version (CLI and FPM use separate INI dirs).pm.max_children
, lower memory_limit
, profile code paths, enable Opcache.sudo systemctl stop php7.4-fpm
sudo apt purge -y 'php7.4*'
sudo apt autoremove -y
sudo nginx -t && sudo systemctl reload nginx
# 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!
When running PHP in production, security hardening is as important as performance. Below are best practices:
Run least privilege
www-data
). 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.
Hide PHP version
expose_php = Off
Prevents leaking PHP version in headers.
Limit file operations
open_basedir
to restrict PHP’s file access:
open_basedir = /var/www/example.com:/tmp/
Session hardening
session.cookie_httponly = 1
session.cookie_secure = 1 ; requires HTTPS
session.use_strict_mode = 1
session.cookie_samesite = Strict
Restrict FPM status & ping pages
Only expose to localhost or monitoring IPs. Never expose /status
or /ping
publicly.
Error handling
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
Prevents leaking sensitive data to users.
Resource control
memory_limit
wisely (e.g., 256M
per pool). post_max_size = 16M
upload_max_filesize = 16M
max_execution_time = 30
Logging & monitoring
Regular patching
unattended-upgrades
) for PHP packages.Isolation & containers
Web server WAF
TLS everywhere
Strict-Transport-Security
headers.File permissions
sudo chown -R root:root /var/www/example.com
sudo chown -R www-data:www-data /var/www/example.com/storage
644
, directories 755
(or stricter).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