Author SHA1 Message Date
Claude 433e74975c fix(cac-lsphp): enable .user.ini support (LSPHP_ENABLE_USER_INI)
php-lsapi compiles .user.ini support in but leaves it DISABLED by default.
sapi/litespeed/lsapi_main.c has `static int parse_user_ini = 0;` and only
sets it when the process environment contains LSPHP_ENABLE_USER_INI=on.
WHP never set it, so lsphp never entered the user-ini chain at all.

The failure was silent: phpinfo() still reports user_ini.filename=.user.ini
and user_ini.cache_ttl=300, because those are core INI defaults that are
simply inert under this SAPI. Every other WHP PHP tier (cac, cac-fpm,
cac-litespeed) honors .user.ini, so shared_ols was quietly inconsistent.

Impact found in production (whp01/whp02/sdbees/TrueSelfCA, 29 sites):
  - Per-site memory_limit / max_input_vars overrides were ignored. A Divi
    site's max_input_vars stayed at the 2000 default while its .user.ini
    asked for 20000.
  - Wordfence's auto_prepend_file WAF never loaded on ANY shared_ols site.
    11 sites had the plugin installed and reporting "Extended Protection"
    enabled while the prepend was never executed.

Verified on a live sidecar (shadowdao.com, whp01) before this commit by
injecting the env var via whp.container_types.startup_env and recreating:
  before: auto_prepend_file=/scripts/cac-lsphp-normalize.php   (WAF absent)
  after:  auto_prepend_file=/home/shadowdao/public_html/wordfence-waf.php
          wordfence-waf.php present in get_included_files()
          class_exists('wfWAF') === true
The platform normalize prepend still chains in behind Wordfence's bootstrap,
so DOCUMENT_ROOT canonicalisation is not lost.

Set in two places on purpose: the Dockerfile ENV makes the value visible in
`docker inspect` and survives an entrypoint override, and the entrypoint
re-exports it with the same default because the runuser fallback exec path
resets the environment. Still overridable per-container
(LSPHP_ENABLE_USER_INI=off) as an escape hatch for a site whose legacy
cPanel-generated .user.ini has not been remediated yet.

