The modern way to run PHP under Apache is the event MPM for connections and PHP-FPM for PHP. Beyond performance, this setup lets every site run as its own Linux user and even its own PHP version — something mod_php can’t do. This guide covers the migration and the per-site configuration.
Why move away from mod_php
mod_php embeds PHP into every Apache process. It forces the old prefork MPM, wastes memory serving static files with PHP-loaded processes, runs all sites as the same user (www-data), and allows only one PHP version server-wide.
With PHP-FPM:
- Apache threads stay small; PHP runs in a separately sized pool.
- Each site can have its own pool, user and
php.inioverrides. - Several PHP versions can run side by side.
- HTTP/2 becomes available (it requires event or worker MPM).
1. Install PHP-FPM and switch MPM
sudo apt install -y php8.3-fpm
sudo a2dismod php8.3 mpm_prefork
sudo a2enmod mpm_event proxy_fcgi setenvif
sudo systemctl restart apache2
At this point no PHP handler is configured yet — PHP files would download as text. Don’t skip the next step on a live server.
2. Option A: one global handler
The package ships a config snippet that sends all .php files to the default pool:
sudo a2enconf php8.3-fpm
sudo systemctl reload apache2
Fine for a single-site server.
3. Option B: a pool per site (recommended for multiple sites)
Create a Linux user per site and a pool for it:
sudo adduser --system --group --home /var/www/shop shop
/etc/php/8.3/fpm/pool.d/shop.conf:
[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 = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
php_admin_value[open_basedir] = /var/www/shop:/tmp
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
sudo systemctl reload php8.3-fpm
In the site’s virtual host, route PHP to that socket:
<VirtualHost *:443>
ServerName shop.example.com
DocumentRoot /var/www/shop/public
<FilesMatch "\.php$">
SetHandler "proxy:unix:/run/php/php8.3-fpm-shop.sock|fcgi://localhost"
</FilesMatch>
<Directory /var/www/shop/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
If you enabled the global php8.3-fpm conf, the vhost’s FilesMatch takes precedence for that site. Files owned by shop are now executed as shop — a compromise of this site can’t touch other sites’ files.
4. Several PHP versions
Add versions from the Ondřej Surý repository (Ubuntu PPA ppa:ondrej/php, or packages.sury.org for Debian):
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install -y php8.2-fpm php8.2-mysql php8.2-mbstring php8.2-xml
Each version has its own service and sockets (php8.2-fpm, /run/php/php8.2-fpm.sock). Point a legacy site at 8.2 and a new one at 8.3 simply by changing the socket in SetHandler. See PHP versions on Ubuntu.
5. Verify
Create a temporary info.php in a site, open it, and check:
- Server API: FPM/FastCGI
- User: the pool user (visible via
$_SERVER['USER']orget_current_user()patterns) - PHP version: the expected one
Delete the file afterwards. From the shell:
apache2ctl -M | grep -E 'mpm|proxy_fcgi'
ls -l /run/php/
sudo systemctl status php8.3-fpm --no-pager
6. Tune
- Apache event MPM workers: see Apache performance tuning.
- PHP-FPM workers per pool, based on RAM: see PHP-FPM tuning.
- OPcache per PHP version: see PHP OPcache.
Troubleshooting
| Symptom | Cause |
|---|---|
| PHP files download | No handler: enable the conf or add SetHandler |
| 503 Service Unavailable | PHP-FPM pool down or socket path wrong — check ls /run/php/ and journalctl -u php8.3-fpm |
| Permission denied on socket | listen.owner/group doesn’t allow Apache (www-data) |
.htaccess php_value causes 500 |
Not supported with FPM — use pool settings or .user.ini |
| File upload/permission errors | Files must be writable by the pool user, not www-data |
Related: installing Apache on Ubuntu and Nginx with PHP-FPM.