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.ini overrides.
  • 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.

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'] or get_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

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.

  • Apache
  • PHP-FPM
  • PHP