Running two, three, five PHP applications on a VPS — perfectly normal in the small-SaaS world. In a default install they all share a single www.conf pool. This usually works — until it doesn’t.
The problem with a single pool
A single pool means:
- If one application exhausts
pm.max_children = 50, the others can’t produce a response. - A memory leak in one application drags the others’ workers into OOM too.
- Restarting one application (e.g. an opcache reset) restarts everyone’s workers.
Pool separation solves all three at once.
Config per pool
/etc/php/8.4/fpm/pool.d/billing.conf:
[billing]
user = billing
group = billing
listen = /run/php/billing.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500
request_terminate_timeout = 30s
catch_workers_output = yes
decorate_workers_output = no
clear_env = no
php_admin_value[memory_limit] = 256M
php_admin_value[error_log] = /var/log/php-fpm/billing-error.log
/etc/php/8.4/fpm/pool.d/notify.conf:
[notify]
user = notify
group = notify
listen = /run/php/notify.sock
; ...
pm.max_children = 8 ; lighter application
php_admin_value[memory_limit] = 128M
We gain two things:
- Filesystem isolation — with
user = billing, the billing app can’t write to notify’s files. - Resource isolation — if billing’s max_children is exhausted, notify still runs.
On the Nginx side
Each vhost connects to its own socket:
server {
server_name billing.example.com;
root /var/www/billing/public;
location ~ \.php$ {
fastcgi_pass unix:/run/php/billing.sock;
# standard fastcgi params
}
}
server {
server_name notify.example.com;
root /var/www/notify/public;
location ~ \.php$ {
fastcgi_pass unix:/run/php/notify.sock;
}
}
opcache: shared or separate?
The PHP-FPM master process shares opcache — so opcache is not automatically isolated between pools. But if you use opcache.validate_root = 1 and different root paths, there’s no collision. In practice: same master, separate pools is enough.
Monitoring
Expose the /status endpoint on the pool:
pm.status_path = /status
Then on the Nginx side, restrict it to localhost only:
location ~ ^/status$ {
access_log off;
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/billing.sock;
# ...
}
With curl localhost/status you can pull metrics like active workers, queue length, and slow request count, and monitor them with a Prometheus exporter or a simple cron.
When is a single pool enough?
If you’re running two copies of the same codebase (e.g. app.example.com and staging.example.com), a separate pool is unnecessary. Different domains but the same process class. Separation is worth it when you want applications to be operationally independent.