Skip to content
Muhammet Şafak
tr
Updated 1 min read

Nginx + PHP-FPM Pool Separation

The concrete benefits of defining a separate pool per application instead of a single PHP-FPM pool when running multiple PHP apps on one VPS

TL;DR

A separate PHP-FPM pool per application separates two things: filesystem access and the worker budget. What it does not separate is opcache and reload — the pools share one master, and its single reload signal takes every worker down with it. The isolation holds at runtime, not on the deploy path. Set it up knowing which of the two you bought.

Nginx + PHP-FPM Pool Separation — cover image

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.

What goes wrong with a single pool?

A single pool means:

  1. If one application exhausts pm.max_children = 50, the others can’t produce a response.
  2. A memory leak in one application drags the others’ workers into OOM too.
  3. Restarting one application (e.g. an opcache reset) restarts everyone’s workers.

Pool separation solves the first two. Not the third: PHP-FPM has no per-pool reload — the single reload signal, SIGUSR2, gracefully reloads all workers under the master.

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. opcache.validate_root = 1 doesn’t change that: it prevents name collisions in chroot’ed environments by folding the inode of / into the cache key, so what it tells apart is chroot roots, not the Nginx root paths above — and these pools have no chroot. 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.

Frequently asked

3 questions

  • Does pool separation isolate opcache between applications?

    No. The pools run under one PHP-FPM master and share its opcache. opcache.validate_root does not change that either: it exists to prevent name collisions in chrooted environments, and these pools have no chroot. Separate pools buy filesystem and resource isolation, not a separate opcode cache.

  • Can a single PHP-FPM pool be reloaded on its own?

    No. FPM has one reload signal, SIGUSR2, and it performs a graceful reload of all workers plus a reload of the FPM config. There is no per-pool signal, which is why restarting one application still recycles every other pool on the machine.

  • When is a single pool enough?

    When the applications are the same process class — two copies of one codebase, for example production and staging on different domains. Separation earns its keep when you want the applications to be operationally independent of each other.

Sources

Related

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind