FastCGI worker pools

The page processor is dual-mode. Invoked as plain CGI it behaves exactly as it always has; spawned with a FastCGI listen socket on fd 0 (the spawn-fcgi convention) it services requests from a persistent accept loop instead. Modules compile once per worker, per-request state resets inside the loop, and both paths share the same request handler - a pooled site renders pages identically to a CGI one.

What runs persistent

One pool per site serves the anonymous, visitor-facing pages - the hot path. The auth wrapper keeps its per-request CGI design, so session-bearing traffic (login, the manager, WebDAV, plugin endpoints) stays on the CGI path; the shipped vhost patterns carve those requests out to the CGI wrapper. Because DOCROOT is fixed at spawn, one pool serves exactly one site and isolation stays process-level.

Measured effect

On the reference host, as the mean of 50 cache-hit requests with one worker: 62.2 ms as plain CGI, 0.4 ms pooled. The CGI figure is almost entirely process start-up, which the accept loop amortises away.

Plain-CGI equivalence

Plain CGI remains the default, the dev-server path, and the zero-dependency fallback - its output is byte-identical to the pre-pool behaviour. FCGI.pm is lazy-required, so a CGI-only host needs no FastCGI modules at all. The pool needs libfcgi-perl (plus libfcgi-procmanager-perl for prefork), both Recommends of the lazysite-common package. Wherever no pool is configured, plain CGI keeps working untouched.

Opting a site in

On a packaged host, describe the pool in /etc/lazysite/pools/ and enable the lazysite@ template unit for the domain:

# /etc/lazysite/pools/example.com.conf
DOCROOT=/home/siteuser/web/example.com/public_html
USER=siteuser
# optional: GROUP= (default www-data), WORKERS= (default 2),
#           MAX_REQUESTS= (default 500), SOCKET=

systemctl enable --now lazysite@example.com

The unit starts the pool launcher as root; the launcher binds /run/lazysite/example.com.sock (mode 0660, connectable by the web server group), drops privileges to the site user - USER=root is refused - and execs the processor with the socket on fd 0. Root never remains in the request path and nothing writes into the site tree.

Then point the web server at the socket. Commented vhost-fcgi examples ship in the lazysite-apache and lazysite-nginx packages; for Apache mod_proxy_fcgi the core of it is:

SetHandler "proxy:unix:/run/lazysite/example.com.sock|fcgi://localhost/"

On HestiaCP, lazysite-hestia-domain add USER DOMAIN --fcgi writes the pool config, enables lazysite@DOMAIN and selects the lazysite-fcgi web template in one step.

Tuning

  • WORKERS - prefork size via FCGI::ProcManager; the pool launcher defaults to 2. With WORKERS=0 a single worker runs, exits after MAX_REQUESTS requests, and systemd's Restart= respawns it.
  • MAX_REQUESTS - a worker recycles after this many requests (default 500); memory hygiene for long-lived processes.

Edit the pool file, then systemctl restart lazysite@<domain>.