NOTE: enabling this activates every previously-inert .user.ini at once.
Audit the fleet for stale cPanel directives before rolling this image —
session.save_path values under /var/cpanel/ that do not exist in the
container, memory_limit above the cgroup cap, and upload_max_filesize
values below the platform default were all found and remediated first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 15:23:05 -07:00
jknapp cc72fda741 Merge pull request 'fix(shared-ols): never cache logged-in pages (disable tier private cache)' (#20) from fix/shared-ols-no-logged-in-cache into trunk
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m22s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m17s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m17s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m17s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m19s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m16s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m25s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m11s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 1m57s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m22s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 34s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 1m12s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 46s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 36s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 1m20s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 1m24s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 30s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 30s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 30s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 29s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 27s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 27s
Reviewed-on: #20
2026-06-27 04:04:49 +00:00
shadowdaoandClaude Opus 4.8 a865a13940 fix(shared-ols): never cache logged-in pages (disable tier private cache)
OLS tier had enablePrivateCache=1 + checkPrivateCache=1 at module scope on the
assumption that with no LiteSpeed Cache WP plugin nothing would be cached. In
practice OLS privately cached logged-in / cookie-bearing responses regardless of
the plugin, serving stale wp-admin pages for the full privateExpireInSeconds TTL
(observed: a WordPress 'automated update failed' nag persisting after the cause
was cleared).

Disable private caching at the tier (enablePrivateCache 0 + checkPrivateCache 0)
so logged-in pages are always served fresh. Public/anonymous caching is unchanged
(enableCache 1 + checkPublicCache 1), still honored from the plugin's
X-LiteSpeed-Cache-Control headers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:58:23 -07:00
shadowdaoandClaude Opus 4.8 8dbfdf599a fix(shared-ols): useIpInProxyHeader 2->1 so real client IP reaches lsphp
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m26s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m24s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m20s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m22s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m21s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m20s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m19s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m20s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 1m22s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m19s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 56s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 34s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 30s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 27s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 25s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 26s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 26s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 28s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 26s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 25s
Mode 2 ("trusted IP only") extracts the real client IP from X-Forwarded-For
ONLY when the connecting peer is in a TRUSTED access-control list — which this
tier never configured (accessControl is `allow ALL`, no trusted designation).
So OLS kept HAProxy's container IP (172.18.0.34) as REMOTE_ADDR for EVERY
request across ALL tenants. WP security plugins (Wordfence etc.) then saw all
traffic as one IP; blocking it locked every site — and the admin — out.

HAProxy already sends X-Forwarded-For and is the ONLY peer that connects to
this tier (client-net, no host-published ports), and it OVERWRITES XFF with
%[src] (set-header), so spoofing is impossible. Mode 1 (always trust XFF) is
correct and safe here — it matches the working standalone configs/litespeed
config which has always used 1.

Verified on whp01: lsphp now receives the forwarded client IP end-to-end
(REMOTE_ADDR=<real-ip>, was 172.18.0.34). Live-hotpatched whp01+whp02 pending
this image rebuild.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 07:58:07 -07:00
jknapp 2e85f458d3 Merge pull request 'feat: OLS tier images — cac-lsphp (detached lsphp) + shared-ols' (#19) from feature/cac-lsphp-image into trunk
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m22s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m15s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m18s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m24s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m23s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m16s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m22s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m18s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 2m17s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m18s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m22s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m15s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m13s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 1m8s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 47s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 30s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 31s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 1m11s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 1m30s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 35s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 29s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 51s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 59s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 45s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 1m3s
Reviewed-on: #19
2026-06-10 16:56:38 +00:00
shadowdaoandClaude Opus 4.8 08f35032c5 fix(shared-ols): re-review hardening — bounded flock + stale-tmp sweep
Follow-up to the review fixes, from a second review pass:
- flock now uses -w 30 (bounded wait) so a hung render can't block the panel's
  docker-exec (and the site-save request) indefinitely; the dead-code timeout
  error path is now reachable.
- sweep stale .httpd_config.conf.tmp.* left by a prior SIGKILL (trap EXIT doesn't
  run on SIGKILL); safe under flock since each render uses a unique $$ suffix.
Verified: render still produces a valid config + serves; stale tmp is swept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:25:05 -07:00
shadowdaoandClaude Opus 4.8 6bb494c72f fix(shared-ols): review fixes — watcher starvation, atomic render, O(N) chown, safe meta parse
Addresses the local code-review on the OLS-tier images:
- [HIGH] ols-htaccess-watcher.sh: the debounce drain read ALL inotify events
  unfiltered, so on a busy multi-tenant server it never timed out and the
  restart was STARVED (rewrite changes silently never applied). Now coalesces
  with a hard DEBOUNCE-bounded window. Verified under continuous noise.
- [HIGH] render-shared-ols-config.sh: built httpd_config.conf in-place across
  several appends, so a concurrent OLS restart (watcher) or parallel render
  could read a half-written config and 503 the whole tier. Now flock-serialized,
  built in a temp file and atomically moved into place; refuses to publish empty.
- [MED] render + entrypoint: replaced recursive chown of the whole conf tree
  (O(N-sites) on every single-site change / boot) with a targeted chown of just
  the file written.
- [MED] render: parse site.meta with sed instead of sourcing it (do not execute
  panel-written data as shell).
- [cleanup] removed the unused configs/shared-ols/vhconf.tpl (the panel copy is
  the single source; the image never read it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 08:35:26 -07:00
shadowdaoandClaude Opus 4.8 7552760ba0 fix(cac-lsphp): normalize $_SERVER DOCUMENT_ROOT/SCRIPT_FILENAME to /home
The symlink makes __FILE__/__DIR__/realpath/getcwd report /home/<user>/public_html
(WordPress/frameworks), but $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] are raw
env vars OLS sets to its /mnt/users view — apps that build/compare paths from
them would see /mnt/users. Added a tiny auto_prepend (cac-lsphp-normalize.php,
wired via a scan-dir ini) that realpath-canonicalises those two back to /home.
Customer sites have no auto_prepend by default, so no conflict.

Verified clean-room (committed image, fresh boot): DOCUMENT_ROOT and
SCRIPT_FILENAME both report /home/<user>/public_html through the shared OLS.
Now byte-for-byte 1:1 with cac-fpm/cac-litespeed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 07:02:54 -07:00
shadowdaoandClaude Opus 4.8 fc65b68bd6 fix(cac-lsphp): mount docroot at /home/$user + symlink for true 1:1 compatibility
Customer concern: sites with /home/<user>/public_html baked into config or the
DB must keep working — a changed in-container docroot path would break WordPress
ABSPATH, hardcoded includes, cached absolute paths, etc., making the upgrade a
non-drop-in.

Fix: the sidecar now mounts the docroot at /home/$user (IDENTICAL to
cac-fpm/cac-litespeed) and the entrypoint symlinks /mnt/users/<user>/<domain> ->
/home/$user. OLS still serves from its bulk /mnt/users mount and sends lsphp
that path (no remap available), but the symlink resolves it to the real
/home/$user files AND PHP canonicalises it — so __FILE__/__DIR__/realpath/ABSPATH
all report /home/<user>/public_html.

Verified end-to-end through the shared OLS: a request reports
__FILE__=/home/homeuser/public_html/probe.php, ABSPATH=/home/homeuser/public_html/,
and stored /home paths resolve. True 1:1 drop-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 06:54:28 -07:00
shadowdaoandClaude Opus 4.8 e99b8cb2d1 fix(cac-lsphp): entrypoint operates on the /mnt/users docroot, not /home/$user
Code-review integration fixes:
- entrypoint-lsphp.sh: the shared-ols tier mounts the docroot at
  /mnt/users/<user>/<domain> (NOT /home/$user). Discover the mount via glob
  (one site per sidecar; wildcard-safe), create public_html + logs/php-fpm under
  it (so OLS docRoot exists), point lsphp error_log there, and chown just those
  dirs. Verified: sidecar creates public_html under the mount, runs as the
  per-site user, OLS serves PHP (SAPI=litespeed) end-to-end.
- shared-ols vhconf.tpl: per-vhost logs -> /usr/local/lsws/logs/<vhname>.* (the
  shared-ols container has no /home/<user>).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 06:42:31 -07:00
shadowdaoandClaude Opus 4.8 19db8f170a feat(shared-ols): shared OpenLiteSpeed tier image (webserver-only, fronts cac-lsphp sidecars)
One OLS container fronting many tenants' detached cac-lsphp sidecars — the
OLS analogue of shared-httpd. Runs NO PHP locally; every site's PHP goes to
its own sidecar over LSAPI (extProcessor type lsapi, address <sidecar>:9000).

Key design fact (established by PoC): OLS has NO top-level 'include' directive,
so render-shared-ols-config.sh assembles httpd_config.conf from the panel's
per-site files (vhconf.conf + site.meta) at boot and on every change — the
'include' OLS lacks. Per-site detail uses the OLS-native configFile +
vhost-scoped extprocessor model. LSCache is module-level (a configFile-loaded
vhost rejects a bare cache{} block); the WP LiteSpeed plugin controls
cacheability via X-LiteSpeed-Cache-Control headers.

- Dockerfile.shared-ols: litespeed base + inotify-tools/envsubst/openssl,
  admin bound to loopback, :80/:443 self-signed, healthz HEALTHCHECK.
- entrypoint-shared-ols.sh: cert + health vhost + render + watcher, then
  daemon-mode OLS supervision (reused from cac-litespeed so self-restarts
  don't kill PID 1).
- render-shared-ols-config.sh: strip stock (incl local lsphp) + append base +
  per-site stanzas + listeners with all maps + catch-all health vhost.
- ols-htaccess-watcher.sh: inotify debounce+floor -> lswsctrl restart (spec 5.3).
- configs/shared-ols/{httpd_config_base,vhconf}.tpl.
- CI: Build-Shared-OLS job.

Verified locally end-to-end: zero-site boot healthy on :443; add site via the
panel contract -> Host-routed to the right sidecar (SAPI=litespeed); real
client IP + HTTPS behind X-Forwarded headers; LSCache miss->hit; .htaccess
change triggers graceful restart; unknown Host hits health catch-all (200).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 01:22:14 -07:00
shadowdaoandClaude Opus 4.8 19092911a3 feat(cac-lsphp): detached lsphp (LSAPI) site image for the shared-ols tier
New slim per-site PHP backend that runs 'lsphp -b 0.0.0.0:9000' (detached
LSAPI) and nothing else — the LiteSpeed analogue of cac-fpm, sitting behind
a shared OpenLiteSpeed container. Built on the same litespeedtech prebuilt
base as cac-litespeed so the lsphp runtime/extensions are identical.

- Dockerfile.lsphp: base + lsphpNN-ldap parity, reuses shared lsphp-overrides.ini,
  exposes only :9000, no webserver started (guaranteed by entrypoint, not by
  stripping OLS binaries).
- entrypoint-lsphp.sh: same uid/user contract + /home/$user/logs layout +
  ini drop-in mechanism as entrypoint-litespeed.sh; sizes PHP_LSAPI_CHILDREN
  from container memory (detect-memory-lsphp.sh) with panel override precedence;
  execs lsphp -b as the per-site user via setpriv (PID 1).
- detect-memory-lsphp.sh: LSAPI_CHILDREN sizing, no OLS daemon reserve.
- healthcheck-lsphp.sh: TCP :9000 + lsphp-alive (LSAPI isn't FastCGI).
- CI: Build-LSPHP-Images job, php81-85 matrix, OLS 1.8.4, cac-lsphp:phpNN.

Verified locally: builds php83+php85; sidecar runs lsphp as the per-site
user (uid 61045) as PID 1, healthcheck green, and a real shared OLS in front
serves PHP over LSAPI (HTTP 200, SAPI=litespeed) with identical docroot path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:28:34 -07:00
shadowdaoandClaude Opus 4.8 50202538e4 cac-litespeed: supervise OLS in daemon mode so self-restarts don't kill PID 1
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m24s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m22s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m17s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m25s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m21s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m15s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m23s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m15s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 1m33s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 1m19s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m24s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 30s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 31s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 31s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 32s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 28s
cac-litespeed containers were dying at random intervals and staying 503 until
manually restarted. Root-caused on whp02 (alsacorp, 2026-06-06): the LiteSpeed
Cache / QUIC.cloud integration refreshes the QUIC.cloud IP allowlist on a
schedule and, when it changes, sends SIGUSR1 → "request a graceful server
restart". The entrypoint ran `openlitespeed -n & wait "$OLS_PID"`, so when the
OLD main PID exited after the zero-downtime handoff, `wait` returned, PID 1
(bash) exited, and the whole container went down. The exit was clean (code 0),
so even a restart policy wouldn't reliably catch it — HAProxy just served 503
until someone ran `docker start`.

Replace the `-n` foreground+wait model with a daemon-mode supervisor: start OLS
via `lswsctrl start` (its native model, where it owns the SIGUSR1 handoff and
keeps listeners bound across generations) and have PID 1 follow `lswsctrl
status`. A graceful self-restart is now invisible here (verified zero-downtime);
PID 1 only relaunches on a genuine crash (no live main), with a 5-in-60s
crash-loop cap that bails out to Docker's restart policy / the site monitor.
SIGTERM still drains and exits cleanly for docker stop / recreate.

Verified on a scratch php85 container: survives `lswsctrl restart`, survives a
raw SIGUSR1 to the main (the exact QUIC.cloud path that used to kill it),
relaunches after `kill -9` of the main, and stops cleanly in ~6s on docker stop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:15:25 -07:00
shadowdaoandClaude Opus 4.8 2837d40f00 cac-litespeed: forward real client IP to logs and PHP behind HAProxy
Cloud Apache Container / Build-and-Push (74) (push) Successful in 4m47s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m18s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m17s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m17s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m20s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m16s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m6s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 2m14s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m16s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 2m21s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 1m19s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m13s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 35s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 45s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 1m9s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 30s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 31s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 26s
OLS had no equivalent of the Apache cac:phpNN mod_remoteip wiring
(configs/remote_ip.conf + RemoteIPInternalProxy), so every migrated
LiteSpeed site logged HAProxy's docker-bridge IP and handed that same
internal IP to lsphp as $_SERVER['REMOTE_ADDR']. That silently broke
traffic analytics, WP security plugins, brute-force detection, Coraza
source-IP correlation, geo, and rate-limiting.

Add server-level `useIpInProxyHeader 1` to the httpd_config append
fragment. OLS then rewrites the remote IP from X-Forwarded-For for both
logging and the LSAPI REMOTE_ADDR before PHP sees it. Value 1 mirrors the
Apache trust model (container is only reachable via HAProxy, never bound
publicly). Confirmed HAProxy customer backends are mode http with
`option forwardfor` and set X-Forwarded-For to the resolved real client IP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:51:25 -07:00
shadowdaoandClaude Opus 4.7 cfdaae116a tune(litespeed): bump opcache 32→64 MB / 4000→8000 files + add per-site override
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m37s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m42s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m50s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m51s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 3m18s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m21s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 3m49s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m0s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m44s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 1m30s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m48s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 1m40s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 1m58s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m15s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 29s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 26s
32M/4000 was too aggressive for heavy WP+Divi+WC sites: 3000+4000 unique
PHP files each blow through max_accelerated_files, causing constant
eviction + recompilation thrash. Manifested 2026-06-03 as ~40% sustained
CPU on alphaoneaminos and 5378 oom_kills/9h on brain-jar.

64M/8000 fits Divi + WC + WP core bytecode without eviction. N lsphp ×
64 MB ≈ 512 MiB shmem worst case — still under the per-instance setUIDMode
fan-out from the original 128M problem (which was 1+ GiB).

Per-site override (OPCACHE_MEMORY_MB / OPCACHE_MAX_FILES env vars) lets the
panel push down for low-traffic sites or up for outliers without rebuilding
the image. WHP panel UI ships in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 06:21:37 -07:00
shadowdaoandClaude Opus 4.7 87f154cdc8 refactor(litespeed): drop setUIDMode for shared lsphp + cut opcache 128→32M
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m19s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m35s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m16s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m29s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m2s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m15s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m22s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m30s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m14s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m6s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 2m20s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 3m20s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m19s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m41s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 43s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 1m16s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 56s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 2m2s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 51s
OLS runs as the customer user end-to-end (server-level user/group set by
create-vhost-litespeed.sh), so lsphp inherits that uid without per-request
suEXEC. Eliminates the per-httpd-worker lsphp instance fan-out — one shared
lsphp parent now serves all httpd workers via the shared socket.

Combined with opcache.memory_consumption 128→32M, brain-jar measured shmem
dropped from ~880 MiB → 32 MiB and memory.current from ~1.1 GiB → 67 MiB
at the 1.5 GiB cap. No new oom_kills since the change.

Safe because cac-litespeed is one-customer-per-container — the container
boundary is the privsep boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 20:06:56 -07:00
shadowdaoandClaude Opus 4.7 f463519998 tune(litespeed): bump LSPHP_WORKER_ESTIMATE_MB 115 → 130
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m33s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m24s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 2m8s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m23s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m21s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m23s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m21s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m14s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 3m26s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 2m16s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m22s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m1s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m28s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 1m30s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 39s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 1m12s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 30s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 30s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 1m30s
115 was set from idle-state per-worker memory. Active workers on
heavy WP/Divi grow to ~130-150 MB (shmem + anon + file), and the
115 formula gave brain-jar.com CHILDREN=8 at 1 GiB — which produced
142 OOM-kills overnight because there was zero headroom once page
renders started.

130 backs off slightly on the bigger sites:
  512 MiB:  3 workers  (unchanged)
  1 GiB:    7 workers  (was 8 — brain-jar's failure point)
  1.5 GiB:  11 workers (was 12)
  2 GiB:    15 workers (was 17)
  4 GiB:    30 workers (was 33)

Per-site FPM_MAX_CHILDREN override still wins for sites that need
tighter caps regardless of formula default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:54:28 -07:00
shadowdaoandClaude Opus 4.7 03cca745f7 feat(litespeed): wire up dynamic LSAPI tuning + idle reduction
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m18s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m14s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 3m21s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m18s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m15s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m11s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m22s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 4m22s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 3m46s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m21s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m15s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m21s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 3m29s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 31s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 31s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 30s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 32s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 31s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 1m33s
Two correctness fixes and a tuning improvement.

CORRECTNESS:
1. Strip the stock 'extProcessor lsphp' from httpd_config.conf before
   appending ours. Previously the stock block (hard-coded
   PHP_LSAPI_CHILDREN=10 regardless of container memory) always won
   because our APPEND fragment didn't include an extProcessor block.
   detect-memory-litespeed.sh was computing LSAPI_CHILDREN but never
   plumbing it anywhere — silent dead code.

2. Bump LSPHP_WORKER_ESTIMATE_MB from 96 → 115 per the 2026-06-02
   memory-sizing finding (vantagehealth OOM-spawn loop). Each lsphp
   carries ~115 MB shmem-rss accounted per worker. 115 MB matches the
   real per-worker baseline.

TUNING (idle reduction, the original ask):
- LSAPI_MAX_IDLE_CHILDREN=2  (was CHILDREN/2 = 5 default)
- LSAPI_MAX_IDLE=60s         (was 300s default)
- PHP_LSAPI_MAX_REQUESTS=500 (recycle workers, prevents bloat)
- memSoftLimit=1024M / memHardLimit=1500M per worker (RLIMIT_AS;
  catches runaway scripts at the worker level, cgroup still backstops
  the container)

Effective LSAPI_CHILDREN per container:
  2 GiB → ~17 (was 10 — brain-jar was saturating)
  1 GiB → ~8
  512 MiB → ~3 (cap-marginal per the memory note; bump container if
                site grows)

Dropped LSAPI_MEM_SOFT/HARD computation in detect-memory: AVAILABLE/CHILDREN
was conflating VSZ with RSS-budget arithmetic and would have killed
legitimate workers. The 1024/1500 hard-coded values in the template
comfortably fit typical Divi/WooCommerce VSZ (280-365 MB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 16:36:25 -07:00
shadowdaoandClaude Opus 4.7 d1c3cfadc0 feat(litespeed): make log paths drop-in compatible with cac:phpNN
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m35s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m20s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m18s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m13s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m21s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m22s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m19s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m14s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 2m25s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m26s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 2m15s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m15s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m58s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m27s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 30s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 29s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 33s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 1m27s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 24s
OLS now writes:
  access -> /home/$user/logs/apache/access_log
  error  -> /home/$user/logs/apache/error_log
  PHP    -> /home/$user/logs/php-fpm/error.log

Matches the cac:phpNN bundled image convention exactly, so existing WHP
log-gathering code (whp-traffic-aggregator.php, process-log-review.php)
works for migrated sites without any panel-side changes. Customer-facing
paths are stable across migrations — "where do I find my access log?"
gets the same answer regardless of image family.

Server-level OLS logs (/usr/local/lsws/logs/) are unchanged — those are
internal diagnostics, not customer-relevant.

PHP error_log is set via a runtime-rendered tiny ini in lsphp's scan dir
(can't be in the static lsphp-overrides.ini because the path is
per-customer).

Customers on the four whp01 migrations (alphaone, peptides, shadowdao,
brain-jar) need a container recreate after CI publishes the new tags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 10:53:44 -07:00
shadowdaoandClaude Opus 4.7 80fa06592b perf(litespeed): defer mariadb-server + memcached install to DEV runtime
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m22s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m23s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m58s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m0s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m14s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m12s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m24s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m44s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m41s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 3m33s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 2m18s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m17s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m21s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m16s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 1m19s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 46s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 31s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 1m26s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 52s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 58s
Drops these from the build-time apt install in Dockerfile.litespeed; they
now install at entrypoint time only when environment=DEV, guarded by
'command -v mysqld' so container restarts skip the apt step.

Mirrors the cac:phpNN pattern. The mysql CLI client is already in the
litespeedtech/openlitespeed base, so wp-cli + DEV creds-bootstrap still work
without a build-time client install.

Measured (php83 / OLS 1.8.4):
  PROD image: 1.64 GB -> 1.20 GB (~440 MB savings)
  PROD first-200 boot: unchanged at ~1.5s
  DEV first boot:  ~51s (apt install cost — one-time per container)
  DEV second boot: ~6s (cache hit, same as PROD)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 08:26:19 -07:00
18 changed files with 1185 additions and 50 deletions
+74
View File
@@ -117,6 +117,47 @@ jobs:
repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }} repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:latest' || '' }} ${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:latest' || '' }}
Build-LSPHP-Images:
runs-on: ubuntu-latest
strategy:
matrix:
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
# backend for the shared-ols tier and shares the litespeed prebuilt
# base, which only ships lsphp for 8.1+. Keep this matrix in lockstep
# with Build-LiteSpeed-Images.
phpver: [81, 82, 83, 84, 85]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }}
- name: Build and Push lsphp Image
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.lsphp
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
OLS_VERSION=1.8.4
# OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same
# prebuilt base, same lsphp binaries. Bump both together.
tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:latest' || '' }}
Build-Shared-httpd: Build-Shared-httpd:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -144,3 +185,36 @@ jobs:
push: true push: true
tags: | tags: |
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
Build-Shared-OLS:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }}
- name: Build and Push Shared OLS Image
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.shared-ols
platforms: linux/amd64
push: true
# Single image (runs no PHP). PHPVER just selects the OLS base tag;
# pinned to 83 / OLS 1.8.4 to match the rest of the litespeed family.
build-args: |
PHPVER=83
OLS_VERSION=1.8.4
tags: |
repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest
+8 -3
View File
@@ -31,10 +31,16 @@ ENV PHPVER=${PHPVER}
## - sudo: install-lscache-wp.sh runs wp-cli as the customer user ## - sudo: install-lscache-wp.sh runs wp-cli as the customer user
## - composer: not in the base image (wp-cli is) ## - composer: not in the base image (wp-cli is)
## - cron: customer crontab support (mirror cac:phpXX behaviour) ## - cron: customer crontab support (mirror cac:phpXX behaviour)
## - mariadb-server + memcached: DEV-mode parity with the existing CAC
## entrypoints. PROD mode never starts these.
## - lsphp83-ldap: not in base image, useful for some WP plugins ## - lsphp83-ldap: not in base image, useful for some WP plugins
## ##
## NOTE: mariadb-server + memcached were previously installed here for
## DEV-mode parity but bloated the PROD image by ~500MB. They are now
## installed at runtime by entrypoint-litespeed.sh ONLY when
## environment=DEV, mirroring the cac:phpNN pattern. The mysql CLI
## client (used by the DEV creds-bootstrap and by wp-cli) is already
## present in the litespeedtech/openlitespeed base via the mysql-client
## package, so no client-side install is needed at build time.
##
## All apt cache is cleaned in the same layer to keep image size down. ## All apt cache is cleaned in the same layer to keep image size down.
## lsphp${PHPVER}-ldap is the only extra ext we add (everything else WP needs ## lsphp${PHPVER}-ldap is the only extra ext we add (everything else WP needs
## ships in the prebuilt base). lsphp84 + lsphp85 don't ship imap or pspell ## ships in the prebuilt base). lsphp84 + lsphp85 don't ship imap or pspell
@@ -43,7 +49,6 @@ RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gettext-base sudo cron \ gettext-base sudo cron \
ca-certificates curl wget \ ca-certificates curl wget \
mariadb-server memcached \
lsphp${PHPVER}-ldap && \ lsphp${PHPVER}-ldap && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
+69
View File
@@ -0,0 +1,69 @@
## cac-lsphp — per-site DETACHED lsphp (LSAPI) backend for the shared-ols tier.
##
## The LiteSpeed analogue of cac-fpm: a slim, single-tenant PHP backend that
## runs `lsphp -b 0.0.0.0:9000` (detached LSAPI mode) and NOTHING ELSE — no
## webserver. The shared OpenLiteSpeed container (shared-ols) sits in front and
## reaches this over the docker network via an extProcessor of type lsapi,
## address <this-container>:9000 — structurally identical to how shared-httpd
## reaches a cac-fpm container's php-fpm on :9000.
##
## Built on the SAME LiteSpeed prebuilt base as cac-litespeed so the lsphp
## binary + extension set are byte-for-byte the runtime customers already get
## on the litespeed tier (memcached, redis, imagick, mbstring, mysqlnd, intl,
## gd, soap, bcmath, gmp, sodium, opcache, ... + lsphpNN-ldap added below).
## We do NOT strip the bundled OpenLiteSpeed binaries: the "no webserver"
## guarantee comes from the ENTRYPOINT (it only ever execs lsphp), and deleting
## OLS files from the upstream image risks breaking lsphp's shared libs for no
## real benefit. Only :9000 is EXPOSEd, and OLS is never started.
##
## See the design spec + PoC: whp docs/superpowers/plans/2026-06-09-ols-lsphp-tier.md
## and the LSAPI path-parity finding (feedback_ols_lsapi_no_script_filename_remap).
ARG OLS_VERSION=1.8.4
ARG PHPVER=83
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
ARG PHPVER=83
ENV PHPVER=${PHPVER}
## Match the cac-litespeed extension surface exactly: the only ext the prebuilt
## base lacks is lsphpNN-ldap. setpriv (util-linux) is already on the Ubuntu
## base; we add nothing else the sidecar doesn't need. All apt cache cleaned in
## the same layer to keep the image small.
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
lsphp${PHPVER}-ldap && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
## Scripts + the SHARED production lsphp ini (reused verbatim from the litespeed
## image — same runtime, same tuning). Scripts layer last (they change most).
COPY ./scripts/entrypoint-lsphp.sh \
./scripts/detect-memory-lsphp.sh \
./scripts/healthcheck-lsphp.sh \
./scripts/cac-lsphp-normalize.php \
/scripts/
RUN chmod +x /scripts/entrypoint-lsphp.sh /scripts/detect-memory-lsphp.sh /scripts/healthcheck-lsphp.sh
## Apply production lsphp ini overrides into lsphp's scan dir (path varies by
## PHP minor version; ask lsphp directly — same idiom as Dockerfile.litespeed).
COPY ./configs/litespeed/lsphp-overrides.ini /etc/lsws-templates/lsphp-overrides.ini
RUN bash -c 'set -e; \
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
mkdir -p "$SCAN_DIR"; \
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
echo "wrote overrides to $SCAN_DIR"'
## php-lsapi gates .user.ini parsing behind this env var (see entrypoint-lsphp.sh
## for the full explanation). Set here so the value is visible in `docker inspect`
## and survives an entrypoint override; the entrypoint re-exports it with the same
## default so the runuser exec path can't drop it.
ENV LSPHP_ENABLE_USER_INI=on
EXPOSE 9000
## TCP-connect + lsphp-alive check (LSAPI isn't FastCGI, so no cgi-fcgi ping).
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD /scripts/healthcheck-lsphp.sh
ENTRYPOINT ["/scripts/entrypoint-lsphp.sh"]
+57
View File
@@ -0,0 +1,57 @@
## shared-ols — the shared OpenLiteSpeed webserver tier.
##
## One OLS container fronting MANY tenants' detached cac-lsphp sidecars — the
## OLS analogue of the shared-httpd container. Runs NO PHP locally: every site's
## PHP goes to its own cac-lsphp:phpNN sidecar over LSAPI (extProcessor type
## lsapi, address <sidecar>:9000). HAProxy stays the TLS/WAF/SNI edge and routes
## OLS-type hostnames here on :443.
##
## Built on the SAME litespeedtech prebuilt base as cac-litespeed / cac-lsphp so
## the OLS build + plumbing (lscgid, cgid socket — see feedback_ols_packaging_landmines)
## are the proven ones. The base is lsphp-tagged but we never run that lsphp;
## the tag just selects the OLS build. Pinned to lsphp83 / OLS 1.8.4.
##
## Config model (established by PoC 2026-06-10): OLS has NO top-level `include`,
## so render-shared-ols-config.sh assembles httpd_config.conf from the panel's
## per-site files at boot + on every change. See that script + the plan.
ARG OLS_VERSION=1.8.4
ARG PHPVER=83
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
## Tooling the shared tier needs on top of the base:
## - inotify-tools: the .htaccess watcher (spec 5.3)
## - gettext-base: envsubst for render-shared-ols-config.sh
## - openssl: self-signed cert for the :443 listener (HAProxy verifies none)
## - curl/ca-certificates: HEALTHCHECK
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
inotify-tools gettext-base openssl ca-certificates curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
## Snapshot the stock httpd_config.conf so render-shared-ols-config.sh always has
## a pristine base to strip-and-rebuild from (the base image keeps it at conf/).
RUN mkdir -p /usr/local/lsws/.conf && \
cp /usr/local/lsws/conf/httpd_config.conf /usr/local/lsws/.conf/httpd_config.conf
COPY ./scripts/entrypoint-shared-ols.sh \
./scripts/render-shared-ols-config.sh \
./scripts/ols-htaccess-watcher.sh \
/scripts/
RUN chmod +x /scripts/entrypoint-shared-ols.sh /scripts/render-shared-ols-config.sh /scripts/ols-htaccess-watcher.sh
COPY ./configs/shared-ols/ /etc/shared-ols-templates/
## Admin console unreachable from tenant/edge networks (spec 5.2): bind the
## WebAdmin listener to loopback. Same sed as Dockerfile.litespeed.
RUN sed -i 's|^[[:space:]]*address[[:space:]]\+\*:| address 127.0.0.1:|' \
/usr/local/lsws/admin/conf/admin_config.conf 2>/dev/null || true
EXPOSE 80 443
## Health: the entrypoint renders a catch-all _health vhost serving /healthz, so
## this passes from boot (zero customer sites) onward. Self-signed :443.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsSk https://127.0.0.1/healthz || exit 1
ENTRYPOINT ["/scripts/entrypoint-shared-ols.sh"]
+64 -1
View File
@@ -7,7 +7,23 @@
## append pattern too (see setup_docker.sh in litespeedtech/ols-dockerfiles). ## append pattern too (see setup_docker.sh in litespeedtech/ols-dockerfiles).
## ##
## Rendered at container start by scripts/create-vhost-litespeed.sh via ## Rendered at container start by scripts/create-vhost-litespeed.sh via
## envsubst. Templated vars: $user $domain $vhost_map_aliases. ## envsubst. Templated vars: $user $domain $vhost_map_aliases $PHPVER
## $LSAPI_CHILDREN (computed by detect-memory-litespeed.sh)
## --- real client IP behind HAProxy ---
## OLS equivalent of the Apache cac:phpNN mod_remoteip wiring
## (configs/remote_ip.conf + RemoteIPInternalProxy in entrypoint.sh). Without
## this, OLS records HAProxy's docker-bridge IP as the peer: every site's
## access_log and lsphp $_SERVER['REMOTE_ADDR'] collapse to one internal IP,
## silently breaking traffic analytics, WP security plugins, brute-force
## detection, Coraza source-IP correlation, geo, and rate-limiting.
## 1 = trust X-Forwarded-For (the container is only reachable via HAProxy;
## it is never bound to a public address). Mirrors the Apache side, which
## trusts the whole docker subnet via RemoteIPInternalProxy $docker_network.
## When enabled, OLS rewrites the remote IP for BOTH logging and the LSAPI
## REMOTE_ADDR before PHP sees it — so the default access_log format already
## records the real visitor; no LogFormat change needed.
useIpInProxyHeader 1
## --- our listeners (replace stock Default :8088) --- ## --- our listeners (replace stock Default :8088) ---
listener HTTP { listener HTTP {
@@ -30,6 +46,53 @@ listener HTTPS {
map siteVH * map siteVH *
} }
## --- lsphp extProcessor (overrides the stock one which is hard-coded to
## PHP_LSAPI_CHILDREN=10 regardless of container memory).
##
## Sized dynamically by detect-memory-litespeed.sh based on the cgroup cap:
## 2 GiB container → LSAPI_CHILDREN ≈ 17 (was stuck at 10)
## 1 GiB container → LSAPI_CHILDREN ≈ 8
## 512 MiB → LSAPI_CHILDREN ≈ 3
##
## Idle-reduction knobs (the question that motivated this whole block):
## LSAPI_MAX_IDLE_CHILDREN=2 default was CHILDREN/2 (so 10/2=5)
## LSAPI_MAX_IDLE=60 default was 300 (5 min)
## Together: max 2 idle workers kept alive, anything idle >60s gets reaped.
## Trade-off: cold-start of an extra worker after idle reaping costs ~50-100ms
## on the first request to it. Worth it for shadowdao-sized low-traffic sites
## where the difference is "30 MB idle" vs "200 MB idle".
##
## memSoftLimit/memHardLimit: per-worker RLIMIT_AS catches a runaway PHP
## script before it hogs the whole pool's memory. Cgroup is still the host
## backstop (one-customer-per-container), but the per-worker cap protects
## the OTHER workers in the same pool from a bad-actor script. 1024M soft
## comfortably accommodates typical Divi/WooCommerce VSZ (~280-365 MB).
extProcessor lsphp {
type lsapi
address uds://tmp/lshttpd/lsphp.sock
maxConns ${LSAPI_CHILDREN}
env PHP_LSAPI_CHILDREN=${LSAPI_CHILDREN}
env LSAPI_MAX_IDLE_CHILDREN=2
env LSAPI_MAX_IDLE=60
env PHP_LSAPI_MAX_REQUESTS=500
env LSAPI_AVOID_FORK=200M
initTimeout 60
retryTimeout 0
persistConn 1
pcKeepAliveTimeout 30
respBuffer 0
autoStart 1
path /usr/local/lsws/lsphp${PHPVER}/bin/lsphp
backlog 100
instances 1
runOnStartUp 1
priority 0
memSoftLimit 1024M
memHardLimit 1500M
procSoftLimit 400
procHardLimit 500
}
## --- our vhost via vhTemplate (upstream's working pattern) --- ## --- our vhost via vhTemplate (upstream's working pattern) ---
## The template file is /usr/local/lsws/conf/templates/site.conf — written ## The template file is /usr/local/lsws/conf/templates/site.conf — written
## by create-vhost-litespeed.sh at the same time as this fragment. ## by create-vhost-litespeed.sh at the same time as this fragment.
+20 -3
View File
@@ -25,9 +25,26 @@ session.gc_divisor = 1000
session.gc_maxlifetime = 1440 session.gc_maxlifetime = 1440
opcache.enable = 1 opcache.enable = 1
opcache.memory_consumption = 128 ; Sized to fit Divi + WooCommerce + WP core comfortably without eviction
opcache.interned_strings_buffer = 8 ; thrash. Per-instance because shmem is per-process-RSS on Linux cgroups
opcache.max_accelerated_files = 10000 ; (vs PHP-FPM's COW-shared model — one lsphp PARENT per httpd worker in
; OLS, each with its own opcache segment).
;
; Sizing history:
; 128 MB / 10000 files (original): blew 800+ MiB shmem under setUIDMode 2
; because that gave 8+ lsphp instances each at 128 MB → 1+ GiB shmem.
; 32 MB / 4000 files (2026-06-02): solved the shmem problem but caused
; opcache eviction thrash on Divi/WC sites (3000+4000 unique PHP files
; each); manifested as ~40% sustained CPU on alphaoneaminos and
; elevated OOM cycling on brain-jar (5378 oom_kills/9h on 2026-06-03).
; 64 MB / 8000 files (current): fits Divi+WC bytecode without eviction;
; N lsphp × 64 MB ≈ 512 MiB shmem worst case, still acceptable.
;
; Override per-site via OPCACHE_MEMORY_MB / OPCACHE_MAX_FILES env vars
; (panel: Advanced Tuning → OpCache size) for outliers.
opcache.memory_consumption = 64
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 8000
opcache.revalidate_freq = 60 opcache.revalidate_freq = 60
opcache.enable_cli = Off opcache.enable_cli = Off
+17 -7
View File
@@ -8,18 +8,28 @@
allowSymbolLink 1 allowSymbolLink 1
enableScript 1 enableScript 1
restrained 1 restrained 1
## setUIDMode 2 = DocRoot UID — lsphp suexec's to the OWNER of vhRoot. ## No setUIDMode — OLS itself runs as ${user} (set at server level by
## We chown /home/${user} to ${user}:${user} in the entrypoint, so PHP ## create-vhost-litespeed.sh), so lsphp inherits that uid without needing
## runs as the customer per request. Container is still the privsep ## suEXEC per request. This is the key to single-lsphp-instance topology:
## boundary; this is the clean "scripts run as user" model. ## with setUIDMode 2, each httpd worker had to lscgid-spawn its own lsphp
setUIDMode 2 ## (= N opcache shmem segments). Without it, ONE persistent lsphp parent
## serves all httpd workers via the shared socket, and LSAPI children-mode
## actually works (1 parent + N children = 1 shmem segment).
##
## Safe because cac-litespeed is one-customer-per-container — the container
## boundary IS the privsep boundary.
vhRoot /home/${user}/public_html/ vhRoot /home/${user}/public_html/
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhconf.conf configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhconf.conf
virtualHostConfig { virtualHostConfig {
docRoot $VH_ROOT docRoot $VH_ROOT
errorlog /home/${user}/logs/litespeed/error.log { ## Drop-in log paths matching cac:phpNN (Apache+FPM bundled) so existing
## WHP log-gathering code (whp-traffic-aggregator.php, process-log-review.php,
## customer-facing log views) keeps working unchanged for migrated sites.
## Customer's "Apache access log" is just OLS's access log under the same
## filename. No `.log` suffix — matches the bundled cac convention.
errorlog /home/${user}/logs/apache/error_log {
useServer 0 useServer 0
logLevel WARN logLevel WARN
rollingSize 10M rollingSize 10M
@@ -27,7 +37,7 @@ virtualHostConfig {
compressArchive 1 compressArchive 1
} }
accesslog /home/${user}/logs/litespeed/access.log { accesslog /home/${user}/logs/apache/access_log {
useServer 0 useServer 0
rollingSize 10M rollingSize 10M
keepDays 7 keepDays 7
+51
View File
@@ -0,0 +1,51 @@
## ---- shared-ols append (do not edit below) ----
## Server-level config for the SHARED OpenLiteSpeed tier. Appended to the
## stock httpd_config.conf AFTER render-shared-ols-config.sh strips the stock
## listeners, vhTemplate docker, AND the stock `extProcessor lsphp` +
## `scriptHandler` (so this server NEVER runs PHP locally — every site's PHP
## goes to its own detached cac-lsphp sidecar over LSAPI). Rendered with
## envsubst; only ${LSCACHE_ROOT} is substituted here.
serverName shared-ols
## Real client IP behind HAProxy. HAProxy sets X-Forwarded-For (the real
## client) and X-Forwarded-Proto. Mode 1 = always use X-Forwarded-For as the
## client IP. HAProxy is the ONLY thing that ever connects to this tier (it's on
## client-net with no host-published ports) and it OVERWRITES X-Forwarded-For
## with %[src] (set-header, not add-header), so a client can't spoof it — mode 1
## is safe here and matches the working standalone litespeed config.
## NOTE: mode 2 ("trusted IP only") does NOT mean "trust the proxy header" — it
## extracts the real IP ONLY when the connecting peer is in a TRUSTED access
## list, which this tier never configured. With mode 2 + no trusted IP, OLS kept
## HAProxy's container IP as REMOTE_ADDR for every request, so WP security
## plugins saw all tenants as one IP and blocking it locked everyone out.
useIpInProxyHeader 1
## LSCache enabled at MODULE scope for the whole tier (dedicated cache volume,
## ephemeral across rebuilds; OLS auto-keys a per-vhost subdir under storagePath).
## PUBLIC (anonymous) caching ONLY: enableCache 1 + checkPublicCache 1 let OLS
## serve cacheable, non-logged-in responses marked by the LiteSpeed Cache WP
## plugin's X-LiteSpeed-Cache-Control headers (ignoreRespCacheCtrl=0 honors them).
##
## PRIVATE caching is intentionally OFF (enablePrivateCache 0 + checkPrivateCache 0).
## Logged-in / cookie-bearing pages must NEVER be cached at the tier. We previously
## left enablePrivateCache=1 assuming "no plugin -> nothing cached," but that was
## WRONG: with private storage + reqCookieCache on, OLS privately cached logged-in
## responses regardless of plugin, serving stale wp-admin (e.g. a "failed update"
## nag that persisted for the full privateExpireInSeconds TTL). Keeping private
## cache off guarantees logged-in pages are always served fresh.
module cache {
storagePath ${LSCACHE_ROOT}
checkPrivateCache 0
checkPublicCache 1
maxCacheObjSize 10000000
maxStaleAge 200
qsCache 1
reqCookieCache 1
respCookieCache 1
ignoreReqCacheCtrl 0
ignoreRespCacheCtrl 0
enableCache 1
enablePrivateCache 0
}
## ---- end shared-ols server append ----
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
* cac-lsphp $_SERVER path normaliser (auto_prepend).
*
* The shared-ols container serves from its bulk /docker/users->/mnt/users mount,
* so OLS sends lsphp $_SERVER['DOCUMENT_ROOT'] / ['SCRIPT_FILENAME'] under
* /mnt/users/<user>/<domain>/... . The sidecar symlinks that back to the real
* /home/<user> mount, so file operations resolve and PHP's own __FILE__/__DIR__/
* realpath()/getcwd() already report /home/<user>/public_html. But the RAW env
* strings OLS set still read /mnt/users, which would leak to the (uncommon) apps
* that build or compare paths from $_SERVER['DOCUMENT_ROOT'].
*
* Canonicalise those two via realpath() so cac-lsphp is byte-for-byte 1:1 with
* cac-fpm/cac-litespeed (where DOCUMENT_ROOT is natively /home/<user>/public_html).
* Cheap (two realpath calls, cached by realpath_cache) and side-effect-free.
*
* Customer sites have no auto_prepend by default, so this is the only prepend in
* play. If a site sets its own auto_prepend_file via .user.ini it overrides this
* (theirs wins) — acceptable: paths still resolve via the symlink, only the raw
* string differs.
*/
foreach (array('DOCUMENT_ROOT', 'SCRIPT_FILENAME') as $__cl_key) {
if (!empty($_SERVER[$__cl_key]) && strncmp($_SERVER[$__cl_key], '/mnt/users/', 11) === 0) {
$__cl_real = realpath($_SERVER[$__cl_key]);
if ($__cl_real !== false) {
$_SERVER[$__cl_key] = $__cl_real;
}
}
}
unset($__cl_key, $__cl_real);
+16 -2
View File
@@ -45,20 +45,34 @@ cp /usr/local/lsws/.conf/httpd_config.conf "$LSWS_CONF/httpd_config.conf"
## Strip the stock blocks we replace. Use awk: easier than sed range-deletes ## Strip the stock blocks we replace. Use awk: easier than sed range-deletes
## to skip a NAMED block of arbitrary length terminated by a top-level `}`. ## to skip a NAMED block of arbitrary length terminated by a top-level `}`.
## extProcessor lsphp is stripped because the stock one hard-codes
## PHP_LSAPI_CHILDREN=10 regardless of container size — our appended
## extProcessor scales it from detect-memory-litespeed.sh.
awk ' awk '
BEGIN { skip = 0 } BEGIN { skip = 0 }
/^listener HTTP \{/ || /^listener HTTPS \{/ || /^vhTemplate docker \{/ { skip = 1; next } /^listener HTTP \{/ || /^listener HTTPS \{/ || /^vhTemplate docker \{/ || /^extProcessor lsphp\{/ || /^extProcessor lsphp \{/ { skip = 1; next }
skip && /^\}/ { skip = 0; next } skip && /^\}/ { skip = 0; next }
!skip { print } !skip { print }
' "$LSWS_CONF/httpd_config.conf" > "$LSWS_CONF/httpd_config.conf.new" ' "$LSWS_CONF/httpd_config.conf" > "$LSWS_CONF/httpd_config.conf.new"
mv "$LSWS_CONF/httpd_config.conf.new" "$LSWS_CONF/httpd_config.conf" mv "$LSWS_CONF/httpd_config.conf.new" "$LSWS_CONF/httpd_config.conf"
## Server-level user/group → customer. Without this, OLS runs as nobody and
## either can't read customer files (no setUIDMode) or has to lscgid-spawn a
## per-uid lsphp for every httpd worker (the setUIDMode 2 pathway). With OLS
## itself running as ${user}, a single shared lsphp parent serves all httpd
## workers, LSAPI children-mode actually engages, and shmem stops fanning out.
## OLS still starts as root (PID 1 binds 80/443) then drops privs after bind.
sed -i \
-e "s|^user[[:space:]].*|user ${user}|" \
-e "s|^group[[:space:]].*|group ${user}|" \
"$LSWS_CONF/httpd_config.conf"
## --- append our listeners + vhTemplate --- ## --- append our listeners + vhTemplate ---
SENTINEL="## ---- cac-litespeed append (do not edit below) ----" SENTINEL="## ---- cac-litespeed append (do not edit below) ----"
{ {
echo "" echo ""
echo "$SENTINEL" echo "$SENTINEL"
envsubst '${user} ${domain} ${vhost_map_aliases}' < "$TPL_DIR/httpd_config.tpl" envsubst '${user} ${domain} ${vhost_map_aliases} ${PHPVER} ${LSAPI_CHILDREN}' < "$TPL_DIR/httpd_config.tpl"
} >> "$LSWS_CONF/httpd_config.conf" } >> "$LSWS_CONF/httpd_config.conf"
## --- write our vhost template to /usr/local/lsws/conf/templates/site.conf --- ## --- write our vhost template to /usr/local/lsws/conf/templates/site.conf ---
+25 -12
View File
@@ -47,9 +47,24 @@ if [ "$AVAILABLE_MB" -lt 60 ]; then
fi fi
## ---- LSAPI children (analogous to PHP_FPM_MAX_CHILDREN) ---- ## ---- LSAPI children (analogous to PHP_FPM_MAX_CHILDREN) ----
## LSAPI is more memory-efficient than FPM; estimate 96 MB / worker ## Per the 2026-06-02 cac-litespeed memory-sizing finding (vantagehealth
## (vs 128 MB for FPM after the 2026-06-01 bump). Floor 2, cap 50. ## OOM-spawn loop at 512 MB cap): each lsphp worker carries ~115 MB
LSPHP_WORKER_ESTIMATE_MB=${LSPHP_WORKER_ESTIMATE_MB:-96} ## shmem-rss + ~25 MB anon + ~10 MB file ≈ 130-150 MB real cgroup cost
## per worker on heavy WP workloads. shmem is RSS-accounted per worker
## (vs cac-fpm's COW-shared fork model) so the cost is real per cgroup,
## not just per process.
##
## 115 (the previous default) was set from idle-state measurements and
## ran brain-jar.com into 142 OOM-kills at 1 GiB on 2026-06-02 night —
## the formula computed CHILDREN=8, which left zero headroom once Divi
## page renders started growing worker anon. Bumped to 130 to track the
## active per-worker cost; gives slightly fewer workers but real headroom.
##
## Sub-512 MB containers remain unsafe for dynamic WP on OLS — the floor
## of 2 workers still applies but it'll be cap-marginal. Per-site override
## via FPM_MAX_CHILDREN env var (panel edit-site UI) overrides this for
## sites where the default isn't right for their workload.
LSPHP_WORKER_ESTIMATE_MB=${LSPHP_WORKER_ESTIMATE_MB:-130}
calc_lsapi_children=$((AVAILABLE_MB / LSPHP_WORKER_ESTIMATE_MB)) calc_lsapi_children=$((AVAILABLE_MB / LSPHP_WORKER_ESTIMATE_MB))
if [ "$calc_lsapi_children" -lt 2 ]; then if [ "$calc_lsapi_children" -lt 2 ]; then
@@ -64,13 +79,11 @@ fi
## else the calculated value. ## else the calculated value.
LSAPI_CHILDREN=${LSAPI_CHILDREN:-${FPM_MAX_CHILDREN:-$calc_lsapi_children}} LSAPI_CHILDREN=${LSAPI_CHILDREN:-${FPM_MAX_CHILDREN:-$calc_lsapi_children}}
## extprocessor mem limits — total LSAPI heap should fit AVAILABLE_MB with ## Per-worker mem limits (RLIMIT_AS) live in httpd_config.tpl now as
## some breathing room. Soft = budget/children, hard = soft * 1.5, both capped ## hard-coded 1024M soft / 1500M hard — those values comfortably fit
## at 2047 (OLS interprets > 2047 oddly in some 1.x builds). ## typical Divi/WooCommerce VSZ (~280-365 MB) while still catching a
LSAPI_MEM_SOFT=$((AVAILABLE_MB / LSAPI_CHILDREN)) ## true runaway script. Cgroup remains the real backstop. The earlier
if [ "$LSAPI_MEM_SOFT" -lt 64 ]; then LSAPI_MEM_SOFT=64; fi ## AVAILABLE/CHILDREN formula was killing legitimate workers because
if [ "$LSAPI_MEM_SOFT" -gt 2047 ]; then LSAPI_MEM_SOFT=2047; fi ## it conflated VSZ (RLIMIT_AS) with RSS-budget arithmetic.
LSAPI_MEM_HARD=$((LSAPI_MEM_SOFT * 3 / 2))
if [ "$LSAPI_MEM_HARD" -gt 2047 ]; then LSAPI_MEM_HARD=2047; fi
export CONTAINER_MEMORY_MB LSAPI_CHILDREN LSAPI_MEM_SOFT LSAPI_MEM_HARD export CONTAINER_MEMORY_MB LSAPI_CHILDREN
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
## detect-memory-lsphp.sh — sibling of detect-memory-litespeed.sh for the
## cac-lsphp DETACHED sidecar (lsphp -b, no local webserver).
##
## Computes PHP_LSAPI_CHILDREN from the container memory cap. Identical worker
## arithmetic to detect-memory-litespeed.sh, with ONE difference: there is no
## OpenLiteSpeed daemon in this container (OLS runs in the shared-ols tier), so
## the ~40 MB OLS_RESERVE is dropped — every MB above the OS reserve goes to
## lsphp workers. Sourced by entrypoint-lsphp.sh.
## ---- container memory detection (mirrors detect-memory-litespeed.sh) ----
CONTAINER_MEMORY_BYTES=""
if [ -f /sys/fs/cgroup/memory.max ]; then
val=$(cat /sys/fs/cgroup/memory.max 2>/dev/null)
if [ "$val" != "max" ] && [ -n "$val" ]; then
CONTAINER_MEMORY_BYTES=$val
fi
fi
if [ -z "$CONTAINER_MEMORY_BYTES" ] && [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
val=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null)
if [ -n "$val" ] && [ "$val" -lt 8589934592000 ] 2>/dev/null; then
CONTAINER_MEMORY_BYTES=$val
fi
fi
if [ -z "$CONTAINER_MEMORY_BYTES" ] && [ -f /proc/meminfo ]; then
mem_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
if [ -n "$mem_kb" ]; then
CONTAINER_MEMORY_BYTES=$((mem_kb * 1024))
fi
fi
if [ -z "$CONTAINER_MEMORY_BYTES" ]; then
CONTAINER_MEMORY_BYTES=$((512 * 1024 * 1024))
fi
CONTAINER_MEMORY_MB=$((CONTAINER_MEMORY_BYTES / 1024 / 1024))
## ---- budget split (all non-OS memory is the lsphp workers' to use) ----
OS_RESERVE_MB=50
DEV_OVERHEAD_MB=0
if [ "${environment:-PROD}" = "DEV" ]; then
DEV_OVERHEAD_MB=125
fi
AVAILABLE_MB=$((CONTAINER_MEMORY_MB - OS_RESERVE_MB - DEV_OVERHEAD_MB))
if [ "$AVAILABLE_MB" -lt 60 ]; then
AVAILABLE_MB=60
fi
## ---- LSAPI children ----
## Same ~130 MB/worker estimate as cac-litespeed (see detect-memory-litespeed.sh
## for the vantagehealth/brain-jar OOM history that set this). Detached lsphp
## has the SAME per-worker shmem-RSS profile as in-container lsphp — splitting
## the webserver out doesn't change lsphp's memory model, only removes the OLS
## daemon footprint from the budget.
LSPHP_WORKER_ESTIMATE_MB=${LSPHP_WORKER_ESTIMATE_MB:-130}
calc_lsapi_children=$((AVAILABLE_MB / LSPHP_WORKER_ESTIMATE_MB))
if [ "$calc_lsapi_children" -lt 2 ]; then
calc_lsapi_children=2
fi
if [ "$calc_lsapi_children" -gt 50 ]; then
calc_lsapi_children=50
fi
## Per-site override precedence — the WHP panel (site-pool-env.php) passes the
## customer's override as LSAPI_CHILDREN and/or FPM_MAX_CHILDREN; either wins
## over the calculated default. entrypoint-lsphp.sh exports the result as
## PHP_LSAPI_CHILDREN (the name lsphp -b reads).
LSAPI_CHILDREN=${LSAPI_CHILDREN:-${FPM_MAX_CHILDREN:-$calc_lsapi_children}}
export CONTAINER_MEMORY_MB LSAPI_CHILDREN
+149 -22
View File
@@ -33,7 +33,12 @@ if ! id -u "$user" >/dev/null 2>&1; then
fi fi
mkdir -p "/home/$user/public_html" mkdir -p "/home/$user/public_html"
mkdir -p "/home/$user/logs/litespeed" ## Log dirs mirror cac:phpNN exactly — apache/ for web server access+error,
## php-fpm/ for PHP errors. OLS isn't Apache and lsphp isn't php-fpm, but
## the customer-facing paths stay identical so log-gathering, analytics,
## and the customer's "where do I find my access log?" mental model all
## just work without per-image-family special cases.
mkdir -p "/home/$user/logs/apache" "/home/$user/logs/php-fpm"
mkdir -p "/home/$user/lscache" mkdir -p "/home/$user/lscache"
mkdir -p /tmp/lshttpd/swap mkdir -p /tmp/lshttpd/swap
@@ -42,7 +47,7 @@ chmod 1777 /tmp/lshttpd
## ---- memory + lsphp pool sizing ---- ## ---- memory + lsphp pool sizing ----
# shellcheck source=/dev/null # shellcheck source=/dev/null
source /scripts/detect-memory-litespeed.sh source /scripts/detect-memory-litespeed.sh
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | LSAPI_CHILDREN=${LSAPI_CHILDREN} | memSoft=${LSAPI_MEM_SOFT}M memHard=${LSAPI_MEM_HARD}M | PHPVER=${PHPVER}" echo "Container memory: ${CONTAINER_MEMORY_MB}MB | LSAPI_CHILDREN=${LSAPI_CHILDREN} | PHPVER=${PHPVER}"
## ---- self-signed cert (idempotent) ---- ## ---- self-signed cert (idempotent) ----
mkdir -p /usr/local/lsws/conf/cert mkdir -p /usr/local/lsws/conf/cert
@@ -56,11 +61,39 @@ fi
## ---- render httpd_config + vhconf from templates ---- ## ---- render httpd_config + vhconf from templates ----
/scripts/create-vhost-litespeed.sh /scripts/create-vhost-litespeed.sh
## ---- ownership: OLS master/workers run as nobody; lsphp suexecs to the ## ---- point PHP error_log at the same customer-visible path that
## customer per request (setUIDMode 2 in httpd_config.tpl). So the customer ## cac:phpNN uses for php-fpm errors. Drop-in compat: customer code that
## owns everything under /home/$user — clean ownership model, no nobody ## was tailing /home/$user/logs/php-fpm/error.log on the old image will
## chowning. OLS's own runtime dirs stay nobody-owned. ## see lsphp's PHP errors in the exact same file on the new image.
chown -R nobody:nogroup /usr/local/lsws/logs /usr/local/lsws/conf/cert /tmp/lshttpd 2>/dev/null || true ## Rendered as a tiny ini in lsphp's scan dir; PHP merges it after the
## production-tuning overrides at startup.
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
if [ -n "$SCAN_DIR" ]; then
cat > "$SCAN_DIR/99-user-error-log.ini" <<EOF
; rendered at container start by entrypoint-litespeed.sh
error_log = /home/${user}/logs/php-fpm/error.log
log_errors = On
EOF
## Per-site opcache override (panel: Advanced Tuning → OpCache size).
## Falls back to the global lsphp-overrides.ini values (64 MB / 8000 files)
## when the env vars aren't set. Numeric range/sanity is enforced in the
## WHP panel before the env var lands here.
if [ -n "${OPCACHE_MEMORY_MB:-}" ] || [ -n "${OPCACHE_MAX_FILES:-}" ]; then
{
echo "; rendered at container start by entrypoint-litespeed.sh"
echo "; per-site override from WHP whp.sites.opcache_*_override"
[ -n "${OPCACHE_MEMORY_MB:-}" ] && echo "opcache.memory_consumption = ${OPCACHE_MEMORY_MB}"
[ -n "${OPCACHE_MAX_FILES:-}" ] && echo "opcache.max_accelerated_files = ${OPCACHE_MAX_FILES}"
} > "$SCAN_DIR/99-user-opcache.ini"
fi
fi
## ---- ownership: OLS runs as $user end-to-end (server-level user set by
## create-vhost-litespeed.sh, no setUIDMode). So OLS runtime dirs need to
## be customer-owned for log writes, swap files, lsphp socket creation.
## Master still starts as root for port binding, then drops privs to $user.
chown -R "$user:$user" /usr/local/lsws/logs /usr/local/lsws/conf/cert /tmp/lshttpd 2>/dev/null || true
chown -R "$user:$user" "/home/$user" chown -R "$user:$user" "/home/$user"
chmod 755 "/home/$user" chmod 755 "/home/$user"
@@ -72,8 +105,20 @@ sudo -u "$user" sh -c "echo ok > /home/$user/public_html/healthz"
if [ "$environment" = "DEV" ]; then if [ "$environment" = "DEV" ]; then
echo "Starting Dev Deployment (litespeed)" echo "Starting Dev Deployment (litespeed)"
mkdir -p "/home/$user/_db_backups" mkdir -p "/home/$user/_db_backups"
## mariadb-server + memcached are already in the image (apt-installed ## mariadb-server + memcached are NOT baked into the image (saves ~500MB
## in the Dockerfile). Just start them. ## on PROD pulls). Install them at runtime, but only once per container —
## the command -v guard means a restart of an already-bootstrapped
## container skips the apt step and DEV boot stays ~1.5s like PROD.
## First-boot in DEV adds ~30-60s for the apt install; acceptable
## tradeoff per the design spec.
if ! command -v mysqld >/dev/null 2>&1; then
echo "DEV first boot: installing mariadb-server + memcached..."
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
mariadb-server memcached
apt-get clean
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
fi
mkdir -p /run/mysqld && chown mysql:mysql /run/mysqld mkdir -p /run/mysqld && chown mysql:mysql /run/mysqld
nohup mysqld --user=mysql &>/dev/null & nohup mysqld --user=mysql &>/dev/null &
if [ ! -f "/home/$user/mysql_creds" ]; then if [ ! -f "/home/$user/mysql_creds" ]; then
@@ -109,20 +154,102 @@ service cron start >/dev/null 2>&1 || /usr/sbin/cron
## ---- LSCache plugin (background, non-fatal) ---- ## ---- LSCache plugin (background, non-fatal) ----
( /scripts/install-lscache-wp.sh "$user" >>/var/log/lscache-install.log 2>&1 || true ) & ( /scripts/install-lscache-wp.sh "$user" >>/var/log/lscache-install.log 2>&1 || true ) &
## ---- start OLS in foreground with crash-guard ---- ## Stream OLS + customer logs to PID-1 stdout so `docker logs` works. Started
## openlitespeed -n = no-daemon + supervisor. Trap forwards SIGTERM cleanly. ## once, before the supervisor loop — it follows the files across OLS restarts.
OLS_PID=""
trap '[ -n "$OLS_PID" ] && kill -TERM "$OLS_PID" 2>/dev/null; wait "$OLS_PID" 2>/dev/null || true' TERM INT
/usr/local/lsws/bin/openlitespeed -n &
OLS_PID=$!
## Stream OLS + customer logs to PID-1 stdout so `docker logs` works.
touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log
touch "/home/$user/logs/litespeed/error.log" "/home/$user/logs/litespeed/access.log" touch "/home/$user/logs/apache/error_log" "/home/$user/logs/apache/access_log"
touch "/home/$user/logs/php-fpm/error.log"
chown "$user:$user" "/home/$user/logs/apache/error_log" \
"/home/$user/logs/apache/access_log" \
"/home/$user/logs/php-fpm/error.log"
tail -F /usr/local/lsws/logs/error.log \ tail -F /usr/local/lsws/logs/error.log \
/usr/local/lsws/logs/access.log \ /usr/local/lsws/logs/access.log \
"/home/$user/logs/litespeed/error.log" \ "/home/$user/logs/apache/error_log" \
"/home/$user/logs/litespeed/access.log" 2>/dev/null & "/home/$user/logs/apache/access_log" \
"/home/$user/logs/php-fpm/error.log" 2>/dev/null &
wait "$OLS_PID" ## ---- supervise OLS in DAEMON mode (NOT `openlitespeed -n` + wait) ----
## OLS performs INTERNAL graceful self-restarts: the LiteSpeed Cache /
## QUIC.cloud integration refreshes the QUIC.cloud IP allowlist on a schedule
## and, when it changes, sends SIGUSR1 → "request a graceful server restart".
## In `-n` foreground mode the OLD main PID exits after the zero-downtime
## handoff; a bare `wait` on that PID lets bash (PID 1) exit and tears the whole
## container down. Worse, that exit is *clean*, so `RestartPolicy` doesn't
## reliably catch it — the container just stops and HAProxy serves 503 until
## someone manually starts it. (Root-caused on whp02 alsacorp, 2026-06-06.)
##
## Daemon mode is OLS's native model: it owns the SIGUSR1 handoff, keeps the
## listeners bound across generations, and rewrites lshttpd.pid to the new main.
## PID 1 just FOLLOWS the pidfile — a graceful self-restart is invisible here
## (zero downtime), and we only ever relaunch on a genuine crash (no live main).
STOP_REQUESTED=0
term_handler() {
STOP_REQUESTED=1
/usr/local/lsws/bin/lswsctrl stop >/dev/null 2>&1 || true
}
trap term_handler TERM INT
## Authoritative, path-independent liveness check: `lswsctrl status` prints
## "litespeed is running with PID N." when up (and "...is not running" when
## down). We match the running message specifically — a bare grep for "running"
## would also match "not running". (This image keeps the pidfile under
## /tmp/lshttpd, not logs/, so we never hard-code a pidfile path.)
ols_running() { /usr/local/lsws/bin/lswsctrl status 2>/dev/null | grep -qi 'running with pid'; }
## Crash-loop cap: if OLS can't stay up, bail out so Docker's restart policy and
## the site-health monitor escalate instead of us hot-looping forever.
MAX_STARTS=5
WINDOW=60
starts=""
start_ols() {
/usr/local/lsws/bin/lswsctrl start >/dev/null 2>&1 || true
## wait up to 10s for the daemon to report running
for _ in $(seq 1 20); do
ols_running && return 0
sleep 0.5
done
return 1
}
if ! start_ols; then
echo "entrypoint: OLS failed to start (not running after 10s)." >&2
exit 1
fi
echo "entrypoint: OLS started in daemon mode — $(/usr/local/lsws/bin/lswsctrl status 2>/dev/null || true)"
while true; do
if ols_running; then
sleep 3
continue
fi
## Not running this instant. This is EITHER a clean shutdown OR the brief
## handoff window of a graceful self-restart (status momentarily reports down
## while the new main takes over). Grace, then re-check before judging.
sleep 2
if [ "$STOP_REQUESTED" -eq 0 ] && ols_running; then
continue
fi
if [ "$STOP_REQUESTED" -eq 1 ]; then
echo "entrypoint: SIGTERM received, OLS stopped — exiting."
exit 0
fi
## Genuine crash: not running and no shutdown requested. Relaunch, capped.
now=$(date +%s)
starts="$starts $now"
pruned=""
for t in $starts; do
[ $((now - t)) -lt "$WINDOW" ] && pruned="$pruned $t"
done
starts="$pruned"
n=$(echo $starts | wc -w)
echo "entrypoint: OLS not running — relaunching (attempt $n/$MAX_STARTS within ${WINDOW}s)." >&2
if [ "$n" -ge "$MAX_STARTS" ]; then
echo "entrypoint: OLS crash-looping ($n starts in ${WINDOW}s) — bailing out for Docker restart policy / monitor." >&2
exit 1
fi
start_ols || true
done
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
## entrypoint-lsphp.sh — PID 1 for cac-lsphp:phpNN.
##
## The per-site PHP backend for the SHARED OpenLiteSpeed tier. Runs lsphp in
## DETACHED LSAPI mode (`lsphp -b <addr:port>`) and nothing else — no
## webserver. The shared-ols container connects to this over the docker
## network (extProcessor type lsapi, address <this-container>:9000) exactly
## like the shared httpd connects to a cac-fpm container's php-fpm on :9000.
##
## Structurally identical to cac-fpm/cac-litespeed: same `uid`/`user` contract,
## the customer docroot mounted at /home/$user (so PHP sees /home/$user/public_html
## EXACTLY like the standalone tiers — true 1:1 drop-in for WordPress ABSPATH,
## config paths, and DB-stored absolute paths). The only difference is OLS lives
## in a separate container, so this PID 1 is lsphp itself.
##
## THE SYMLINK (see feedback_ols_lsapi_no_script_filename_remap): OLS has no
## ProxyFCGISetEnvIf-style remap — it hands lsphp exactly its vhost docRoot path.
## The shared-ols container serves from its bulk /docker/users->/mnt/users mount,
## so its docRoot (and the SCRIPT_FILENAME it sends us) is
## /mnt/users/<user>/<domain>/public_html. We create a symlink
## /mnt/users/<user>/<domain> -> /home/$user so that path resolves to the real
## /home/$user/public_html files. PHP canonicalises the symlink, so
## __FILE__/__DIR__/realpath all report /home/$user/public_html (verified
## 2026-06-10) — the customer never sees the /mnt/users path.
set -euo pipefail
: "${PHPVER:=83}"
: "${environment:=PROD}"
export CONTAINER_ROLE="lsphp_only"
export PHPVER environment
## ---- env validation (same contract as entrypoint-fpm / entrypoint-litespeed) ----
if [ -z "${uid:-}" ] || [ -z "${user:-}" ]; then
echo "FATAL: 'uid' and 'user' env vars are required (panel sets these from WHP_UID/WHP_USER)." >&2
exit 1
fi
: "${domain:=localhost}"
export user domain
LSPHP_BIN="/usr/local/lsws/lsphp${PHPVER}/bin/lsphp"
if [ ! -x "$LSPHP_BIN" ]; then
echo "FATAL: lsphp binary not found at $LSPHP_BIN (PHPVER=$PHPVER)." >&2
exit 1
fi
## ---- user + directories (identical to entrypoint-litespeed.sh: docroot at
## /home/$user, the customer's bind-mounted domain dir) ----
if ! id -u "$user" >/dev/null 2>&1; then
useradd -u "$uid" -m -s /bin/bash "$user"
fi
mkdir -p "/home/$user/public_html" "/home/$user/logs/php-fpm"
## ---- compatibility symlink for the OLS-sent path ----
## OLS sends SCRIPT_FILENAME under /mnt/users/<user>/<safe-domain>/public_html
## (the shared-ols container's view). Point that at our real /home/$user mount so
## the path resolves. <safe-domain> matches the on-disk convention: wildcard
## `*.foo.com` is stored as `wildcard.foo.com`.
SAFE_DOMAIN="$domain"
case "$domain" in
\*.*) SAFE_DOMAIN="wildcard.${domain#\*.}" ;;
esac
mkdir -p "/mnt/users/$user"
ln -sfn "/home/$user" "/mnt/users/$user/$SAFE_DOMAIN"
## ---- detached-lsphp pool sizing ----
# shellcheck source=/dev/null
source /scripts/detect-memory-lsphp.sh
## LSAPI tuning (spec §5.1). PHP_LSAPI_CHILDREN MUST equal the shared-ols vhost
## maxConns — the WHP panel writes both from the single fpm_max_children value,
## so they can't drift. LSAPI_MAX_IDLE is THE RAM win: idle children exit, so an
## idle site's footprint collapses toward baseline (ondemand-like).
export PHP_LSAPI_CHILDREN="${PHP_LSAPI_CHILDREN:-$LSAPI_CHILDREN}"
export PHP_LSAPI_MAX_REQUESTS="${PHP_LSAPI_MAX_REQUESTS:-500}"
export LSAPI_MAX_IDLE="${LSAPI_MAX_IDLE:-30}"
export LSAPI_EXTRA_CHILDREN="${LSAPI_EXTRA_CHILDREN:-5}"
export LSAPI_AVOID_FORK="${LSAPI_AVOID_FORK:-0}"
LSPHP_BIND="${LSPHP_BIND:-0.0.0.0:9000}"
## ---- .user.ini support ----
## php-lsapi compiles .user.ini support in but leaves it DISABLED by default:
## sapi/litespeed/lsapi_main.c has `static int parse_user_ini = 0;` and only
## sets it in PHP_MINIT_FUNCTION(litespeed) when the PROCESS ENV contains
## LSPHP_ENABLE_USER_INI=on. Without it, lsphp never enters the user-ini chain
## at all — and does so SILENTLY, because `user_ini.filename` / `user_ini.cache_ttl`
## still report their core defaults in phpinfo(). Every other WHP PHP tier
## (cac, cac-fpm, cac-litespeed) honors .user.ini, so leaving it off here made
## the shared-ols tier quietly inconsistent: customer memory_limit /
## max_input_vars overrides were ignored, and — the reason this was found —
## Wordfence's `auto_prepend_file` WAF never loaded on ANY shared-ols site.
##
## Exported here rather than relying solely on the Dockerfile ENV because the
## runuser fallback below resets the environment; an export survives all three
## exec paths. Still overridable per-container (set LSPHP_ENABLE_USER_INI=off in
## the site's env) as an escape hatch for a site whose legacy cPanel-generated
## .user.ini has not been remediated yet.
export LSPHP_ENABLE_USER_INI="${LSPHP_ENABLE_USER_INI:-on}"
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP_LSAPI_CHILDREN=${PHP_LSAPI_CHILDREN} | LSAPI_MAX_IDLE=${LSAPI_MAX_IDLE} | PHPVER=${PHPVER} | bind=${LSPHP_BIND} | user_ini=${LSPHP_ENABLE_USER_INI}"
## ---- per-site ini drop-ins (identical mechanism to entrypoint-litespeed.sh) ----
## error_log → the same customer-visible path cac:phpNN / cac-litespeed use, so
## "where's my PHP error log?" is answered identically across all site types.
SCAN_DIR=$("$LSPHP_BIN" -i 2>/dev/null | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
if [ -n "$SCAN_DIR" ]; then
mkdir -p "$SCAN_DIR"
cat > "$SCAN_DIR/99-user-error-log.ini" <<EOF
; rendered at container start by entrypoint-lsphp.sh
error_log = /home/${user}/logs/php-fpm/error.log
log_errors = On
EOF
## Normalise \$_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] from the OLS-sent
## /mnt/users path back to /home/<user> so cac-lsphp is byte-for-byte 1:1 with
## cac-fpm. Customer sites have no auto_prepend by default, so this is safe; a
## site that sets its own .user.ini auto_prepend overrides it (paths still
## resolve via the symlink either way).
cat > "$SCAN_DIR/99-cac-lsphp-normalize.ini" <<'EOF'
; rendered at container start by entrypoint-lsphp.sh
auto_prepend_file = /scripts/cac-lsphp-normalize.php
EOF
## Per-site opcache override (panel: Advanced Tuning → OpCache size); falls
## back to the baked lsphp-overrides.ini defaults when unset.
if [ -n "${OPCACHE_MEMORY_MB:-}" ] || [ -n "${OPCACHE_MAX_FILES:-}" ]; then
{
echo "; rendered at container start by entrypoint-lsphp.sh"
echo "; per-site override from WHP whp.sites.opcache_*_override"
[ -n "${OPCACHE_MEMORY_MB:-}" ] && echo "opcache.memory_consumption = ${OPCACHE_MEMORY_MB}"
[ -n "${OPCACHE_MAX_FILES:-}" ] && echo "opcache.max_accelerated_files = ${OPCACHE_MAX_FILES}"
} > "$SCAN_DIR/99-user-opcache.ini"
fi
fi
## ---- ownership ----
## Ensure the dirs we created + the log file are customer-owned so lsphp (running
## as $user) can read code and write logs. Customer content is already
## customer-owned from the host side, so we don't recurse the whole (potentially
## large) tree on every boot.
touch "/home/$user/logs/php-fpm/error.log"
chown "$uid:$uid" "/home/$user" "/home/$user/public_html" "/home/$user/logs" "/home/$user/logs/php-fpm" "/home/$user/logs/php-fpm/error.log" 2>/dev/null || true
## ---- exec lsphp -b as the customer user (PID 1) ----
## Bind port is unprivileged (9000), so no root port-bind step is needed — start
## directly as $user. Prefer setpriv (util-linux, on the Ubuntu base); fall back
## to runuser. exec so lsphp becomes PID 1 and receives Docker's signals
## directly (clean stop/restart, matches the php-fpm container's lifecycle).
echo "entrypoint-lsphp: exec $LSPHP_BIN -b $LSPHP_BIND as $user (uid=$uid)"
if command -v setpriv >/dev/null 2>&1; then
exec setpriv --reuid "$uid" --regid "$uid" --init-groups "$LSPHP_BIN" -b "$LSPHP_BIND"
elif command -v runuser >/dev/null 2>&1; then
exec runuser -u "$user" -- "$LSPHP_BIN" -b "$LSPHP_BIND"
else
exec sudo -u "$user" -E "$LSPHP_BIN" -b "$LSPHP_BIND"
fi
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
## entrypoint-shared-ols.sh — PID 1 for the shared-ols tier.
##
## One OpenLiteSpeed container fronting MANY tenants' detached cac-lsphp
## sidecars (the OLS analogue of the shared-httpd container). Webserver ONLY —
## it runs NO PHP locally (render-shared-ols-config.sh strips the stock local
## lsphp; every site's PHP goes to its own sidecar over LSAPI). HAProxy stays
## the TLS/WAF/SNI edge and routes OLS-type hostnames here on :443.
##
## Reuses cac-litespeed's hard-won DAEMON-MODE supervision (NOT `openlitespeed
## -n` + wait): OLS self-restarts on QUIC.cloud IP refresh would otherwise exit
## PID 1 cleanly and tear the container down. See entrypoint-litespeed.sh and
## feedback_ols_quiccloud_restart_kills_container.
set -euo pipefail
: "${environment:=PROD}"
export CONTAINER_ROLE="shared_ols"
LSWS_CONF=/usr/local/lsws/conf
CERT_DIR="$LSWS_CONF/cert"
HEALTH_DIR=/usr/local/lsws/shared-ols-health
export SITES_ROOT="${SITES_ROOT:-$LSWS_CONF/shared-sites}"
export LSCACHE_ROOT="${LSCACHE_ROOT:-/var/lscache}"
export CERT_FILE="$CERT_DIR/shared-ols.crt"
export KEY_FILE="$CERT_DIR/shared-ols.key"
mkdir -p "$SITES_ROOT" "$LSCACHE_ROOT" "$CERT_DIR" "$HEALTH_DIR/html"
## ---- self-signed cert for the :443 listener (HAProxy verifies none) ----
if [ ! -f "$CERT_FILE" ]; then
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout "$KEY_FILE" -out "$CERT_FILE" -subj "/CN=shared-ols" 2>/dev/null
fi
## ---- health vhost (catch-all): valid server with zero customer sites +
## answers HAProxy health checks that hit by IP / unknown Host with a 200 ----
cat > "$HEALTH_DIR/vhconf.conf" <<'EOF'
docRoot $VH_ROOT/html
enableScript 0
context / {
allowBrowse 1
location $DOC_ROOT/
}
EOF
printf 'ok\n' > "$HEALTH_DIR/html/healthz"
printf 'shared-ols\n' > "$HEALTH_DIR/html/index.html"
## ---- ownership: OLS reads conf/ as lsadm. chown the base conf dir + health dir
## NON-recursively (the per-site files under conf/shared-sites are written by the
## panel and are world-readable; a recursive chown here would be O(N-sites) on
## every container (re)start, delaying first-listen after a crash). The render
## script chowns the httpd_config.conf it produces. ----
chown lsadm:nogroup "$LSWS_CONF" "$HEALTH_DIR" "$HEALTH_DIR/html" 2>/dev/null || true
chown lsadm:nogroup "$HEALTH_DIR/vhconf.conf" "$HEALTH_DIR/html/healthz" "$HEALTH_DIR/html/index.html" 2>/dev/null || true
## ---- assemble httpd_config.conf from the panel's per-site files ----
/scripts/render-shared-ols-config.sh
## ---- stream OLS logs to PID-1 stdout (follows across restarts) ----
mkdir -p /usr/local/lsws/logs
touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log
tail -F /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log 2>/dev/null &
## ---- .htaccess watcher (required; spec 5.3). Background; the panel monitors
## that it stays alive (its death silently stops rewrite changes applying). ----
/scripts/ols-htaccess-watcher.sh &
WATCHER_PID=$!
## ---- supervise OLS in DAEMON mode (verbatim model from entrypoint-litespeed.sh) ----
STOP_REQUESTED=0
term_handler() {
STOP_REQUESTED=1
kill "$WATCHER_PID" 2>/dev/null || true
/usr/local/lsws/bin/lswsctrl stop >/dev/null 2>&1 || true
}
trap term_handler TERM INT
ols_running() { /usr/local/lsws/bin/lswsctrl status 2>/dev/null | grep -qi 'running with pid'; }
MAX_STARTS=5
WINDOW=60
starts=""
start_ols() {
/usr/local/lsws/bin/lswsctrl start >/dev/null 2>&1 || true
for _ in $(seq 1 20); do
ols_running && return 0
sleep 0.5
done
return 1
}
if ! start_ols; then
echo "entrypoint-shared-ols: OLS failed to start (not running after 10s)." >&2
exit 1
fi
echo "entrypoint-shared-ols: OLS started in daemon mode — $(/usr/local/lsws/bin/lswsctrl status 2>/dev/null || true)"
while true; do
if ols_running; then
sleep 3
continue
fi
sleep 2
if [ "$STOP_REQUESTED" -eq 0 ] && ols_running; then
continue
fi
if [ "$STOP_REQUESTED" -eq 1 ]; then
echo "entrypoint-shared-ols: SIGTERM received, OLS stopped — exiting."
exit 0
fi
now=$(date +%s)
starts="$starts $now"
pruned=""
for t in $starts; do
[ $((now - t)) -lt "$WINDOW" ] && pruned="$pruned $t"
done
starts="$pruned"
n=$(echo $starts | wc -w)
echo "entrypoint-shared-ols: OLS not running — relaunching (attempt $n/$MAX_STARTS within ${WINDOW}s)." >&2
if [ "$n" -ge "$MAX_STARTS" ]; then
echo "entrypoint-shared-ols: OLS crash-looping — bailing for Docker restart policy / monitor." >&2
exit 1
fi
start_ols || true
done
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
## healthcheck-lsphp.sh — liveness for the detached-lsphp sidecar.
##
## LSAPI is not FastCGI, so the cac-fpm `cgi-fcgi ... | grep pong` ping doesn't
## apply here. Minimum viable check (spec §5.1 fallback): the LSAPI listener is
## accepting TCP connections on :9000 AND at least one lsphp process is alive.
## A bound-but-wedged listener with no lsphp would fail the pgrep; a crashed
## listener fails the connect.
PORT="${LSPHP_HEALTH_PORT:-9000}"
# bash /dev/tcp connect test (bash is present on the litespeedtech base).
exec 3<>"/dev/tcp/127.0.0.1/${PORT}" 2>/dev/null || exit 1
exec 3>&- 3<&-
pgrep -x lsphp >/dev/null 2>&1 || exit 1
exit 0
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
## ols-htaccess-watcher.sh — graceful-restart the shared OLS when any tenant's
## .htaccess changes. OLS reads .htaccess (RewriteFile) at (re)start, NOT per
## request, so without this a WordPress permalink/LiteSpeed-Cache change would
## silently not take effect. Required by spec 5.3.
##
## Watches all docroots for .htaccess writes, COALESCES bursts (a WP plugin save
## touches the file several times) within a debounce window, and RATE-LIMITS to
## a floor (one restart per FLOOR seconds) so many tenants saving at once can't
## trigger a restart storm. Debounce/floor are env-tunable (panel discloses the
## resulting "~60s" window to customers).
##
## Failure of THIS process is the silent-ticket failure mode (spec 7): if it
## dies, tenants' rewrite changes stop applying with no error. The entrypoint
## runs it and the panel monitors it (check-ols-htaccess-watcher.php).
set -uo pipefail
WATCH_ROOT="${OLS_WATCH_ROOT:-/mnt/users}"
DEBOUNCE="${OLS_HTACCESS_DEBOUNCE:-15}" # coalesce window (s)
FLOOR="${OLS_HTACCESS_FLOOR:-60}" # min seconds between restarts
LSWSCTRL=/usr/local/lsws/bin/lswsctrl
last_restart=0
log() { echo "ols-htaccess-watcher: $*" >&2; }
do_restart() {
now=$(date +%s)
if [ $((now - last_restart)) -lt "$FLOOR" ]; then
log "within ${FLOOR}s floor — coalescing, skipping restart"
return
fi
if "$LSWSCTRL" restart >/dev/null 2>&1; then
last_restart=$now
log "graceful restart issued (.htaccess change)"
else
log "WARNING: lswsctrl restart failed"
fi
}
if ! command -v inotifywait >/dev/null 2>&1; then
log "FATAL: inotifywait not installed (inotify-tools)"; exit 1
fi
mkdir -p "$WATCH_ROOT"
log "watching $WATCH_ROOT for .htaccess changes (debounce=${DEBOUNCE}s floor=${FLOOR}s)"
## -m monitor, -r recursive. We filter to .htaccess in the read loop rather than
## --include so this works on older inotify-tools too. modify/create/delete/move
## all matter (delete of .htaccess also changes rewrite behavior).
inotifywait -m -r -e modify,create,delete,move "$WATCH_ROOT" --format '%f' 2>/dev/null |
while read -r fname; do
case "$fname" in
.htaccess) ;;
*) continue ;;
esac
## A tenant .htaccess changed. Coalesce the save-burst, then restart ONCE.
##
## The coalesce is HARD-BOUNDED to DEBOUNCE seconds: a previous version blocked
## on `read -t DEBOUNCE` which, on a busy multi-tenant server, never timed out
## (unrelated file writes under $WATCH_ROOT kept resetting it) — so the restart
## was starved and rewrite changes silently never applied. Here we read further
## events only until the deadline OR ~2s of total quiet, whichever comes first,
## so continuous activity can delay us by at most DEBOUNCE. do_restart's FLOOR
## then rate-limits across consecutive bursts.
deadline=$(( $(date +%s) + DEBOUNCE ))
while [ "$(date +%s)" -lt "$deadline" ]; do
if read -r -t 2 _; then
continue # more activity — keep coalescing toward the deadline
else
break # ~2s of total quiet — the burst has settled
fi
done
do_restart
done
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env bash
## render-shared-ols-config.sh — assemble httpd_config.conf for the shared-ols
## tier from the per-site files the WHP panel drops into $SITES_ROOT.
##
## WHY THIS EXISTS: OpenLiteSpeed has NO top-level `include` directive (unlike
## Apache's IncludeOptional that shared-httpd relies on). So we cannot just drop
## per-vhost files in a dir and have OLS pick them up — the listener `map` lines
## and the vhost stanzas must live IN httpd_config.conf. This script is the
## "include" OLS lacks: it concatenates the panel's per-site pieces into one
## valid httpd_config.conf, then the caller issues `lswsctrl restart`.
## (Empirically established 2026-06-10 — see the OLS-tier PoC.)
##
## Per-site contract — the panel writes, for each site, a directory:
## $SITES_ROOT/<vhname>/vhconf.conf (rendered by the WHP panel from its own
## web-files/configs/shared-ols-vhconf-template.tpl
## — the single source of truth for vhost detail)
## $SITES_ROOT/<vhname>/site.meta (VHNAME=, VHROOT=, DOMAINS=a.com,www.a.com)
## This script turns each into a `virtualhost {configFile}` stanza + a listener
## `map` line. A site dir missing either file is skipped (logged).
##
## Idempotent: always rebuilds from the stock config, so re-runs never compound.
set -euo pipefail
LSWS_CONF=/usr/local/lsws/conf
TPL_DIR=${TPL_DIR:-/etc/shared-ols-templates}
SITES_ROOT=${SITES_ROOT:-$LSWS_CONF/shared-sites}
LSCACHE_ROOT=${LSCACHE_ROOT:-/var/lscache}
CERT_FILE=${CERT_FILE:-$LSWS_CONF/cert/shared-ols.crt}
KEY_FILE=${KEY_FILE:-$LSWS_CONF/cert/shared-ols.key}
export LSCACHE_ROOT
OUT="$LSWS_CONF/httpd_config.conf"
TMP="$LSWS_CONF/.httpd_config.conf.tmp.$$"
STOCK="/usr/local/lsws/.conf/httpd_config.conf"
mkdir -p "$SITES_ROOT" "$LSCACHE_ROOT"
## --- SERIALIZE concurrent renders + write ATOMICALLY ---
## The panel can fire two renders at once (parallel provisioning), and the
## in-container .htaccess watcher issues `lswsctrl restart` independently. If OLS
## (re)reads httpd_config.conf while it's half-written, it fails to parse and the
## whole tier 503s. So: (1) flock so only one render runs at a time; (2) build
## into $TMP and atomically `mv` into place at the end, so any concurrent OLS
## restart always sees a COMPLETE config (the old one until the instant of mv).
exec 9>"$LSWS_CONF/.render.lock"
## Bounded wait (-w): if a previous render is hung, fail after 30s rather than
## blocking the panel's `docker exec` call (and thus the site-save request)
## indefinitely. The caller re-tries on the next change.
flock -w 30 9 || { echo "render-shared-ols: could not acquire render lock within 30s" >&2; exit 1; }
trap 'rm -f "$TMP"' EXIT
## Sweep any stale temp configs left by a prior SIGKILL (trap EXIT doesn't run on
## SIGKILL); each render uses a unique $$ suffix so this never races a live render.
rm -f "$LSWS_CONF"/.httpd_config.conf.tmp.* 2>/dev/null || true
## From here on, build into $TMP (not $OUT).
## --- 1. start from a pristine stock config (idempotent) ---
if [ ! -f "$STOCK" ]; then
## Some image builds keep the only copy at conf/; snapshot it once so future
## renders have a clean base to strip.
mkdir -p "$(dirname "$STOCK")"
cp "$OUT" "$STOCK"
fi
## --- 2. strip stock blocks that conflict or would run PHP LOCALLY ---
## extProcessor lsphp (autoStart 1, uds) + the server scriptHandler are removed
## so this server NEVER executes PHP itself — all PHP goes to remote sidecars.
## listener HTTP/HTTPS + vhTemplate docker are removed (we add our own).
awk '
/^listener HTTP \{/ { skip=1; next }
/^listener HTTPS \{/ { skip=1; next }
/^vhTemplate docker ?\{/ { skip=1; next }
/^extProcessor lsphp ?\{/{ skip=1; next }
/^scriptHandler ?\{/ { skip=1; next }
skip && /^\}/ { skip=0; next }
!skip { print }
' "$STOCK" > "$TMP"
## --- 3. append our server-level base (real-IP, cache module, no local PHP) ---
{
echo ""
envsubst '${LSCACHE_ROOT}' < "$TPL_DIR/httpd_config_base.tpl"
} >> "$TMP"
## --- 4. emit per-site vhost stanzas + collect listener map lines ---
maps=""
site_count=0
for meta in "$SITES_ROOT"/*/site.meta; do
[ -e "$meta" ] || continue
sdir=$(dirname "$meta")
## PARSE site.meta with sed — do NOT `source` it. The panel writes these values
## (derived from DB domains), so they should be safe, but sourcing paneldata as
## shell would execute any metacharacters as root in this container if a value
## ever slipped validation. sed extraction treats them as plain data.
VHNAME=$(sed -n 's/^VHNAME=//p' "$meta" | head -1)
VHROOT=$(sed -n 's/^VHROOT=//p' "$meta" | head -1)
DOMAINS=$(sed -n 's/^DOMAINS=//p' "$meta" | head -1)
if [ -z "$VHNAME" ] || [ -z "$VHROOT" ] || [ -z "$DOMAINS" ] || [ ! -f "$sdir/vhconf.conf" ]; then
echo "render-shared-ols: skipping $sdir (incomplete: VHNAME/VHROOT/DOMAINS/vhconf.conf)" >&2
continue
fi
{
echo ""
echo "virtualhost ${VHNAME} {"
echo " vhRoot ${VHROOT}"
echo " configFile ${sdir}/vhconf.conf"
echo " allowSymbolLink 1"
echo " enableScript 1"
echo " restrained 1"
echo "}"
} >> "$TMP"
maps="${maps} map ${VHNAME} ${DOMAINS}"$'\n'
site_count=$((site_count + 1))
done
## --- 5. ALWAYS add a health vhost mapped to the catch-all so the server is
## valid with zero customer sites and HAProxy health checks (which hit by IP /
## unknown Host) get a 200. Exact-domain maps above win over this '*'. ---
{
echo ""
echo "virtualhost _health {"
echo " vhRoot /usr/local/lsws/shared-ols-health"
echo " configFile /usr/local/lsws/shared-ols-health/vhconf.conf"
echo " allowSymbolLink 1"
echo " enableScript 0"
echo "}"
} >> "$TMP"
maps="${maps} map _health *"$'\n'
## --- 6. listeners (HTTP :80 + HTTPS :443 self-signed) carrying ALL maps.
## HAProxy terminates real TLS and connects to this tier on :443 ssl verify
## none (same as shared-httpd), so :443 needs a cert — self-signed is fine. ---
{
echo ""
echo "listener shared_http {"
echo " address *:80"
echo " secure 0"
printf '%s' "$maps"
echo "}"
echo ""
echo "listener shared_https {"
echo " address *:443"
echo " secure 1"
echo " keyFile ${KEY_FILE}"
echo " certFile ${CERT_FILE}"
printf '%s' "$maps"
echo "}"
} >> "$TMP"
## --- 7. publish atomically. Validate the temp parses as non-empty, then mv into
## place (rename is atomic on the same filesystem) so a concurrent OLS restart
## never sees a half-written config. chown only the file we wrote — NOT a
## recursive chown of the whole conf tree (that was O(N-sites) on every single
## change; the per-site files are world-readable and owned correctly already). ---
if [ ! -s "$TMP" ]; then
echo "render-shared-ols: refusing to publish empty config" >&2
exit 1
fi
chown lsadm:nogroup "$TMP" 2>/dev/null || true
mv -f "$TMP" "$OUT"
echo "render-shared-ols: wrote $OUT ($site_count customer vhost(s) + health)"