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
| file | type | who can edit | reload |
|---|---|---|---|
conf/php.ini | soft — overridable | site owner | systemctl reload php<ver>-fpm |
conf/php-fpm.conf (symlink to /etc/php/<ver>/fpm/pool.d/<site>.conf) | FPM pool + hard locks | root | systemctl reload php<ver>-fpm |
Inside the pool, two prefixes determine override behavior:
php_admin_value[X] = Y/php_admin_flag[X] = on|off— locked. The site cannot override these fromphp.ini,ini_set(), or.htaccess.php_value[X] = Y/php_flag[X] = on|off— soft. Overridable byconf/php.iniand (where allowed) byini_set().
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_*)
| setting | value | why |
|---|---|---|
open_basedir | public:tmp:conf:/tmp | PHP cannot open() outside these paths. Site B is invisible to site A. |
disable_functions | exec, passthru, shell_exec, system, proc_open, popen, pcntl_exec, pcntl_fork, dl, show_source | Removes the most common LFI/RCE escalation paths. |
upload_tmp_dir | tmp/ | Uploads land inside the per-site dir, never /tmp. |
sys_temp_dir | tmp/ | Same for tmpfile() and friends. |
session.save_path | tmp/ | Sessions are per-site; never world-readable in /tmp. |
error_log | logs/php-error.log | Error log lives inside the site dir. |
expose_php | off | No X-Powered-By banner. |
allow_url_include | off | Blocks the include "http://attacker/payload" family of exploits. |
Pool — soft tunables (php_value)
| setting | default | notes |
|---|---|---|
memory_limit | 256M | Per-request memory cap. |
max_execution_time | 60 | CPU seconds, not wall clock. |
max_input_time | 60 | Wall clock for parsing input. |
post_max_size | 64M | Must be ≥ upload_max_filesize. |
upload_max_filesize | 64M | Per-file. |
max_file_uploads | 20 | Per request. |
default_socket_timeout | 30 | Affects 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.