From 87f154cdc8813168f63a3ee84dad76418abd07fb Mon Sep 17 00:00:00 2001 From: jknapp Date: Tue, 2 Jun 2026 20:06:56 -0700 Subject: [PATCH] =?UTF-8?q?refactor(litespeed):=20drop=20setUIDMode=20for?= =?UTF-8?q?=20shared=20lsphp=20+=20cut=20opcache=20128=E2=86=9232M?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- configs/litespeed/lsphp-overrides.ini | 9 +++++++-- configs/litespeed/site-template.tpl | 15 ++++++++++----- scripts/create-vhost-litespeed.sh | 11 +++++++++++ scripts/entrypoint-litespeed.sh | 13 +++++-------- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/configs/litespeed/lsphp-overrides.ini b/configs/litespeed/lsphp-overrides.ini index f9cd4c8..8fd7f69 100644 --- a/configs/litespeed/lsphp-overrides.ini +++ b/configs/litespeed/lsphp-overrides.ini @@ -25,9 +25,14 @@ session.gc_divisor = 1000 session.gc_maxlifetime = 1440 opcache.enable = 1 -opcache.memory_consumption = 128 +; Sized small because shmem is per-process-RSS on Linux cgroups (vs PHP-FPM's +; COW-shared model). At 128 MB × N lsphp instances we were hitting 800+ MiB +; shmem on heavy WP sites; 32 MB × N fits comfortably and still caches ~4000 +; scripts (covering Divi + WC + WP core easily). Bump per-site via WHP user +; variables (OPCACHE_MEMORY_MB) if a high-traffic site needs more. +opcache.memory_consumption = 32 opcache.interned_strings_buffer = 8 -opcache.max_accelerated_files = 10000 +opcache.max_accelerated_files = 4000 opcache.revalidate_freq = 60 opcache.enable_cli = Off diff --git a/configs/litespeed/site-template.tpl b/configs/litespeed/site-template.tpl index ad4a429..b508e54 100644 --- a/configs/litespeed/site-template.tpl +++ b/configs/litespeed/site-template.tpl @@ -8,11 +8,16 @@ allowSymbolLink 1 enableScript 1 restrained 1 -## setUIDMode 2 = DocRoot UID — lsphp suexec's to the OWNER of vhRoot. -## We chown /home/${user} to ${user}:${user} in the entrypoint, so PHP -## runs as the customer per request. Container is still the privsep -## boundary; this is the clean "scripts run as user" model. -setUIDMode 2 +## No setUIDMode — OLS itself runs as ${user} (set at server level by +## create-vhost-litespeed.sh), so lsphp inherits that uid without needing +## suEXEC per request. This is the key to single-lsphp-instance topology: +## with setUIDMode 2, each httpd worker had to lscgid-spawn its own lsphp +## (= 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/ configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhconf.conf diff --git a/scripts/create-vhost-litespeed.sh b/scripts/create-vhost-litespeed.sh index 1e25495..f058347 100644 --- a/scripts/create-vhost-litespeed.sh +++ b/scripts/create-vhost-litespeed.sh @@ -56,6 +56,17 @@ awk ' ' "$LSWS_CONF/httpd_config.conf" > "$LSWS_CONF/httpd_config.conf.new" 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 --- SENTINEL="## ---- cac-litespeed append (do not edit below) ----" { diff --git a/scripts/entrypoint-litespeed.sh b/scripts/entrypoint-litespeed.sh index 211e142..05e8eca 100644 --- a/scripts/entrypoint-litespeed.sh +++ b/scripts/entrypoint-litespeed.sh @@ -76,16 +76,13 @@ log_errors = On EOF fi -## ---- ownership: OLS master/workers run as nobody; lsphp suexecs to the -## customer per request (setUIDMode 2 in httpd_config.tpl). So the customer -## owns everything under /home/$user — clean ownership model, no nobody -## chowning. OLS's own runtime dirs stay nobody-owned. -chown -R nobody:nogroup /usr/local/lsws/logs /usr/local/lsws/conf/cert /tmp/lshttpd 2>/dev/null || true +## ---- 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" chmod 755 "/home/$user" -## logs/apache and logs/php-fpm are written by OLS (running as the customer -## via setUIDMode 2) so they need to be customer-owned, not nobody. The -## chown -R above already covers them since they're under /home/$user. ## ---- drop healthz so docker HEALTHCHECK passes before customer files ## Always rewrite as customer; suexec lsphp will read it as that uid too.