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_limit
  • upload_max_filesize
  • post_max_size
  • max_execution_time
  • max_input_time
  • max_input_vars
  • display_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:

ModeProcess managermax_childrenstartmin_sparemax_sparemax_requestsFor
balanceddynamic102130Default
low_memoryondemand51120Small VPS, many sites
high_trafficdynamic256380High-load
wordpressdynamic15426500WordPress / WooCommerce

Package-level FPM policy

Set per hosting package (/jabali-admin/packages → Edit):

FieldDefaultEffect
fpm_max_children_cap20 (user), 2000 (admin)Hard ceiling on any pool’s pm.max_children. Advanced-mode edits are clamped.
fpm_worker_mem_mb64Advisory memory-per-worker budget shown alongside the sliders (not enforced).
fpm_user_can_editfalseWhen on, users can pick preset modes in /jabali-panel/php-settings.
fpm_advanced_modefalseWhen on, users can edit raw pm.* fields (clamped by the cap above). Switching flips the pool’s performance_mode to custom.
php_exec_enabledfalseWhen 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_filesize than 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>