PHP & PHP-FPM

Every site gets its own PHP-FPM pool listening on a unix socket inside the site's tmp/ directory. Pool config is split into two parts: soft settings the site owner can edit, and hard locks only root can change.

Two files, two layers

filetypewho can editreload
conf/php.inisoft — overridablesite ownersystemctl reload php<ver>-fpm
conf/php-fpm.conf (symlink to /etc/php/<ver>/fpm/pool.d/<site>.conf)FPM pool + hard locksrootsystemctl reload php<ver>-fpm

Inside the pool, two prefixes determine override behavior:

tulixhost uses php_admin_* for security-critical settings (open_basedir, disable_functions, error logs, session paths, expose_php, allow_url_include) and php_value for tunable performance settings.

Pool — process manager

pm                  = dynamic
pm.max_children     = 20
pm.start_servers    = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests     = 500
pm.process_idle_timeout = 30s

Tune for your traffic. A rough guide for pm.max_children:

(available_ram_mb − reserved) / avg_worker_mb

Where avg_worker_mb is what you observe in fpm-status after a few hours of real traffic. pm.max_requests = 500 recycles workers periodically to bound memory fragmentation from extensions with known leaks.

Pool — status endpoints

Two paths are reserved by the per-site nginx vhost and proxied to FPM, but accessible only from 127.0.0.1 / ::1:

/fpm-status   (key=value summary of pool state)
/fpm-ping     (returns "pong" — for monitoring)

From the server itself:

curl --unix-socket /data/web/example.com/tmp/php-fpm.sock \
     http://localhost/fpm-status?full

Pool — security locks (php_admin_*)

settingvaluewhy
open_basedirpublic:tmp:conf:/tmpPHP cannot open() outside these paths. Site B is invisible to site A.
disable_functionsexec, passthru, shell_exec, system, proc_open, popen, pcntl_exec, pcntl_fork, dl, show_sourceRemoves the most common LFI/RCE escalation paths.
upload_tmp_dirtmp/Uploads land inside the per-site dir, never /tmp.
sys_temp_dirtmp/Same for tmpfile() and friends.
session.save_pathtmp/Sessions are per-site; never world-readable in /tmp.
error_loglogs/php-error.logError log lives inside the site dir.
expose_phpoffNo X-Powered-By banner.
allow_url_includeoffBlocks the include "http://attacker/payload" family of exploits.

Pool — soft tunables (php_value)

settingdefaultnotes
memory_limit256MPer-request memory cap.
max_execution_time60CPU seconds, not wall clock.
max_input_time60Wall clock for parsing input.
post_max_size64MMust be ≥ upload_max_filesize.
upload_max_filesize64MPer-file.
max_file_uploads20Per request.
default_socket_timeout30Affects file_get_contents() over HTTP.

OPcache + JIT

php_value[opcache.enable]                = 1
php_value[opcache.memory_consumption]    = 128
php_value[opcache.interned_strings_buffer] = 16
php_value[opcache.max_accelerated_files] = 10000
php_value[opcache.revalidate_freq]       = 2
php_value[opcache.validate_timestamps]   = 1
php_value[opcache.save_comments]         = 1
php_value[opcache.fast_shutdown]         = 1
php_value[opcache.jit]                   = tracing
php_value[opcache.jit_buffer_size]       = 64M

For pure production, set opcache.validate_timestamps = 0 — PHP will never check whether files changed on disk, eliminating stat() calls. You must then reload PHP-FPM after every deploy.

JIT in tracing mode helps CPU-bound code (image processing, regex, math). For typical PHP web apps that spend most time in DB calls, the speedup is marginal — but the 64MB buffer is cheap enough to leave on. Set opcache.jit = off if you suspect a JIT-related bug (the JIT has been mostly stable since PHP 8.2, but it does happen).

Live OPcache numbers are visible in the dashboard's PHP tab: memory used, hit rate, OOM restarts.

Editing the soft php.ini

The site owner can edit conf/php.ini to bump memory_limit, change upload_max_filesize, set a different timezone, etc. After saving:

sudo systemctl reload php8.4-fpm   # (or whatever PHP version is installed)

To verify the new value is live, hit the dashboard's PHP tab — it queries ini_get() inside the running pool.

Common edits

Bigger file uploads

Edit conf/php.ini:

post_max_size       = 512M
upload_max_filesize = 512M
max_file_uploads    = 50

You also need to allow nginx to accept the body. Edit conf/nginx-extra.conf:

client_max_body_size 512m;

Long-running requests (export jobs, large reports)

Edit conf/php-fpm.conf (root):

request_terminate_timeout = 600s

And in conf/php.ini:

max_execution_time = 600

Disabling a function the site needs

The disable_functions list is hard-locked. To allow, e.g., shell_exec for a specific site, edit conf/php-fpm.conf directly:

php_admin_value[disable_functions] = exec,passthru,system,proc_open,popen

Then reload PHP-FPM. We do not recommend doing this. The functions are disabled for a reason.