PHP
Last updated
Multi-version PHP via Sury + per-user FPM pools.
Per-version installation
/jabali-admin/php-pools lists all PHP versions installed on the host. The installer puts Sury’s PHP repo on the system; subsequent versions can be added with apt install php8.x-fpm and friends, once installed they’re picked up by the panel.
Per-user pools
Each panel user gets a private PHP-FPM pool socket at /run/php/jabali-<user>/fpm.sock. The user must be a member of www-data (handled by useradd). Pool config lives at /etc/php/<version>/fpm/pool.d/jabali-<user>.conf, written by the agent.
Per-user pools mean:
- Per-user OPcache (no cross-tenant cache poisoning).
- Per-user
memory_limit,upload_max_filesize, etc. (managed in the panel UI under PHP Settings). - Per-user worker counts (computed from the user’s package).
Per-version extensions (M9.6)
/jabali-admin/php-pools → Extensions tab. Server-wide enable/disable for each extension on each installed PHP version. The agent’s phpext package handles install/remove via phpenmod/phpdismod, then issues a graceful FPM reload.
phpext lives at internal/phpext/ (repo root, Go internal rule, ADR-0031).
Per-domain version
Each Domain row has a php_pool_id foreign key into php_pools. The vhost fastcgi_pass line is rendered from that pool’s socket path. Change the version per-domain via Domains → Edit → PHP Version.
User-facing PHP settings
/jabali-panel/php-settings exposes:
memory_limitupload_max_filesizepost_max_sizemax_execution_timemax_input_timemax_input_varsdisplay_errors(off by default)date.timezone
The package the user is on caps each of these to a maximum the admin chose. Attempts to exceed the cap are clamped on save with a UI warning.
Performance modes
Every pool records a performance_mode — either one of the built-in presets or custom (raw tuning). Presets seed pm.* values in the pool config:
| Mode | Process manager | max_children | start | min_spare | max_spare | max_requests | For |
|---|---|---|---|---|---|---|---|
balanced | dynamic | 10 | 2 | 1 | 3 | 0 | Default |
low_memory | ondemand | 5 | 1 | 1 | 2 | 0 | Small VPS, many sites |
high_traffic | dynamic | 25 | 6 | 3 | 8 | 0 | High-load |
wordpress | dynamic | 15 | 4 | 2 | 6 | 500 | WordPress / WooCommerce |
Package-level FPM policy
Set per hosting package (/jabali-admin/packages → Edit):
| Field | Default | Effect |
|---|---|---|
fpm_max_children_cap | 20 (user), 2000 (admin) | Hard ceiling on any pool’s pm.max_children. Advanced-mode edits are clamped. |
fpm_worker_mem_mb | 64 | Advisory memory-per-worker budget shown alongside the sliders (not enforced). |
fpm_user_can_edit | false | When on, users can pick preset modes in /jabali-panel/php-settings. |
fpm_advanced_mode | false | When on, users can edit raw pm.* fields (clamped by the cap above). Switching flips the pool’s performance_mode to custom. |
php_exec_enabled | false | When on, pools on the package skip the default disable_functions blocklist. Admin-only override. |
The raw pool tuning surface (M11 phase 1b) includes pm_start_servers, pm_min_spare_servers, pm_max_spare_servers, pm_max_requests (0–100 000), and request_terminate_timeout_seconds (0–3600).
Locked-down functions
By default every pool renders with:
php_admin_value[disable_functions] = "exec, passthru, shell_exec, system, proc_open, popen, pcntl_exec, pcntl_fork, proc_nice, dl"
curl and file_get_contents stay available so the WordPress HTTP API keeps working. Only admins can lift the blocklist by setting php_exec_enabled = 1 on the package. The agent verb php.pool.apply rejects tenant-supplied disable_functions overrides — the value can only come from the package policy.
The pool renderer also refuses tenant overrides for open_basedir, extension_dir, and zend_extension, and strips values with control characters.
Per-user and per-domain settings
PHP settings (memory_limit, upload_max_filesize, opcache toggles, JIT, timezone, custom .ini directives) can be overridden at two layers:
- Per-user — Users → Edit → PHP Settings. Applies to every domain on that user unless the domain has its own override.
- Per-domain — Domains → Edit → PHP Settings. Wins over the user-level value. Useful when one domain on the account needs a bigger
upload_max_filesizethan the rest.
Both surfaces write to the same pool_ini_overrides table but scoped differently; the reconciler renders the final php.ini for each pool from admin package policy → user overrides → domain overrides in that order.
CLI:
jabali domain php-settings get <domain>
jabali domain php-settings set <domain>
OpCache + JIT
Recent PHP versions (8.3+) ship JIT. Jabali leaves JIT off by default (silent CPU spikes on some shared workloads): re-enable per-user (or per-domain) in the PHP Settings tab if you know your app benefits.
Snuffleupagus
PHP hardening (no-eval, no-include-from-uploads, taint tracking) is on by default, see security.md.
CLI
jabali php list # installed PHP versions and pool counts
jabali php install <version> # install a new PHP version
jabali php enable-ext <version> <ext>
jabali php disable-ext <version> <ext>