Compare commits

..

92 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
50202538e4 cac-litespeed: supervise OLS in daemon mode so self-restarts don't kill PID 1
All checks were successful
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
2837d40f00 cac-litespeed: forward real client IP to logs and PHP behind HAProxy
All checks were successful
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
cfdaae116a tune(litespeed): bump opcache 32→64 MB / 4000→8000 files + add per-site override
All checks were successful
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
87f154cdc8 refactor(litespeed): drop setUIDMode for shared lsphp + cut opcache 128→32M
All checks were successful
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
f463519998 tune(litespeed): bump LSPHP_WORKER_ESTIMATE_MB 115 → 130
All checks were successful
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
03cca745f7 feat(litespeed): wire up dynamic LSAPI tuning + idle reduction
All checks were successful
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
d1c3cfadc0 feat(litespeed): make log paths drop-in compatible with cac:phpNN
All checks were successful
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
80fa06592b perf(litespeed): defer mariadb-server + memcached install to DEV runtime
All checks were successful
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
9e13571d61 Drop stale configs/litespeed/vhconf.tpl
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m48s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m35s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m22s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 3m38s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m30s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 3m15s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m20s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 2m49s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 3m52s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 2m27s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m32s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 3m0s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m33s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 53s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 52s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 2m59s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 58s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 1m56s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 26s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m56s
Leftover from v1 direct-virtualHost iteration. Superseded by site-template.tpl
when we switched to the vhTemplate + member pattern. Nothing references it
in scripts/ or configs/; was only included in the initial commit by oversight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 07:33:06 -07:00
55c28a0c11 Add cac-litespeed image family (OpenLiteSpeed, native LSAPI)
New paid-tier per-customer image built on litespeedtech/openlitespeed:1.8.4-lsphpNN.
Matrix: 8.1-8.5. Native LSAPI suexec to customer uid, server-level LSCache,
all WP/WooCommerce extensions (memcached, redis, imagick, mbstring, etc.) baked in.

Files:
- Dockerfile.litespeed (FROM prebuilt LiteSpeed base, layers wp-cli/composer/mariadb)
- configs/litespeed/{httpd_config,site-template,lsphp-overrides}.tpl
- scripts/{entrypoint,create-vhost,detect-memory}-litespeed.sh + install-lscache-wp.sh

CI: new Build-LiteSpeed-Images matrix job. OLS_VERSION pinned to 1.8.4 (only
release with prebuilt images for all 5 PHP versions on Docker Hub).

Spec: whp/docs/superpowers/specs/2026-06-01-cac-litespeed-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 07:32:47 -07:00
Claude Code
1756d496e5 detect-memory: raise PHP_WORKER_ESTIMATE_MB default 60→128
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m22s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m20s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m15s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m19s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m17s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m25s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m16s
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 1m21s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 1m15s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 1m23s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m15s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 27s
The 60 MB worker estimate was optimistic for plugin-heavy WordPress
and WooCommerce stacks. Concrete measurement on alphaone 2026-06-01:

  Container memory : 1024 MiB (later 2048 MiB)
  Pool sized by formula : pm.max_children = (1024-100)/60 = 15
  Actual per-worker RSS : ~193 MB (anon+file+shmem from kernel OOM dumps)
  Worst-case peak       : 15 × 193 MB ≈ 2.9 GB

That math put traffic-burst peak demand well over the container cap,
producing 1,586 cumulative oom_kills across alphaone's two containers
over 18 days and intermittent fork-starvation for unrelated tenants
on the host.

128 MB is a more realistic baseline: closer to actual WP+Woo+page-
builder worker footprint, still conservative enough that lighter
sites continue to get reasonable concurrency. The matrix at common
container tiers:

  Tier (MiB)  | old children | new children | new peak demand
  256         | 2 (floored)  | 2 (floored)  | ~256 MB
  512         | 6            | 3            | ~384 MB
  768         | 11           | 5            | ~640 MB
  1024        | 15           | 7            | ~896 MB
  2048        | 15 (capped*) | 15           | ~1.9 GB
  (* old formula returned 32 at 2 GiB but production containers were
    booted at lower tiers and never recalculated; see whp01 audit.)

Existing containers keep their boot-time pm.max_children until they
are recreated — this change only affects new containers. Customers
or operators can override per-container via FPM_MAX_CHILDREN env.
2026-06-01 08:23:09 -07:00
d5d027c0ab chore(ci): trigger fresh build to verify older PHP tags repopulate
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m31s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m23s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m22s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m16s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m17s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m17s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m23s
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 1m15s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 1m22s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m14s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 26s
The registry currently only carries cac:{latest,php84,php85} and
cac-fpm:{latest,php84,php85}, even though run #49's runner log shows
all 14 jobs (74,80,81,82,83,84,85 × cac, cac-fpm) successfully pushed
on 2026-04-02. The older manifests have since been deleted from the
registry — direct probe by digest returns 404, so it's not just an
orphaned-tag situation.

We do not believe there is an active cleanup process. This empty
commit triggers a fresh push so we can confirm the workflow is still
producing all 14 images and that the tags persist after build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 07:59:12 -07:00
28bb1055da Use proxy_block placeholder in vhost template for FPM load balancing
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 3m59s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m27s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 2m18s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m4s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m15s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m28s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m17s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m23s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m26s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 1m22s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 2m21s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m12s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m16s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m40s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 27s
Replaced hardcoded SetHandler + ProxyFCGISetEnvIf directives with a
~~proxy_block~~ placeholder. The shared_httpd_manager generates either
a direct SetHandler (single container) or a mod_proxy_balancer config
(multiple containers) depending on the site's container count.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 09:03:11 -07:00
e9604b8721 Fix shared httpd log tailing for dynamically added vhosts
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m25s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m23s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 2m22s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m21s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m21s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m19s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m20s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m33s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 2m15s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m14s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m19s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 1m22s
The entrypoint used 'tail -f /var/log/httpd/*' which expands the glob
at startup. Log files created later (when new vhost configs are added)
were never tailed, so 'docker logs' showed nothing for sites added
after the container started.

Replaced with a loop that re-discovers log files every 60 seconds and
restarts tail to include new ones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:22:10 -07:00
e81b0df5b8 Reduce idle PHP-FPM memory footprint
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m22s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m7s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 2m16s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m13s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m23s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 3m31s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m2s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m21s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m23s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m51s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m4s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m6s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m17s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 26s
Opcache:
- memory_consumption: 128MB → 64MB (most WordPress sites use <40MB)
- max_accelerated_files: 10000 → 4000 (sufficient for WordPress)
- revalidate_freq: 2s → 60s (reduce stat() calls in production)
- enable_cli: Off (don't cache scripts run from command line)

FPM workers:
- process_idle_timeout: 10s → 5s (faster worker teardown when idle)
- max_requests: 500 → 200 (recycle workers sooner to release leaked memory)

These changes primarily reduce the baseline memory of idle containers
where opcache was reserving 128MB even for small sites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:58:42 -07:00
c65f533dcc Add HEIC/HEIF/AVIF support + fix MariaDB repo for AlmaLinux 10
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m6s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m23s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m55s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m39s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m35s
Cloud Apache Container / Build-and-Push (85) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (74) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (80) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (81) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (82) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (83) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (84) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (85) (push) Has been cancelled
Cloud Apache Container / Build-Shared-httpd (push) Has been cancelled
Cloud Apache Container / Build-and-Push (84) (push) Has been cancelled
Added ImageMagick-heic package to both Dockerfile and Dockerfile.fpm.
This is a separate EPEL subpackage that provides HEIC, HEIF, and AVIF
format support via libheif. Without it, ImageMagick is installed but
cannot process iPhone photos and modern image formats.

Also fixed MariaDB repo URL: AlmaLinux 10 uses $releasever=10 but
MariaDB mirrors don't have an 'almalinux10' directory. Changed to
'rhel10' which is the supported path for EL10 derivatives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:48:58 -07:00
c6f1f42987 Final vhost template: SetHandler + ProxyFCGISetEnvIf for both paths
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m21s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m21s
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 1m54s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m20s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 2m16s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m17s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m15s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 1m15s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m9s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m5s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 27s
Reverts from ProxyPassMatch back to SetHandler + ProxyFCGISetEnvIf.
ProxyPassMatch couldn't override DOCUMENT_ROOT (Apache sets it as a
CGI param after all directives run). SetHandler with unconditional
ProxyFCGISetEnvIf correctly overrides both:

- DOCUMENT_ROOT: set to /home/{user}/public_html (FPM path)
- SCRIPT_FILENAME: constructed from DOCUMENT_ROOT + SCRIPT_NAME

This fixes WordFence WAF and other plugins that use DOCUMENT_ROOT to
locate config/log files. Tested on live sites with WordPress pretty
URLs, wp-admin, static assets, and WordFence WAF optimization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:58:11 -07:00
e20f5620d7 Fix DOCUMENT_ROOT for PHP-FPM in shared httpd mode
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m19s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m5s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 2m9s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m15s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m11s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m12s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m14s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m18s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 2m14s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m51s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m27s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m0s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m12s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m6s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 1m13s
WordPress plugins like WordFence use $_SERVER['DOCUMENT_ROOT'] to locate
config/log files. With ProxyPassMatch, Apache sends its own mount path
(/mnt/users/...) as DOCUMENT_ROOT, which doesn't exist in the FPM
container.

ProxyFCGISetEnvIf can't override DOCUMENT_ROOT when using ProxyPassMatch
(Apache sets it after the directive evaluates). Instead, set it via the
FPM pool config's env[] directive which takes precedence.

create-php-config.sh now adds env[DOCUMENT_ROOT] = /home/$user/public_html
when in TCP listen mode (shared httpd), giving PHP the correct path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:04:53 -07:00
1490bde56e Switch shared vhost from SetHandler to ProxyPassMatch for PHP-FPM
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m7s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m59s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 2m3s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m26s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m21s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m51s
Cloud Apache Container / Build-FPM-Images (74) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (80) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (81) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (82) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (83) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (84) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (85) (push) Has been cancelled
Cloud Apache Container / Build-Shared-httpd (push) Has been cancelled
Cloud Apache Container / Build-and-Push (85) (push) Has been cancelled
SetHandler + ProxyFCGISetEnvIf doesn't work for path remapping because
reqenv('SCRIPT_FILENAME') is empty when the directive evaluates with
the SetHandler approach.

ProxyPassMatch directly maps .php URLs to the FPM container's filesystem
path, bypassing the SCRIPT_FILENAME rewrite issue entirely:
  ^/(.*\.php(/.*)?)$ -> fcgi://fpm:9000/home/{user}/public_html/$1

Static assets (CSS, JS, images) bypass the proxy since they don't match
\.php and are served directly by Apache from the read-only mount.

Tested and confirmed working on live site with WordPress (including
pretty URLs via .htaccess mod_rewrite).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:50:54 -07:00
e5e055d198 Fix ProxyFCGISetEnvIf syntax for SCRIPT_FILENAME rewrite
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m1s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m25s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 2m15s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m18s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m17s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m46s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m18s
Cloud Apache Container / Build-FPM-Images (81) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (82) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (83) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (84) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (85) (push) Has been cancelled
Cloud Apache Container / Build-Shared-httpd (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (80) (push) Has been cancelled
The previous expr= with s|...|...| substitution syntax doesn't exist
in Apache expressions — it silently failed, leaving SCRIPT_FILENAME
pointing to /mnt/users/ which PHP-FPM can't find.

Fixed to use regex match in the conditional with backreferences:
  reqenv('SCRIPT_FILENAME') =~ m#^/mnt/users/([^/]+)/([^/]+)/public_html(.*)#
  -> /home/$1/public_html$3

This is also generic (captures user from the path) so the template
no longer needs per-user placeholder substitution for this directive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:32:52 -07:00
c68b555a5f Fix PHP-FPM path mismatch in shared httpd vhost template
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m9s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m12s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m57s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m25s
Cloud Apache Container / Build-and-Push (84) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (85) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (74) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (80) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (81) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (82) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (83) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (84) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (85) (push) Has been cancelled
Cloud Apache Container / Build-Shared-httpd (push) Has been cancelled
Cloud Apache Container / Build-and-Push (83) (push) Has been cancelled
The shared httpd serves files from /mnt/users/{user}/{domain}/public_html
but PHP-FPM containers have them at /home/{user}/public_html. When Apache
proxied PHP requests via fcgi, SCRIPT_FILENAME pointed to the Apache path
which doesn't exist inside the FPM container, causing "File not found".

Added ProxyFCGISetEnvIf to rewrite SCRIPT_FILENAME from the shared httpd
path to the FPM container path before proxying the request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:22:53 -07:00
7f7cb456f0 Add openssl to package installs for AlmaLinux 10
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m16s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m31s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 2m18s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 3m19s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m15s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m22s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m12s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m19s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m23s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 3m18s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m21s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m57s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 35s
AlmaLinux 10 base image does not include openssl by default (AL9 did).
Add it explicitly to all three Dockerfiles since it's needed for
self-signed cert generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:11:10 -07:00
dc6ce2bf12 Upgrade base image from AlmaLinux 9 to AlmaLinux 10
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Failing after 1m14s
Cloud Apache Container / Build-and-Push (80) (push) Failing after 1m46s
Cloud Apache Container / Build-and-Push (81) (push) Failing after 2m11s
Cloud Apache Container / Build-and-Push (82) (push) Failing after 1m7s
Cloud Apache Container / Build-and-Push (83) (push) Failing after 1m6s
Cloud Apache Container / Build-and-Push (84) (push) Failing after 1m53s
Cloud Apache Container / Build-and-Push (85) (push) Failing after 1m14s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m7s
Cloud Apache Container / Build-FPM-Images (81) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (82) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (83) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (84) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (85) (push) Has been cancelled
Cloud Apache Container / Build-Shared-httpd (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (80) (push) Has been cancelled
Bump all three Dockerfiles to almalinux/10-base with matching EPEL 10
and Remi 10 repository URLs. AlmaLinux 10.1 has been stable since Nov
2025. All PHP versions (7.4-8.5) confirmed available via Remi for EL10.

Also removes --allowerasing from shared-httpd Dockerfile since AL10
base does not ship curl-minimal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:55:26 -07:00
fc55752379 Fix curl-minimal conflict in shared-httpd Dockerfile
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Successful in 3m32s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 3m36s
Cloud Apache Container / Build-and-Push (82) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (83) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (84) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (85) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (74) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (80) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (81) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (82) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (83) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (84) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (85) (push) Has been cancelled
Cloud Apache Container / Build-Shared-httpd (push) Has been cancelled
Cloud Apache Container / Build-and-Push (81) (push) Has been cancelled
The almalinux/9-base image ships curl-minimal which conflicts with the
full curl package. Add --allowerasing to allow dnf to replace it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:44:24 -07:00
367da7806c Fix ImageMagick install: use EPEL packages instead of upstream RPMs
Some checks failed
Cloud Apache Container / Build-and-Push (80) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (81) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (82) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (83) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (84) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (85) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (74) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (80) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (81) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (82) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (83) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (84) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (85) (push) Has been cancelled
Cloud Apache Container / Build-Shared-httpd (push) Has been cancelled
Cloud Apache Container / Build-and-Push (74) (push) Has been cancelled
The official ImageMagick 7.1.2-18 RPMs require GLIBC 2.38 which is not
available on AlmaLinux 9 (ships GLIBC 2.34). Switch to EPEL-provided
ImageMagick packages which are built for EL9 and guaranteed compatible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:44:03 -07:00
a5cb45a386 Install latest ImageMagick 7.1.2-18 from official RPMs
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Failing after 1m39s
Cloud Apache Container / Build-and-Push (80) (push) Failing after 1m11s
Cloud Apache Container / Build-and-Push (81) (push) Failing after 1m31s
Cloud Apache Container / Build-and-Push (82) (push) Failing after 54s
Cloud Apache Container / Build-and-Push (83) (push) Failing after 1m46s
Cloud Apache Container / Build-and-Push (84) (push) Failing after 1m47s
Cloud Apache Container / Build-and-Push (85) (push) Failing after 56s
Cloud Apache Container / Build-FPM-Images (74) (push) Failing after 1m42s
Cloud Apache Container / Build-FPM-Images (80) (push) Failing after 1m1s
Cloud Apache Container / Build-FPM-Images (81) (push) Failing after 55s
Cloud Apache Container / Build-FPM-Images (82) (push) Failing after 55s
Cloud Apache Container / Build-FPM-Images (83) (push) Failing after 59s
Cloud Apache Container / Build-FPM-Images (84) (push) Failing after 55s
Cloud Apache Container / Build-FPM-Images (85) (push) Failing after 57s
Cloud Apache Container / Build-Shared-httpd (push) Failing after 26s
Adds ImageMagick and ImageMagick-libs from the official CentOS x86_64
RPMs before PHP installation so php-pecl-imagick links against the
latest version. Applied to both Dockerfile (standalone) and
Dockerfile.fpm (shared httpd mode).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:15:15 -07:00
c78167871c Add shared httpd + PHP-FPM-only container architecture
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m22s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 3m14s
Cloud Apache Container / Build-and-Push (82) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (83) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (84) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (85) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (74) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (80) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (81) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (82) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (83) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (84) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (85) (push) Has been cancelled
Cloud Apache Container / Build-Shared-httpd (push) Has been cancelled
Cloud Apache Container / Build-and-Push (81) (push) Has been cancelled
Separate Apache and PHP-FPM into distinct container roles to reduce
per-customer memory overhead on shared servers. Adds three new images:
- Dockerfile.fpm: PHP-FPM only (no Apache), listens on TCP port 9000
- Dockerfile.shared-httpd: Apache only (no PHP), with SSL and proxy_fcgi
- Existing Dockerfile unchanged for standalone mode

Key changes:
- detect-memory.sh: CONTAINER_ROLE env var (combined/fpm_only/httpd_only)
  controls the memory budget split
- create-php-config.sh: FPM_LISTEN env var for TCP port vs Unix socket,
  added /fpm-ping and /fpm-status health endpoints
- New entrypoints for each container role
- tune-mpm.sh for hot-adjusting Apache MPM settings
- shared-vhost-template.tpl with proxy_fcgi and SSL on port 443
- CI/CD builds all three image types in parallel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:08:00 -07:00
87c4f2befc Optimize Apache & PHP-FPM memory for lower idle usage
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m31s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m54s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m51s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m52s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m39s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m58s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m51s
Switch PHP-FPM from pm=dynamic to pm=ondemand (zero idle workers),
auto-detect container memory via cgroups to calculate appropriate
limits, and generate Apache MPM config at runtime. All tuning values
are now overridable via environment variables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 18:52:15 -08:00
a153385d8f Adding support for PHP 8.5
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m12s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m46s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m47s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m44s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m47s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m46s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m47s
2026-02-08 07:57:04 -08:00
468bc7b088 Move user crontab to persistent home directory
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m52s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m48s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m45s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m54s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m50s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m48s
- Created user-specific crontab file at /home/$user/crontab
- Crontab now persists through container restarts/refreshes
- Users can manage their own cron jobs by editing their crontab file
- Automatically loads user crontab on container start
- Updated DEV environment to use user crontab for MySQL backups

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-13 07:36:35 -07:00
8b9708e351 Add essential development tools to container
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 3m31s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m55s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m58s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m52s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m48s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 3m24s
Added git, nano, rsync, unzip, zip, mariadb client, bind-utils, jq, patch, nc, tree, and dos2unix to provide developers with commonly needed tools for PHP development and debugging.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-13 07:19:25 -07:00
92ed9885ec Remove php-ioncube-loader from PHP 8.1 to fix Composer installation
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m48s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m44s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m42s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m43s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m47s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m15s
The php-ioncube-loader package is incompatible with PHP 8.1 and was causing
a segmentation fault (exit code 139) when the Composer installer tried to
run PHP. This aligns PHP 8.1 with other PHP versions that already had
ioncube-loader removed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-12 16:41:33 -07:00
844b21bd7c Add Composer to container for PHP dependency management
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Successful in 3m1s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 2m0s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m58s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m3s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m2s
Cloud Apache Container / Build-and-Push (81) (push) Failing after 1m28s
- Install Composer globally at /usr/local/bin/composer
- Available for all PHP versions and users
- Also includes previously added microdnf and less utilities

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-12 15:50:18 -07:00
3d903b437f Fix PHP error log path to use correct user directory
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m45s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m39s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m38s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m39s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m42s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m50s
PHP error logs were incorrectly being written to /etc/httpd/logs/error_log
instead of the expected /home/$user/logs/php-fpm/ directory. Updated the
php_admin_value[error_log] setting to point to the proper location.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-31 10:21:15 -07:00
152dd413ef adding claude infor
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 3m12s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m37s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m54s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m23s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m8s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 3m21s
2025-07-28 07:29:03 -07:00
617fdbcd21 Add PostgreSQL support for all PHP versions
- Added postgresql-devel package to Dockerfile for client libraries
- Added php-pgsql extension to all PHP versions (7.4, 8.0, 8.1, 8.2, 8.3, 8.4)
- Enables PHP applications to connect to PostgreSQL databases

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-28 07:27:39 -07:00
154f42ae09 Optimize memory usage for Apache and PHP-FPM, remove ioncube-loader
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 3m7s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m42s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m37s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m39s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 3m3s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m40s
- Apache mpm_event: Reduced StartServers from 10 to 2, adjusted spare threads
  and worker limits for container environments
- PHP-FPM: Switched from static to dynamic process management with lower
  process counts (5 max children instead of 10)
- Removed php-ioncube-loader from PHP 8.0 installation
- Expected memory reduction: 60-70% in idle state while maintaining responsiveness

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 16:52:04 -07:00
b5857d73c2 Fix issue where PHP Sessions were not working as expected
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m37s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 44s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m41s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m39s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m37s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m35s
2025-07-20 12:06:15 -07:00
b1de7021a3 fix build issues
All checks were successful
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m44s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m38s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 8m24s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 5m1s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 7m30s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 8m55s
2025-07-16 08:01:07 -07:00
9f8beb45b8 Switching builds to include PHP version to limit memory requirements on deploy.
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Failing after 56s
Cloud Apache Container / Build-and-Push (80) (push) Failing after 36s
Cloud Apache Container / Build-and-Push (81) (push) Failing after 56s
Cloud Apache Container / Build-and-Push (82) (push) Failing after 55s
Cloud Apache Container / Build-and-Push (83) (push) Failing after 40s
Cloud Apache Container / Build-and-Push (84) (push) Failing after 57s
improve build size and speed for images.
2025-07-16 07:55:03 -07:00
88f462eb04 Updated the README.md
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 1m1s
Added a healthcheck to the container
adjusted Apache limits for memory consumption
switch to microdnf for improved memory usage
2025-07-16 05:56:33 -07:00
e7b0bce666 Update MariaDB Version
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 41s
2025-06-14 16:02:00 -07:00
5a097034c4 Update MariaDB Version
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 44s
2025-03-16 11:12:46 -07:00
a41157fad0 fix url
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 39s
2025-03-16 10:58:25 -07:00
4fd7ee465a Adding support for PHP 8.4 and upgrading MariaDB to 11.4.5
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 47s
2025-03-16 10:43:16 -07:00
8a7490ef98 forgot to add iproute
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 1m32s
2024-12-17 21:52:38 -08:00
jknapp
9df776ef08 Merge pull request 'fix path to remote_ip.conf' (#18) from update-to-fix-ip-and-options-issue into trunk
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 39s
Reviewed-on: #18
2024-12-18 05:47:59 +00:00
7bab6d39fc fix path to remote_ip.conf 2024-12-17 21:47:32 -08:00
jknapp
9630408ca0 Merge pull request 'Added fix for issues found while setting up anhonesthost' (#17) from update-to-fix-ip-and-options-issue into trunk
Some checks failed
Cloud Apache Container / Build-and-Push (push) Failing after 8s
Reviewed-on: #17
2024-12-18 05:44:51 +00:00
49c5438866 Added fix for issues found while setting up anhonesthost 2024-12-17 21:44:09 -08:00
jknapp
885deb5979 Merge pull request 'fix spacing on versions' (#16) from fix-readme into trunk
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 35s
Reviewed-on: #16
2024-10-16 01:50:31 +00:00
23253e9f37 fix spacing on versions 2024-10-15 18:50:11 -07:00
jknapp
fde567d5f9 Merge pull request 'Fix/Update README.md' (#15) from fix-readme into trunk
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 35s
Reviewed-on: #15
2024-10-16 01:45:51 +00:00
b2675abc30 Fix/Update README.md 2024-10-15 18:44:09 -07:00
jknapp
aab89a7412 Merge pull request 'Update for log rotation and backups' (#14) from log-cleanup into trunk
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 1m22s
Reviewed-on: #14
2024-10-15 02:32:44 +00:00
527ba5cf58 Adding better backups and log rotation, and updating files around it 2024-10-14 19:30:51 -07:00
bbd2de6792 Update for log rotation and backups 2024-10-14 12:15:11 -07:00
jknapp
ed9ba0118b Update to reflect changes for user directory
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 41s
2024-10-14 17:28:24 +00:00
jknapp
715b998404 Update README to reflect gitea address and adding logs
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 1m22s
2024-10-14 17:25:10 +00:00
jknapp
7d988b338c Merge pull request 'Fixing script to add more time for startup and add backup crons for database' (#13) from fix-script into trunk
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 34s
Reviewed-on: #13
2024-10-02 20:22:22 +00:00
b3e284a547 Fixing script to add more time for startup and add backup crons for database 2024-10-02 13:21:49 -07:00
jknapp
565482764d Merge pull request 'Update script to default to PHP 8.3 and have options' (#12) from add-php83 into trunk
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 32s
Reviewed-on: #12
2024-10-02 18:55:46 +00:00
3d3e353c66 Update script to default to PHP 8.3 and have options 2024-10-02 11:55:22 -07:00
jknapp
0373eb4ea8 Merge pull request 'Fix script host location' (#11) from add-php83 into trunk
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 33s
Reviewed-on: #11
2024-10-02 16:44:31 +00:00
36757fac8f fix docker command 2024-10-02 09:43:47 -07:00
0c8bdc4f04 Update local-dev script 2024-10-02 09:40:53 -07:00
jknapp
f1ab086228 Merge pull request 'Adding PHP 8.3 and updating README.md for moving repos' (#10) from add-php83 into trunk
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 33s
Reviewed-on: #10
2024-10-02 16:02:24 +00:00
520af5b3a8 Adding PHP 8.3 and updating README.md for moving repos 2024-10-02 08:58:42 -07:00
jknapp
06a7cbc88d Merge pull request 'fix push target' (#9) from add-ci into trunk
All checks were successful
Cloud Apache Container / Build-and-Push (push) Successful in 1m9s
Reviewed-on: #9
2024-10-01 21:20:09 +00:00
b1ec63617a fix push target 2024-10-01 14:19:51 -07:00
jknapp
5ead6ed456 Merge pull request 'fix push target' (#8) from add-ci into trunk
Some checks failed
Cloud Apache Container / Build-and-Push (push) Failing after 47s
Reviewed-on: #8
2024-10-01 21:17:55 +00:00
b38b80e6fc fix push target 2024-10-01 14:09:12 -07:00
jknapp
b53a4999bf Merge pull request 'fix push target' (#7) from add-ci into trunk
Some checks failed
Cloud Apache Container / Build-and-Push (push) Failing after 36s
Reviewed-on: #7
2024-10-01 21:08:03 +00:00
49f2266974 fix push target 2024-10-01 14:07:22 -07:00
jknapp
abb1da3a0f Merge pull request 'fix push target' (#6) from add-ci into trunk
Some checks failed
Cloud Apache Container / Build-and-Push (push) Failing after 59s
Reviewed-on: #6
2024-10-01 21:04:09 +00:00
ac5c70d26b fix push target 2024-10-01 14:03:29 -07:00
jknapp
1d4d440a88 Merge pull request 'fix branch' (#5) from add-ci into trunk
Some checks failed
Cloud Apache Container / Build-and-Push (push) Failing after 1m5s
Reviewed-on: #5
2024-10-01 21:00:31 +00:00
5108689aa4 fix branch 2024-10-01 14:00:12 -07:00
jknapp
3d51a63ae4 Merge pull request 'First attempt at creating CI with Gitea Actions' (#4) from add-ci into trunk
Reviewed-on: #4
2024-10-01 20:57:37 +00:00
4ba4b7ae1e First attempt at creating CI with Gitea Actions 2024-10-01 13:57:01 -07:00
jknapp
07999c4252 Merge pull request 'update for prod run' (#3) from update-for-prod into trunk
Reviewed-on: #3
2024-08-13 01:21:13 +00:00
root
90841ada03 update for prod run 2024-08-12 21:20:00 -04:00
jknapp
b2b3d284a6 Merge pull request 'Streamline WordPress Setup' (#2) from add-script into trunk
Reviewed-on: #2
2024-01-31 00:44:15 +00:00
b6fe0d77fd update script to setup the config 2024-01-30 16:40:42 -08:00
2e912bc4ab update script 2024-01-30 14:01:46 -08:00
50 changed files with 2878 additions and 187 deletions

39
.dockerignore Normal file
View File

@@ -0,0 +1,39 @@
# Ignore version control
.git
.gitignore
# Ignore CI/CD and workflow files
.gitea/
.github/
.gitlab/
# Ignore local development files
*.swp
*.swo
*.bak
*.tmp
*.log
# Ignore OS and editor files
.DS_Store
Thumbs.db
.vscode/
.idea/
# Ignore test and documentation files
tests/
docs/
README*
# Ignore node and Python artifacts (if present)
node_modules/
__pycache__/
# Ignore build output
dist/
build/
# Ignore secrets and configs
*.env
.env.*
secrets/

View File

@@ -0,0 +1,220 @@
name: Cloud Apache Container
run-name: ${{ gitea.actor }} pushed a change to trunk
on:
push:
branches:
- trunk
jobs:
Build-and-Push:
runs-on: ubuntu-latest
strategy:
matrix:
phpver: [74, 80, 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 Image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }}
Build-FPM-Images:
runs-on: ubuntu-latest
strategy:
matrix:
phpver: [74, 80, 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 FPM Image
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.fpm
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }}
Build-LiteSpeed-Images:
runs-on: ubuntu-latest
strategy:
matrix:
# PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base
# images stop at older OLS releases for those PHP versions, and the
# cac-litespeed tier is a paid premium offering: 8.1+ is the
# modernization story we're selling.
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 LiteSpeed Image
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.litespeed
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
OLS_VERSION=1.8.4
# OLS_VERSION pinned to 1.8.4 — only release with prebuilt images
# for every PHP version we ship (1.8.5 and 1.9.0 don't have an
# lsphp81 variant on Docker Hub). Bump alongside a local rebuild
# test when LiteSpeed publishes lsphp81 on a newer OLS release.
# See spec: docs/superpowers/specs/2026-06-01-cac-litespeed-design.md
tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
${{ 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 (8185): 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:
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 httpd Image
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.shared-httpd
platforms: linux/amd64
push: true
tags: |
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

86
CLAUDE.md Normal file
View File

@@ -0,0 +1,86 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Cloud Apache Container (CAC) is a Docker-based PHP web hosting environment that supports multiple PHP versions (7.4 through 8.4) with Apache, designed for both local development and production deployment.
## Common Development Commands
### Local Development Setup
```bash
# Quick start with automated setup (creates helper scripts)
./local-dev.sh -n local-dev
# With specific PHP version
./local-dev.sh -n myproject -a 84 # PHP 8.4
# Helper scripts created by local-dev.sh:
./instance_start # Start container
./instance_stop # Stop container
./instance_logs # Tail Apache logs
./instance_db_info # Show MySQL credentials
```
### Building and Testing
```bash
# Build container locally
docker build -t cac:latest .
# Build with specific PHP version
docker build --build-arg PHP_VER=83 -t cac:php83 .
# Run container manually
docker run -d -p 80:80 -p 443:443 \
-e PHPVER=83 -e environment=DEV \
-e uid=$(id -u) -e user=$(whoami) -e domain=localhost \
-v"$(pwd)/user":/home/$(whoami) \
--name test-container cac:latest
```
### Server Deployment
- Production git directory: `/root/whp`
- After `git pull`, sync web files: `rsync -av web-files/ /docker/whp/web/`
## Architecture and Key Components
### Directory Structure
- `/scripts/` - Container setup scripts (entrypoint, PHP installers, vhost creation)
- `/config/` - Apache and PHP configuration files
- `/web-files/` - Default web content (ping endpoint)
- `/.gitea/workflows/` - CI/CD pipeline for multi-PHP version builds
### Container Behavior
1. **Entrypoint Flow** (`scripts/entrypoint.sh`):
- Creates user with specified UID
- Sets up directory structure
- Configures Apache vhost based on environment variables
- In DEV mode: starts MariaDB and Memcached
- Starts Apache and PHP-FPM
2. **Environment Modes**:
- **DEV** (`environment=DEV`): Local database, memcached, automatic backups
- **PROD** (default): Expects external database/cache services
3. **PHP Version Management**:
- Controlled via `PHPVER` environment variable (74, 80, 81, 82, 83, 84)
- Each version has dedicated install script in `/scripts/`
- PHP-FPM configuration dynamically created based on version
### Key Environment Variables
- `uid` (required): User ID for file permissions
- `user` (required): Username for container user
- `domain` (required): Primary domain for Apache vhost
- `serveralias`: Additional domains (comma-separated)
- `PHPVER`: PHP version to use (default: 83)
- `environment`: DEV or PROD mode
## Important Technical Details
1. **Health Check**: Available at `/ping` endpoint
2. **Logs Location**: `/home/$user/logs/apache/` and `/home/$user/logs/php-fpm/`
3. **Database Backups** (DEV mode): Every 15 minutes to `/home/$user/_db_backups/`
4. **Log Rotation**: Compress after 3 days, delete after 7 days
5. **SSL**: Self-signed certificate auto-generated, proper SSL configured
6. **WordPress**: WP-CLI pre-installed for WordPress development

View File

@@ -1,23 +1,50 @@
FROM almalinux/9-base
ARG PHPVER=81
#RUN dnf update -y && dnf upgrade -y
RUN dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm -y
RUN dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
#RUN dnf update -y && dnf upgrade -y
RUN dnf install -y httpd mod_ssl wget procps
RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt
RUN mkdir /run/php-fpm/
RUN mkdir /scripts
COPY ./scripts/* /scripts/
FROM almalinux/10-base
ARG PHPVER=83
# Install repos, update, install only needed packages, clean up in one layer
RUN dnf install -y \
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
dnf update -y && \
dnf install -y httpd mod_ssl openssl wget procps cronie iproute postgresql-devel microdnf less git \
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix && \
dnf clean all && \
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
# Copy scripts into the image and set permissions
COPY ./scripts/ /scripts/
RUN chmod +x /scripts/*
#RUN /scripts/install-php$PHPVER.sh
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
RUN chmod +x wp-cli.phar
RUN mv wp-cli.phar /usr/local/bin/wp
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
dnf clean all
# Generate self-signed cert, create needed dirs, install PHP, clean up
RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt && \
mkdir -p /run/php-fpm/ && \
/scripts/install-php$PHPVER.sh && \
rm -rf /tmp/*
# Download and install wp-cli (consider pinning version for reproducibility)
RUN curl -L -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
chmod +x /usr/local/bin/wp
# Download and install Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \
chmod +x /usr/local/bin/composer
# Copy configs and web files
COPY ./configs/default-index.conf /etc/httpd/conf.d/
COPY ./configs/prod-php.ini /etc/php.ini
COPY ./configs/phpinfo.php /var/www/html/
COPY ./configs/mariadb.repo /etc/yum.repos.d/
COPY ./configs/index.php /var/www/html/
RUN yum clean all
COPY ./configs/remote_ip.conf /etc/httpd/conf.d/
# Set up cron job in a single layer
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD curl -f http://localhost/ || exit 1
ENTRYPOINT [ "/scripts/entrypoint.sh" ]

47
Dockerfile.fpm Normal file
View File

@@ -0,0 +1,47 @@
FROM almalinux/10-base
ARG PHPVER=83
# Install repos, update, install only needed packages (no httpd/mod_ssl), clean up in one layer
RUN dnf install -y \
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
dnf update -y && \
dnf install -y openssl wget procps cronie iproute postgresql-devel microdnf less git \
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix fcgi && \
dnf clean all && \
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
# Copy scripts into the image and set permissions
COPY ./scripts/ /scripts/
RUN chmod +x /scripts/*
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
dnf clean all
# Create needed dirs, install PHP, clean up (no SSL cert, no httpd)
RUN mkdir -p /run/php-fpm/ && \
/scripts/install-php$PHPVER.sh && \
rm -rf /tmp/*
# Download and install wp-cli
RUN curl -L -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
chmod +x /usr/local/bin/wp
# Download and install Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \
chmod +x /usr/local/bin/composer
# Copy configs (PHP only, no Apache configs)
COPY ./configs/prod-php.ini /etc/php.ini
COPY ./configs/mariadb.repo /etc/yum.repos.d/
# Set up cron job for log rotation
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
EXPOSE 9000
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD SCRIPT_FILENAME=/fpm-ping SCRIPT_NAME=/fpm-ping REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 | grep -q pong || exit 1
ENTRYPOINT [ "/scripts/entrypoint-fpm.sh" ]

97
Dockerfile.litespeed Normal file
View File

@@ -0,0 +1,97 @@
## cac-litespeed — OpenLiteSpeed customer container, LSAPI-native.
##
## Built on top of the LiteSpeed-maintained prebuilt image rather than
## installed-from-RPM on AlmaLinux 10. Rationale:
## - The EL10 RPM ships an empty /usr/local/lsws/cgid/ directory (the
## lscgid suexec helper is built by the upstream tarball install.sh,
## not packaged), which makes LSAPI unusable.
## - The prebuilt image is Ubuntu 24.04-based and includes lsphp +
## everything WP/WooCommerce needs out of the box (memcached, redis,
## imagick, mbstring, mysqlnd, intl, gd, soap, bcmath, gmp, sodium,
## opcache, ...) — saves us a dozen explicit installs and avoids the
## libonig.so.105 packaging bug entirely.
## - LiteSpeed Inc maintains it; OLS upgrades become a base-image bump.
##
## Tradeoff vs the rest of the CAC family: this image is Ubuntu-based,
## not AlmaLinux. The "cac" naming is now slightly misleading (it's no
## longer Cloud *Apache* Container, it's Cloud LiteSpeed Container) but
## the panel doesn't care and the customer-facing contract is identical.
## ARG before FROM is special — it can be used in the FROM line, but the
## value goes out of scope inside the image, so we re-declare ARG PHPVER
## after FROM for any RUN steps that need it.
ARG OLS_VERSION=1.8.5
ARG PHPVER=83
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
ARG PHPVER=83
ENV PHPVER=${PHPVER}
## Tooling we layer on top of the base:
## - gettext-base: envsubst for runtime template rendering
## - sudo: install-lscache-wp.sh runs wp-cli as the customer user
## - composer: not in the base image (wp-cli is)
## - cron: customer crontab support (mirror cac:phpXX behaviour)
## - 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.
## 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
## from LiteSpeed — customers needing imap should pin to 8.3 or earlier.
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gettext-base sudo cron \
ca-certificates curl wget \
lsphp${PHPVER}-ldap && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
## Composer (matches the wp-cli pattern in the existing CAC Dockerfile —
## phar download, no `php install` pipe since lsphp's CLI mode is fine).
RUN curl -fsSL -o /usr/local/bin/composer https://getcomposer.org/download/latest-stable/composer.phar && \
chmod +x /usr/local/bin/composer
## Our scripts + config templates layer in last (they change most often,
## keep them off the slow apt layer).
COPY ./scripts/entrypoint-litespeed.sh \
./scripts/create-vhost-litespeed.sh \
./scripts/detect-memory-litespeed.sh \
./scripts/install-lscache-wp.sh \
./scripts/log-rotate.sh \
/scripts/
RUN chmod +x /scripts/*
COPY ./configs/litespeed/ /etc/lsws-templates/
## Apply our production lsphp ini overrides. Ask lsphp for its scan dir
## directly (varies by PHP minor version: 8.3/8.4/8.5 each have their own
## /usr/local/lsws/lsphpNN/etc/php/8.M/mods-available/). Dockerfile RUN uses
## /bin/sh so we explicitly `bash -c` for safer scripting.
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"'
## Disable the OLS WebAdmin port for customer-facing containers. Bind admin
## listener to loopback so it's unreachable even from the docker network.
RUN sed -i 's|^[[:space:]]*address[[:space:]]\+\*:| address 127.0.0.1:|' \
/usr/local/lsws/admin/conf/admin_config.conf 2>/dev/null || true
## Cron entry for log rotation (mirrors cac:phpXX).
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
EXPOSE 80 443
## Healthcheck: the entrypoint drops a static /healthz into the customer
## docroot at boot, so this passes even before any customer files exist.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsSk https://127.0.0.1/healthz || exit 1
ENTRYPOINT ["/scripts/entrypoint-litespeed.sh"]

63
Dockerfile.lsphp Normal file
View File

@@ -0,0 +1,63 @@
## 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"'
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"]

40
Dockerfile.shared-httpd Normal file
View File

@@ -0,0 +1,40 @@
FROM almalinux/10-base
# Install Apache and minimal dependencies (no PHP at all)
RUN dnf install -y \
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm && \
dnf update -y && \
dnf install -y httpd mod_ssl openssl iproute cronie procps curl && \
dnf clean all && \
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
# Copy scripts and set permissions
COPY ./scripts/detect-memory.sh /scripts/detect-memory.sh
COPY ./scripts/create-apache-mpm-config.sh /scripts/create-apache-mpm-config.sh
COPY ./scripts/log-rotate.sh /scripts/log-rotate.sh
COPY ./scripts/entrypoint-shared-httpd.sh /scripts/entrypoint-shared-httpd.sh
COPY ./scripts/tune-mpm.sh /scripts/tune-mpm.sh
RUN chmod +x /scripts/*
# Generate self-signed SSL cert (same as main CAC image)
RUN openssl req -newkey rsa:2048 -nodes \
-keyout /etc/pki/tls/private/localhost.key \
-x509 -days 3650 -subj "/CN=localhost" \
-out /etc/pki/tls/certs/localhost.crt
# Copy Apache configs
COPY ./configs/remote_ip.conf /etc/httpd/conf.d/
COPY ./configs/default-index.conf /etc/httpd/conf.d/
# Create vhosts directory (will be volume-mounted from host)
RUN mkdir -p /etc/httpd/conf.d/vhosts
# Set up cron job for log rotation
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
EXPOSE 80 443
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD curl -sfk https://localhost/ping || exit 1
ENTRYPOINT [ "/scripts/entrypoint-shared-httpd.sh" ]

57
Dockerfile.shared-ols Normal file
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"]

151
README.md
View File

@@ -1,71 +1,134 @@
# Cloud Apache Container #
This is the base container for running PHP based applications. Select the PHP version environment variables. PHP Version Defaults to PHP 8.1
# Cloud Apache Container
*__You mush have docker or compatable containerization software running.__*
This is a base container for running PHP-based applications, supporting multiple PHP versions (7.4, 8.0, 8.1, 8.2, 8.3, 8.4). The default is PHP 8.3. The container is based on AlmaLinux 9 and uses Apache with mod_ssl. It is designed for both development and production use.
__You can pull this image locally by running:__
**You must have Docker or compatible containerization software running.**
```console
docker pull public.ecr.aws/s1f6k4w4/cac:latest
---
## What's New?
- **Optimized Image:** The Dockerfile has been refactored for smaller size, faster builds, and improved security. Unnecessary files and caches are removed during build.
- **Pre-built Images for Each PHP Version:** On every push, images for all supported PHP versions are built and pushed to the registry. You can pull the exact version you need (e.g., `cac:php74`, `cac:php84`, or `cac:latest`).
- **.dockerignore Added:** The build context is now minimized, making builds faster and more secure.
---
## Quick Start: Local Development with `local-dev.sh`
The easiest way to start a local development environment is with the provided `local-dev.sh` script. This script automates container setup, volume creation, log directories, and WordPress installation.
### Usage Example
```bash
./local-dev.sh -n local-dev
```
__You can then run a development version of the server by running the following commands:__
*Note this is an example, you can modify the command(s) to fit your needs.*
**Flags:**
- `-n` Name of the container (required)
- `-p` HTTP port (default: 80)
- `-s` HTTPS port (default: 443)
- `-r` Root path for files and database (default: current directory)
- `-a` PHP version (default: 8.3; options: 74, 80, 81, 82, 83, 84)
- `-v` Enable verbose mode
- `-h` Show help
```console
The script will:
- Create a user directory and log folders
- Create a Docker volume for MySQL
- Start the container with the correct environment variables
- Generate helper scripts in your root path:
- `instance_start` Start the container
- `instance_stop` Stop the container
- `instance_logs` Tail Apache logs
- `instance_db_info` Show MySQL credentials
- Install WordPress in your web root
- Print MySQL credentials
---
## Manual Docker Usage
You can also run the container manually:
```bash
mkdir -p local-development/domain.tld
cd local-development/domain.tld
mkdir {web,db}
docker run -it -p 80:80 -p 443:443 -e PHPVER=81 -e environment=DEV --mount type=bind,source="$(pwd)"/web,target=/home/myuser/public_html --mount type=bind,source="$(pwd)"/db,target=/var/lib/mysql -e uid=30001 -e user=myuser -e domain=domain.tld -e serveralias=www.domain.tld --name local-dev public.ecr.aws/s1f6k4w4/cac:latest
mkdir user
mkdir -p user/logs/{apache,system}
docker run -d -it -p 80:80 -p 443:443 -e PHPVER=84 -e environment=DEV --mount type=bind,source="$(pwd)"/user,target=/home/myuser -v"$name-mysql":/var/lib/mysql -e uid=30001 -e user=myuser -e domain=localhost --name local-dev repo.anhonesthost.net/cloud-hosting-platform/cac:latest
```
*This will start the processes needed to run sites locally.*
---
The first time you start the container, it will take some time as it is installing all the required software to run the dev instance.
## Accessing the Container
__If you need to get into the container you can run:__
```console
```bash
docker exec -it local-dev /bin/bash
```
__To install WordPress for your site__
---
```console
cat /var/lib/mysql/creds
## WordPress Installation
If using `local-dev.sh`, WordPress is installed automatically. For manual setup:
```bash
cat /home/myuser/mysql_creds
su - myuser
cd ~/public_html
wp core download
```
You should be able to then go into your browser and go to https://localhost (accept the SSL warning if it appears) and follow the prompts to setup the site.
Then visit https://localhost (accept the SSL warning) to complete setup.
The database credentials are shown in the /var/lib/mysql/creds file, which we had *cat* in the commands above.
---
### PHPVER ###
*74* - PHP 7.4
*80* - PHP 8.0
*81* - PHP 8.1
*82* - PHP 8.2
## Features
### Environment Variables ###
__Required Tags__
*uid* - User ID for File Permissions
*user* - Username for File Permissions
*domain* - Primary Domain for configuration
- **Multiple PHP Versions:** 7.4, 8.0, 8.1, 8.2, 8.3, 8.4 (set with `PHPVER` or `-a` flag)
- **Pre-built Images:** Pull the image for your desired PHP version directly from the registry. No need to build locally unless customizing.
- **Optimized Build:** Smaller, faster, and more secure images thanks to the improved Dockerfile and `.dockerignore`.
- **Automatic Database Setup:** MariaDB is started in DEV mode, credentials are auto-generated and stored in `/home/$user/mysql_creds`.
- **Database Backups:** Cron job backs up the database every 15 minutes to `/home/$user/_db_backups`.
- **Log Management:** Log rotation compresses logs older than 3 days and deletes those older than 7 days.
- **Memcached:** Started automatically in DEV mode.
- **SSL:** Self-signed certificate enabled by default.
- **Default Web Content:** `/home/$user/public_html` is the web root. `/ping` endpoint and `phpinfo.php` are available for diagnostics.
- **Helper Scripts:** `instance_start`, `instance_stop`, `instance_logs`, `instance_db_info` (created by `local-dev.sh`).
__Optional Tags__
*environment* - Set to DEV to start memcached and mysql locally for development purposes
*serveralias* - Set to allow alternative hostnames for a site.
*PHPVER* - Set to use a different version of PHP [refer to versions here.](#phpver)
---
### Helpful Notes ###
## Environment Variables
* On your first creation of a dev instance, you will be dumped to the logs output. Hit ```ctrl + c``` to exit the running process.
* If you want to restart the instance again, run ```docker start {name-of-your-container}``` in the example, *name-of-your-cintainer* is *local-dev*
* To stop a restarted instance, run ```docker stop {name-of-your-container}```
* To view log stream from container, run ```docker logs -f {name-of-your-container}```
* To delete a container, run ```docker rm {name-of-your-container}``` *__Note:__ this does not delete the files in public_html or database, as those are store in your system*
* To view running containers, run ```docker ps```
* To view all created containers, run ```docker ps --all``
* To view all container images downloaded on your system, run ```docker images```
**Required:**
- `uid` User ID for file permissions
- `user` Username for file permissions
- `domain` Primary domain for configuration
**Optional:**
- `environment` Set to `DEV` to start memcached and MySQL locally for development
- `serveralias` Comma-separated list of alternative hostnames
- `PHPVER` PHP version (see above)
---
## Helpful Notes
- To restart the instance: `./instance_start` or `docker start {container-name}`
- To stop: `./instance_stop` or `docker stop {container-name}`
- To view logs: `./instance_logs` or `docker logs -f {container-name}`
- To get DB credentials: `./instance_db_info` or `cat /home/$user/mysql_creds`
- To delete a container: `docker rm {container-name}` (does not delete user files or DB volume)
- To view running containers: `docker ps`
- To view all containers: `docker ps --all`
- To view images: `docker images`
---
## Troubleshooting
- The first run may take several minutes as dependencies are installed.
- If you need to change PHP version, stop and remove the container, then recreate with the desired version.
- For advanced configuration, see the scripts in the `scripts/` directory.
- The image is optimized for size and speed, but local development in DEV mode may install additional packages (MariaDB, memcached) at runtime using microdnf.
- The build context is minimized by the included `.dockerignore` file.

View File

@@ -1,13 +1,2 @@
DirectoryIndex index.html index.htm index.php
Alias "/ping" "/var/www/html"
<IfModule mpm_event_module>
StartServers 10
MinSpareThreads 25
MaxSpareThreads 75
ThreadLimit 64
ThreadsPerChild 25
MaxRequestWorkers 800
ServerLimit 32
MaxConnectionsPerChild 1500
</IfModule>

View File

@@ -0,0 +1,111 @@
## OpenLiteSpeed APPEND fragment — added to the stock httpd_config.conf
## that ships with litespeedtech/openlitespeed. Keeping the stock config
## intact preserves all the cgid/lscgid plumbing (CGIRLimit defaults,
## fileAccessControl defaults, etc.) — when we tried writing a fully
## custom httpd_config.conf, lscgid never created its IPC socket and
## every PHP request 503'd. The upstream OLS docker template uses this
## append pattern too (see setup_docker.sh in litespeedtech/ols-dockerfiles).
##
## Rendered at container start by scripts/create-vhost-litespeed.sh via
## 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) ---
listener HTTP {
address *:80
secure 0
map siteVH *
## NB: HTTPHTTPS redirect is in site-template.tpl's rewrite{} block,
## NOT here — OLS 1.8 listener-level rewrites are inert for vhTemplate
## members. Don't move it back to this listener.
}
listener HTTPS {
address *:443
secure 1
keyFile /usr/local/lsws/conf/cert/self.key
certFile /usr/local/lsws/conf/cert/self.crt
sslProtocol 24
enableSpdy 15
enableQuic 0
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) ---
## The template file is /usr/local/lsws/conf/templates/site.conf — written
## by create-vhost-litespeed.sh at the same time as this fragment.
vhTemplate site {
templateFile conf/templates/site.conf
listeners HTTP, HTTPS
note cac-litespeed per-customer vhost
## vhDomain: customer's domain + serveralias list + `*` catchall so
## ip-only requests (e.g. HAProxy backend health check by container_name)
## still resolve. WHP/HAProxy filters hostnames upstream no risk to
## allowing the catchall here.
member siteVH {
vhDomain ${domain}${vhost_map_aliases}, *
}
}

View File

@@ -0,0 +1,60 @@
; Production lsphp overrides — mirrors configs/prod-php.ini for the FPM
; image, adapted for LSAPI defaults. Dropped into /usr/local/lsws/lsphpNN/etc/php.d/
memory_limit = 256M
post_max_size = 384M
upload_max_filesize = 256M
max_input_vars = 2000
max_execution_time = 60
max_input_time = 120
expose_php = Off
short_open_tag = Off
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
zend.exception_ignore_args = On
session.save_handler = files
session.use_cookies = 1
session.use_only_cookies = 1
session.use_strict_mode = 0
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
opcache.enable = 1
; Sized to fit Divi + WooCommerce + WP core comfortably without eviction
; thrash. Per-instance because shmem is per-process-RSS on Linux cgroups
; (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.enable_cli = Off
output_buffering = 4096
default_charset = "UTF-8"
file_uploads = On
max_file_uploads = 20
soap.wsdl_cache_enabled = 1
soap.wsdl_cache_dir = "/tmp"
soap.wsdl_cache_ttl = 86400
soap.wsdl_cache_limit = 5

View File

@@ -0,0 +1,97 @@
## OLS vhTemplate for the per-customer vhost. Mirrors the structure of the
## upstream docker.conf template but with our paths and LSCache wiring.
## Templated vars (envsubst): $user
##
## $VH_NAME, $VH_ROOT, $DOC_ROOT, $SERVER_ROOT are OLS macros — they MUST
## stay literal in the output (not in the envsubst allow-list).
allowSymbolLink 1
enableScript 1
restrained 1
## 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
virtualHostConfig {
docRoot $VH_ROOT
## 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
logLevel WARN
rollingSize 10M
keepDays 14
compressArchive 1
}
accesslog /home/${user}/logs/apache/access_log {
useServer 0
rollingSize 10M
keepDays 7
compressArchive 1
}
index {
useServer 0
indexFiles index.php, index.html
autoIndex 0
}
## LSCache plugin owns Cache-Control / Expires entirely server-level
## expires off so we don't double-emit headers.
expires {
enableExpires 0
}
accessControl {
allow *
}
context / {
location $DOC_ROOT/
allowBrowse 1
rewrite {
enable 1
inherit 0
autoLoadHtaccess 1
RewriteFile .htaccess
}
addDefaultCharset off
}
rewrite {
enable 1
autoLoadHtaccess 1
logLevel 0
## Force HTTPS — OLS 1.8 listener-level rewrites don't apply per-vhost,
## so the redirect lives here. The RewriteCond guards against an infinite
## loop (SERVER_PORT=80 means "this request came in on the HTTP listener,
## not HTTPS"). Per-customer .htaccess rules still apply (autoLoadHtaccess).
RewriteCond %{SERVER_PORT} 80
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [L,R=301]
}
## Per-vhost LSCache storage. The server-level `module cache` block in
## stock httpd_config.conf is already enabled (ls_enabled 1); the LSCWP
## plugin flips cache on/off per request via X-LiteSpeed-Cache-Control.
module cache {
storagePath /home/${user}/lscache
checkPrivateCache 1
checkPublicCache 1
enableCache 0
enablePrivateCache 0
}
}

View File

@@ -1,10 +1,10 @@
# MariaDB 10.11 CentOS repository list - created 2023-04-03 23:52 UTC
# MariaDB 11.4.5 CentOS repository list - created 2023-04-03 23:52 UTC
# https://mariadb.org/download/
[mariadb]
name = MariaDB
# rpm.mariadb.org is a dynamic mirror if your preferred mirror goes offline. See https://mariadb.org/mirrorbits/ for details.
# baseurl = https://rpm.mariadb.org/10.11/centos/$releasever/$basearch
baseurl = https://mirrors.xtom.com/mariadb/yum/10.11/centos/$releasever/$basearch
baseurl = https://mirror.mariadb.org/yum/11.4/rhel$releasever-amd64
module_hotfixes = 1
# gpgkey = https://rpm.mariadb.org/RPM-GPG-KEY-MariaDB
gpgkey = https://mirrors.xtom.com/mariadb/yum/RPM-GPG-KEY-MariaDB

View File

@@ -1091,7 +1091,7 @@ session.save_handler = memcache
; RPM note : session directory must be owned by process owner
; for mod_php, see /etc/httpd/conf.d/php.conf
; for php-fpm, see /etc/php-fpm.d/*conf
session.save_path = "tcp://localhost:11211"
session.save_path = "tcp://memcache:11211"
; Whether to use strict session mode.
; Strict session mode does not accept an uninitialized session ID, and
@@ -1496,7 +1496,15 @@ ldap.max_links = -1
;dba.default_handler=
[opcache]
; see /etc/php.d/10-opcache.ini
; Optimized for shared hosting — reduce idle memory footprint
; Default 128MB is excessive for most WordPress sites
opcache.memory_consumption = 64
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 4000
; Revalidate files every 60s in production (reduces stat() calls)
opcache.revalidate_freq = 60
; Don't waste memory on CLI scripts
opcache.enable_cli = Off
[curl]
; A default value for the CURLOPT_CAINFO option. This is required to be an

2
configs/remote_ip.conf Normal file
View File

@@ -0,0 +1,2 @@
RemoteIPHeader X-Forwarded-For
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

View File

@@ -0,0 +1,38 @@
## ---- 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 2 = trust the proxy header. HAProxy is
## the only thing that ever connects to this tier (it's not publicly exposed),
## so trusting the header from the docker-network peer is safe — same trust
## model as the shared httpd's RemoteIPInternalProxy.
useIpInProxyHeader 2
## LSCache enabled at MODULE scope for the whole tier (dedicated cache volume,
## ephemeral across rebuilds; OLS auto-keys a per-vhost subdir under storagePath).
## enableCache/enablePrivateCache ON here means the cache module is ACTIVE, but a
## response is only cached if it's marked cacheable — the LiteSpeed Cache WP
## plugin sets X-LiteSpeed-Cache-Control headers, and checkPublic/PrivateCache +
## ignoreRespCacheCtrl=0 make OLS honor them. No plugin → nothing cached (safe).
module cache {
storagePath ${LSCACHE_ROOT}
checkPrivateCache 1
checkPublicCache 1
maxCacheObjSize 10000000
maxStaleAge 200
qsCache 1
reqCookieCache 1
respCookieCache 1
ignoreReqCacheCtrl 0
ignoreRespCacheCtrl 0
enableCache 1
enablePrivateCache 1
}
## ---- end shared-ols server append ----

View File

@@ -0,0 +1,37 @@
<Directory "/mnt/users/~~user~~/~~domain~~">
AllowOverride None
Require all granted
</Directory>
<Directory "/mnt/users/~~user~~/~~domain~~/public_html">
Options All MultiViews
AllowOverride All
Require all granted
</Directory>
<VirtualHost *:80>
ServerName "~~domain~~"
~~alias_block~~
DocumentRoot "/mnt/users/~~user~~/~~domain~~/public_html"
RewriteEngine on
RewriteCond %{SERVER_NAME} =~~domain~~
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName "~~domain~~"
~~alias_block~~
DocumentRoot "/mnt/users/~~user~~/~~domain~~/public_html"
SSLCertificateFile /etc/pki/tls/certs/localhost.crt
SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
~~proxy_block~~
DirectoryIndex index.php index.html index.htm
ErrorLog "/var/log/httpd/~~domain~~-error.log"
CustomLog "/var/log/httpd/~~domain~~-access.log" combined
</VirtualHost>
</IfModule>

View File

@@ -40,6 +40,42 @@
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
#
# When we also provide SSL we have to listen to the
# standard HTTPS port in addition.
#
Listen 443 https
##
## SSL Global Context
##
## All SSL configuration in this context applies both to
## the main server and all SSL-enabled virtual hosts.
##
# Pass Phrase Dialog:
# Configure the pass phrase gathering process.
# The filtering dialog program (`builtin' is a internal
# terminal dialog) has to provide the pass phrase on stdout.
SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog
# Inter-Process Session Cache:
# Configure the SSL Session Cache: First the mechanism
# to use and second the expiring timeout (in seconds).
SSLSessionCache shmcb:/run/httpd/sslcache(512000)
SSLSessionCacheTimeout 300
#
# Use "SSLCryptoDevice" to enable any supported hardware
# accelerators. Use "openssl engine -v" to list supported
# engine names. NOTE: If you enable an accelerator and the
# server does not start, consult the error logs and ensure
# your accelerator is functioning properly.
#
SSLCryptoDevice builtin
<IfModule mod_ssl.c>
<VirtualHost _default_:443>
ServerName "~~domain~~"

View File

@@ -5,18 +5,20 @@ https_port='443'
root_path="$(pwd)"
verbose='false'
while getopts 'n:p:s:r:vh' flag; do
while getopts 'n:p:s:r:a:vh' flag; do
case "${flag}" in
n) name="${OPTARG}" ;;
p) http_port="${OPTARG}" ;;
s) https_port="${OPTARG}" ;;
r) root_path="${OPTARG}" ;;
a) phpver="${OPTARG}" ;;
v) verbose='true' ;;
h) echo "Variables"
echo "-n = Name of Container, Required"
echo "-p = Non-https Port Override, default 80"
echo "-s = Https Port Override, default 443"
echo "-r = Root Path for files and database, defaults to current working path"
echo "-a = PHP App Version, Default to 8.3"
echo "-v = Enable Verbose Mode"
exit 1 ;;
esac
@@ -34,16 +36,20 @@ if [ -z "$name" ]; then
echo "Name not set, please set it with -n"
exit 1
fi
if [ -z "$phpver" ]; then
phpver=83;
fi
echo "Building Docker Image..."
user=$(whoami)
uid=$(id -u)
if [ ! -d "$root_path/db" ]; then
mkdir -p "$root_path/db";
if [ ! -d "$root_path/user" ]; then
mkdir -p "$root_path/user";
mkdir -p "$root_path/user/logs/{apache,system}";
fi
if [ ! -d "$root_path/web" ]; then
mkdir -p "$root_path/web";
fi
$check_docker run -d -p "$http_port":80 -p "$https_port":443 -e PHPVER=82 -e environment=DEV --mount type=bind,source="$root_path"/web,target=/home/"$user"/public_html --mount type=bind,source="$root_path"/db,target=/var/lib/mysql -e uid="$uid" -e user="$user" -e domain="$name-local.dev" --name "$name" public.ecr.aws/s1f6k4w4/cac
$check_docker volume create "$name-mysql"
$check_docker run --pull=always -d -p "$http_port":80 -p "$https_port":443 -e PHPVER=$phpver -e environment=DEV --mount type=bind,source="$root_path"/user,target=/home/"$user" --mount type=bind,source="$(pwd)"/user/logs/apache,target=/etc/httpd/logs --mount type=bind,source="$(pwd)"/user/logs/system,target=/var/log -v"$name-mysql":/var/lib/mysql -e uid="$uid" -e user="$user" -e domain="$name-local.dev" --name "$name" repo.anhonesthost.net/cloud-hosting-platform/cac:latest
echo "Creating management scripts in root directory..."
echo "#!/usr/bin/env bash" > "$root_path/instance_start"
echo "docker start $name" >> "$root_path/instance_start"
@@ -54,10 +60,13 @@ echo "docker exec $name bash -c 'tail -f /etc/httpd/logs/*'" >> "$root_path/inst
echo "#!/usr/bin/env bash" > "$root_path/instance_db_info"
echo "docker exec $name cat /var/lib/mysql/creds" >> "$root_path/instance_db_info"
chmod +x $root_path/instance_*
echo "Waiting 120 seconds for setup to finish"
sleep 120;
echo "Waiting 160 seconds for setup to finish"
sleep 160;
echo "Installing WordPress..."
docker exec $name bash -c "cd /home/$(whoami)/public_html; wp core download; chown -R $(whoami) /home/$(whoami)/public_html"
wpdbuser=$(docker exec $name cat /var/lib/mysql/creds |grep User| awk -F ": " {'print $2'})
wpdbpass=$(docker exec $name cat /var/lib/mysql/creds |grep Password| awk -F ": " {'print $2'})
wpdb=$(docker exec $name cat /var/lib/mysql/creds |grep Database| awk -F ": " {'print $2'})
docker exec $name bash -c "cd /home/$(whoami)/public_html; wp core download; wp config create --dbname=$wpdb --dbuser=$wpdbuser --dbpass=$wpdbpass ; chown -R $(whoami):$(whoami) /home/$(whoami)/public_html;"
echo "Local Development Instance Created, to stop run ./instance_stop from within the base directory"
echo "MySQL DB Credentials"
docker exec $name cat /var/lib/mysql/creds

View File

@@ -1,70 +0,0 @@
---
resources:
- name: cac
type: git
source:
uri: https://repo.anhonesthost.net/cloud-hosting-platform/cloud-apache-container.git
branch: trunk
- name: build-cac-74
type: docker-image
source:
repository: registry.dnspegasus.net/cac
tag: 74
- name: build-cac-80
type: docker-image
source:
repository: registry.dnspegasus.net/cac
tag: 80
- name: build-cac-81
type: docker-image
source:
repository: registry.dnspegasus.net/cac
tag: 81
- name: build-cac-82
type: docker-image
source:
repository: registry.dnspegasus.net/cac
tag: 82
jobs:
- name: publish-cac-74
plan:
- get: cac
trigger: true
- put: build-cac-74
params:
build: cac
build_args:
PHPVER: 74
- name: publish-cac-80
plan:
- get: cac
trigger: true
- put: build-cac-80
params:
build: cac
build_args:
PHPVER: 80
- name: publish-cac-81
plan:
- get: cac
trigger: true
- put: build-cac-81
params:
build: cac
build_args:
PHPVER: 81
- name: publish-cac-82
plan:
- get: cac
trigger: true
- put: build-cac-82
params:
build: cac
build_args:
PHPVER: 82

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);

View File

@@ -0,0 +1,17 @@
#!/bin/bash
# Generate Apache MPM event tuning config at runtime using detect-memory.sh values.
cat <<EOF > /etc/httpd/conf.d/mpm-tuning.conf
<IfModule mpm_event_module>
StartServers ${APACHE_START_SERVERS}
MinSpareThreads ${APACHE_MIN_SPARE_THREADS}
MaxSpareThreads ${APACHE_MAX_SPARE_THREADS}
ThreadLimit 64
ThreadsPerChild 25
MaxRequestWorkers ${APACHE_MAX_REQUEST_WORKERS}
ServerLimit ${APACHE_SERVER_LIMIT}
MaxConnectionsPerChild ${APACHE_MAX_CONNECTIONS_PER_CHILD}
</IfModule>
EOF
exit 0

View File

@@ -2,26 +2,56 @@
rm /etc/php-fpm.d/www.conf
FPM_LISTEN=${FPM_LISTEN:-/run/php-fpm/www.sock}
# Determine listen directive and ownership based on socket vs TCP
if echo "$FPM_LISTEN" | grep -q '/'; then
# Unix socket mode (standalone — Apache and FPM in same container)
listen_directive="$FPM_LISTEN"
listen_owner_block="listen.owner = apache
listen.group = apache"
env_block=""
else
# TCP port mode (shared httpd — FPM in separate container)
listen_directive="0.0.0.0:${FPM_LISTEN}"
listen_owner_block=""
# Override DOCUMENT_ROOT so PHP plugins (e.g., WordFence) that use
# $_SERVER['DOCUMENT_ROOT'] find files at the FPM container's path,
# not the shared httpd's /mnt/users/ mount path.
env_block="env[DOCUMENT_ROOT] = /home/$user/public_html"
fi
cat <<EOF > /etc/php-fpm.d/$user.conf
[$user]
user = $user
group = $user
listen = /run/php-fpm/www.sock
listen.owner = apache
listen.group = apache
listen = ${listen_directive}
${listen_owner_block}
pm = static
pm.max_children = 10
pm.max_requests = 150
pm = ${PHP_FPM_PM}
pm.max_children = ${PHP_FPM_MAX_CHILDREN}
pm.max_requests = ${PHP_FPM_MAX_REQUESTS}
pm.process_idle_timeout = ${PHP_FPM_PROCESS_IDLE_TIMEOUT}
slowlog = /etc/httpd/logs/error_log
; Settings used when pm = dynamic (fallback if user overrides FPM_PM)
pm.start_servers = ${PHP_FPM_START_SERVERS}
pm.min_spare_servers = ${PHP_FPM_MIN_SPARE}
pm.max_spare_servers = ${PHP_FPM_MAX_SPARE}
; Health check endpoints
ping.path = /fpm-ping
ping.response = pong
pm.status_path = /fpm-status
slowlog = /home/$user/logs/php-fpm/slowlog
request_slowlog_timeout = 3s
php_admin_value[error_log] = /etc/httpd/logs/error_log
php_admin_value[error_log] = /home/$user/logs/php-fpm/error.log
php_admin_flag[log_errors] = on
php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache
${env_block}
EOF

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env bash
## create-vhost-litespeed.sh — sets up OLS config for one customer site.
##
## Approach: keep the stock LiteSpeed-shipped httpd_config.conf VERBATIM
## (it has all the cgid/lscgid plumbing that lscgid needs to actually
## create its IPC socket), and just APPEND our listeners + vhTemplate.
## The custom vhost template lives at conf/templates/site.conf and points
## at /home/${user}/public_html. envsubst renders our user/domain into
## both files at container start.
##
## Expects in env: user, domain, serveralias (optional).
set -euo pipefail
TPL_DIR=${TPL_DIR:-/etc/lsws-templates}
LSWS_CONF=/usr/local/lsws/conf
## Ensure the conf dir has stock config to append to. On first boot with
## a fresh image this is a no-op (image ships with conf/ populated). With
## a future volume mount of conf/, the upstream entrypoint pattern would
## copy from .conf/* — keep parity:
if [ -z "$(ls -A -- "$LSWS_CONF/" 2>/dev/null)" ]; then
cp -R /usr/local/lsws/.conf/* "$LSWS_CONF/"
fi
## Build the serveralias suffix for vhDomain. Empty for none, else
## ",alias1,alias2" prepended to the comma list.
vhost_map_aliases=""
if [ -n "${serveralias:-}" ]; then
for alias in $(echo "$serveralias" | tr ',' ' '); do
[ -z "$alias" ] && continue
vhost_map_aliases="${vhost_map_aliases},${alias}"
done
fi
export vhost_map_aliases user domain
## --- prep the stock httpd_config.conf before appending ours ---
## Stock ships with `listener HTTP {*:80}`, `listener HTTPS {*:443}`, and
## a `vhTemplate docker` mapped to /var/www/vhosts/$VH_NAME/html — these
## conflict with our ports and would shadow our siteVH vhost. Strip them
## and the demo `virtualHost Example`, but KEEP `listener Default` (it's
## bound to 8088 — harmless internally, removing risks unrelated breakage).
## Always restart from a stock copy so re-runs are idempotent (otherwise
## a second sed pass on already-stripped config corrupts it).
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
## 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 '
BEGIN { skip = 0 }
/^listener HTTP \{/ || /^listener HTTPS \{/ || /^vhTemplate docker \{/ || /^extProcessor lsphp\{/ || /^extProcessor lsphp \{/ { skip = 1; next }
skip && /^\}/ { skip = 0; next }
!skip { print }
' "$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) ----"
{
echo ""
echo "$SENTINEL"
envsubst '${user} ${domain} ${vhost_map_aliases} ${PHPVER} ${LSAPI_CHILDREN}' < "$TPL_DIR/httpd_config.tpl"
} >> "$LSWS_CONF/httpd_config.conf"
## --- write our vhost template to /usr/local/lsws/conf/templates/site.conf ---
envsubst '${user}' < "$TPL_DIR/site-template.tpl" \
> "$LSWS_CONF/templates/site.conf"
## --- per-vhost config file the vhTemplate will reference ---
## OLS creates conf/vhosts/$VH_NAME/ at template-instantiation time, but
## we pre-create it to satisfy the configFile path and write a minimal
## vhconf.conf (empty body — all real config is inline in the template's
## virtualHostConfig{} block).
mkdir -p "$LSWS_CONF/vhosts/siteVH"
echo "## auto-generated; real vhost config is in templates/site.conf" \
> "$LSWS_CONF/vhosts/siteVH/vhconf.conf"
## Permissions: OLS reads conf/ as lsadm. Don't break that.
chown -R lsadm:nogroup "$LSWS_CONF" 2>/dev/null || true

View File

@@ -34,6 +34,12 @@ cat <<EOF > /etc/httpd/conf.d/$domain.conf
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
Listen 443 https
SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog
SSLSessionCache shmcb:/run/httpd/sslcache(512000)
SSLSessionCacheTimeout 300
SSLCryptoDevice builtin
<IfModule mod_ssl.c>
<VirtualHost _default_:443>
ServerName "$domain"

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env bash
## detect-memory-litespeed.sh — sibling to detect-memory.sh.
## Computes LSAPI_CHILDREN + extprocessor memSoftLimit/memHardLimit from
## container memory cap. Sourced by entrypoint-litespeed.sh.
## ---- container memory detection (mirrors detect-memory.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 (LSAPI workers get the lion's share) ----
OS_RESERVE_MB=50
OLS_RESERVE_MB=40 # OpenLiteSpeed daemon footprint
DEV_OVERHEAD_MB=0
if [ "${environment:-PROD}" = "DEV" ]; then
DEV_OVERHEAD_MB=125
fi
AVAILABLE_MB=$((CONTAINER_MEMORY_MB - OS_RESERVE_MB - OLS_RESERVE_MB - DEV_OVERHEAD_MB))
if [ "$AVAILABLE_MB" -lt 60 ]; then
AVAILABLE_MB=60
fi
## ---- LSAPI children (analogous to PHP_FPM_MAX_CHILDREN) ----
## Per the 2026-06-02 cac-litespeed memory-sizing finding (vantagehealth
## OOM-spawn loop at 512 MB cap): each lsphp worker carries ~115 MB
## 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))
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 knobs — site-pool-env.php still passes FPM_MAX_CHILDREN
## for backward compat, so prefer LSAPI_CHILDREN if set, else FPM_MAX_CHILDREN,
## else the calculated value.
LSAPI_CHILDREN=${LSAPI_CHILDREN:-${FPM_MAX_CHILDREN:-$calc_lsapi_children}}
## Per-worker mem limits (RLIMIT_AS) live in httpd_config.tpl now as
## hard-coded 1024M soft / 1500M hard — those values comfortably fit
## typical Divi/WooCommerce VSZ (~280-365 MB) while still catching a
## true runaway script. Cgroup remains the real backstop. The earlier
## AVAILABLE/CHILDREN formula was killing legitimate workers because
## it conflated VSZ (RLIMIT_AS) with RSS-budget arithmetic.
export CONTAINER_MEMORY_MB LSAPI_CHILDREN

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

142
scripts/detect-memory.sh Executable file
View File

@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# detect-memory.sh — Detect container memory and calculate tuning parameters.
# Must be sourced (not executed) so variables are available to the caller.
# --- Memory detection (cgroups v2 → v1 → /proc/meminfo → fallback) ---
CONTAINER_MEMORY_BYTES=""
# cgroups v2
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
# cgroups v1
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)
# Values near page-aligned max (like 9223372036854771712) mean "no limit"
if [ -n "$val" ] && [ "$val" -lt 8589934592000 ] 2>/dev/null; then
CONTAINER_MEMORY_BYTES=$val
fi
fi
# /proc/meminfo (host memory — used when no cgroup limit is set)
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
# Fallback
if [ -z "$CONTAINER_MEMORY_BYTES" ]; then
CONTAINER_MEMORY_BYTES=$((512 * 1024 * 1024))
fi
CONTAINER_MEMORY_MB=$((CONTAINER_MEMORY_BYTES / 1024 / 1024))
# --- Budget calculation ---
CONTAINER_ROLE=${CONTAINER_ROLE:-combined} # combined | fpm_only | httpd_only
OS_RESERVE_MB=50
FIXED_PROCESS_MB=50
DEV_OVERHEAD_MB=0
if [ "$environment" = "DEV" ]; then
DEV_OVERHEAD_MB=125
fi
AVAILABLE_MB=$((CONTAINER_MEMORY_MB - OS_RESERVE_MB - FIXED_PROCESS_MB - DEV_OVERHEAD_MB))
if [ "$AVAILABLE_MB" -lt 60 ]; then
AVAILABLE_MB=60
fi
case "$CONTAINER_ROLE" in
fpm_only)
PHP_BUDGET_MB=$AVAILABLE_MB
APACHE_BUDGET_MB=0
;;
httpd_only)
PHP_BUDGET_MB=0
APACHE_BUDGET_MB=$AVAILABLE_MB
;;
*)
PHP_BUDGET_MB=$((AVAILABLE_MB * 80 / 100))
APACHE_BUDGET_MB=$((AVAILABLE_MB * 20 / 100))
;;
esac
# --- PHP-FPM parameters (skipped for httpd_only) ---
if [ "$CONTAINER_ROLE" != "httpd_only" ]; then
# PHP_WORKER_ESTIMATE_MB sizes the divisor for pm.max_children. The
# previous default of 60 was optimistic for modern Woo/Elementor stacks:
# the alphaone 2026-06-01 incident measured ~193 MB resident per worker
# against the 60 MB assumption, and 15 calculated children put peak
# demand (15 * 193 = 2.9 GB) over the 1-2 GiB container cap. 128 lands
# closer to plugin-heavy WP reality while remaining conservative for
# leaner sites. Customers can still override via the FPM_MAX_CHILDREN
# env var on the container if a different shape is justified.
PHP_WORKER_ESTIMATE_MB=${PHP_WORKER_ESTIMATE_MB:-128}
calc_max_children=$((PHP_BUDGET_MB / PHP_WORKER_ESTIMATE_MB))
# Floor at 2, cap at 50
if [ "$calc_max_children" -lt 2 ]; then
calc_max_children=2
fi
if [ "$calc_max_children" -gt 50 ]; then
calc_max_children=50
fi
PHP_FPM_PM=${FPM_PM:-ondemand}
PHP_FPM_MAX_CHILDREN=${FPM_MAX_CHILDREN:-$calc_max_children}
PHP_FPM_PROCESS_IDLE_TIMEOUT=${FPM_PROCESS_IDLE_TIMEOUT:-5s}
PHP_FPM_MAX_REQUESTS=${FPM_MAX_REQUESTS:-200}
# Dynamic mode fallbacks (used if user overrides FPM_PM=dynamic)
PHP_FPM_START_SERVERS=${FPM_START_SERVERS:-2}
PHP_FPM_MIN_SPARE=${FPM_MIN_SPARE_SERVERS:-1}
PHP_FPM_MAX_SPARE=${FPM_MAX_SPARE_SERVERS:-3}
fi
# --- Apache MPM parameters (skipped for fpm_only) ---
if [ "$CONTAINER_ROLE" != "fpm_only" ]; then
# ServerLimit: roughly 1 process per ~25 workers, floor 2, cap 16
calc_server_limit=$((APACHE_BUDGET_MB / 30))
if [ "$calc_server_limit" -lt 2 ]; then
calc_server_limit=2
fi
if [ "$calc_server_limit" -gt 16 ]; then
calc_server_limit=16
fi
# MaxRequestWorkers: ServerLimit * ThreadsPerChild (25)
calc_max_request_workers=$((calc_server_limit * 25))
if [ "$calc_max_request_workers" -gt 400 ]; then
calc_max_request_workers=400
fi
# StartServers: 1 for ≤1GB, 2 for larger
calc_start_servers=1
if [ "$CONTAINER_MEMORY_MB" -gt 1024 ]; then
calc_start_servers=2
fi
APACHE_START_SERVERS=${APACHE_START_SERVERS:-$calc_start_servers}
APACHE_SERVER_LIMIT=${APACHE_SERVER_LIMIT:-$calc_server_limit}
APACHE_MAX_REQUEST_WORKERS=${APACHE_MAX_REQUEST_WORKERS:-$calc_max_request_workers}
APACHE_MIN_SPARE_THREADS=${APACHE_MIN_SPARE_THREADS:-5}
APACHE_MAX_SPARE_THREADS=${APACHE_MAX_SPARE_THREADS:-15}
APACHE_MAX_CONNECTIONS_PER_CHILD=${APACHE_MAX_CONNECTIONS_PER_CHILD:-3000}
fi
# --- Export all variables ---
export CONTAINER_ROLE CONTAINER_MEMORY_MB
if [ "$CONTAINER_ROLE" != "httpd_only" ]; then
export PHP_FPM_PM PHP_FPM_MAX_CHILDREN PHP_FPM_PROCESS_IDLE_TIMEOUT PHP_FPM_MAX_REQUESTS
export PHP_FPM_START_SERVERS PHP_FPM_MIN_SPARE PHP_FPM_MAX_SPARE
fi
if [ "$CONTAINER_ROLE" != "fpm_only" ]; then
export APACHE_START_SERVERS APACHE_SERVER_LIMIT APACHE_MAX_REQUEST_WORKERS
export APACHE_MIN_SPARE_THREADS APACHE_MAX_SPARE_THREADS APACHE_MAX_CONNECTIONS_PER_CHILD
fi

86
scripts/entrypoint-fpm.sh Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
if [ -z "$PHPVER" ]; then
PHPVER="83";
fi
if [ -z "$environment" ]; then
environment="PROD"
fi
# Default to FPM-only role
export CONTAINER_ROLE="fpm_only"
export FPM_LISTEN=${FPM_LISTEN:-9000}
adduser -u $uid $user
mkdir -p /home/$user/public_html
mkdir -p /home/$user/logs/php-fpm
ln -sf /home/$user/logs/php-fpm /var/log/php-fpm
source /scripts/detect-memory.sh
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP-FPM pm=${PHP_FPM_PM} max_children=${PHP_FPM_MAX_CHILDREN} | Listen=${FPM_LISTEN}"
/scripts/create-php-config.sh
mkdir -p /run/php-fpm/
/usr/sbin/php-fpm -y /etc/php-fpm.conf
chown -R $user:$user /home/$user
chmod -R 755 /home/$user
if [[ $environment == 'DEV' ]]; then
echo "Starting Dev Deployment (FPM-only mode)"
mkdir -p /home/$user/_db_backups
if ! command -v microdnf &> /dev/null; then
echo "microdnf not found, installing with dnf..."
dnf install -y microdnf && dnf clean all
fi
microdnf install -y MariaDB-server MariaDB-client memcached
sed -r -i 's/session.save_path="memcache:11211/session.save_path="localhost:11211/' /etc/php.ini
nohup mysqld -umysql &
if [ ! -f /home/$user/mysql_creds ]; then
echo "Give MySQL a chance to finish starting..."
sleep 10
mysql_user=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13 ; echo '')
mysql_password=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 18 ; echo '')
mysql_db=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 6 ; echo '')
mysql -e "CREATE DATABASE devdb_"$mysql_db";"
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
mysql -e "FLUSH PRIVILEGES;"
echo "# User crontab for $user" > /home/$user/crontab
echo "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" >> /home/$user/crontab
chown $user:$user /home/$user/crontab
echo "MySQL User: "$mysql_user > /home/$user/mysql_creds
echo "MySQL Password: "$mysql_password >> /home/$user/mysql_creds
echo "MySQL Database: devdb_"$mysql_db >> /home/$user/mysql_creds
cat /home/$user/mysql_creds
fi
/usr/bin/memcached -d -u $user
fi
if [[ $environment == 'PROD' ]]; then
if [ -f /etc/php.d/50-memcached.ini ]; then
sed -r -i 's/;session.save_path="localhost:11211/session.save_path="memcache:11211/' /etc/php.d/50-memcached.ini
fi
fi
# Set up user crontab
if [ ! -f /home/$user/crontab ]; then
echo "# User crontab for $user" > /home/$user/crontab
echo "# Add your cron jobs here" >> /home/$user/crontab
echo "# Example: */5 * * * * /home/$user/scripts/my-script.sh" >> /home/$user/crontab
chown $user:$user /home/$user/crontab
fi
# Load user crontab
crontab -u $user /home/$user/crontab
/usr/sbin/crond
# Tail PHP-FPM logs (becomes PID 1 process)
touch /home/$user/logs/php-fpm/error.log
tail -f /home/$user/logs/php-fpm/*
exit 0

View File

@@ -0,0 +1,255 @@
#!/usr/bin/env bash
## entrypoint-litespeed.sh — PID 1 for cac-litespeed:phpNN.
## Built on litespeedtech/openlitespeed:1.8.x-lsphp83 prebuilt base. Native
## LSAPI (no FPM proxy), one customer per container.
##
## Process supervision: starts OLS via `openlitespeed -n` (no-daemon +
## crash-guard, per OLS source: lshttpdmain.cpp). SIGTERM is forwarded.
## crond runs in the background for customer crontabs; OLS itself is the
## process we wait on (if OLS dies, the container exits and Docker
## restarts it per its restart policy).
set -euo pipefail
: "${PHPVER:=83}"
: "${environment:=PROD}"
: "${LSCACHE_AUTOINSTALL:=1}"
export CONTAINER_ROLE="litespeed_only"
export PHPVER environment LSCACHE_AUTOINSTALL
## ---- env validation ----
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
## ---- user + directories ----
if ! id -u "$user" >/dev/null 2>&1; then
## Ubuntu's useradd; mirror what the AL10 entrypoints do with adduser
useradd -u "$uid" -m -s /bin/bash "$user"
fi
mkdir -p "/home/$user/public_html"
## 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 /tmp/lshttpd/swap
chmod 1777 /tmp/lshttpd
## ---- memory + lsphp pool sizing ----
# shellcheck source=/dev/null
source /scripts/detect-memory-litespeed.sh
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | LSAPI_CHILDREN=${LSAPI_CHILDREN} | PHPVER=${PHPVER}"
## ---- self-signed cert (idempotent) ----
mkdir -p /usr/local/lsws/conf/cert
if [ ! -f /usr/local/lsws/conf/cert/self.crt ]; then
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout /usr/local/lsws/conf/cert/self.key \
-out /usr/local/lsws/conf/cert/self.crt \
-subj "/CN=${domain}" 2>/dev/null
fi
## ---- render httpd_config + vhconf from templates ----
/scripts/create-vhost-litespeed.sh
## ---- point PHP error_log at the same customer-visible path that
## cac:phpNN uses for php-fpm errors. Drop-in compat: customer code that
## was tailing /home/$user/logs/php-fpm/error.log on the old image will
## see lsphp's PHP errors in the exact same file on the new image.
## 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"
chmod 755 "/home/$user"
## ---- drop healthz so docker HEALTHCHECK passes before customer files
## Always rewrite as customer; suexec lsphp will read it as that uid too.
sudo -u "$user" sh -c "echo ok > /home/$user/public_html/healthz"
## ---- DEV: local mariadb + memcached for parity with cac entrypoints ----
if [ "$environment" = "DEV" ]; then
echo "Starting Dev Deployment (litespeed)"
mkdir -p "/home/$user/_db_backups"
## mariadb-server + memcached are NOT baked into the image (saves ~500MB
## 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
nohup mysqld --user=mysql &>/dev/null &
if [ ! -f "/home/$user/mysql_creds" ]; then
sleep 10
mysql_user=$(openssl rand -hex 7)
mysql_password=$(openssl rand -hex 12)
mysql_db="devdb_$(openssl rand -hex 3)"
mysql -e "CREATE DATABASE $mysql_db;"
mysql -e "CREATE USER '$mysql_user'@'localhost' IDENTIFIED BY '$mysql_password';"
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '$mysql_user'@'localhost' WITH GRANT OPTION;"
mysql -e "FLUSH PRIVILEGES;"
{
echo "MySQL User: $mysql_user"
echo "MySQL Password: $mysql_password"
echo "MySQL Database: $mysql_db"
} > "/home/$user/mysql_creds"
cat "/home/$user/mysql_creds"
fi
/usr/bin/memcached -d -u "$user"
fi
## ---- user crontab ----
if [ ! -f "/home/$user/crontab" ]; then
{
echo "# User crontab for $user"
echo "# Add your cron jobs here"
} > "/home/$user/crontab"
chown "$user:$user" "/home/$user/crontab"
fi
crontab -u "$user" "/home/$user/crontab"
service cron start >/dev/null 2>&1 || /usr/sbin/cron
## ---- LSCache plugin (background, non-fatal) ----
( /scripts/install-lscache-wp.sh "$user" >>/var/log/lscache-install.log 2>&1 || true ) &
## Stream OLS + customer logs to PID-1 stdout so `docker logs` works. Started
## once, before the supervisor loop — it follows the files across OLS restarts.
touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/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 \
/usr/local/lsws/logs/access.log \
"/home/$user/logs/apache/error_log" \
"/home/$user/logs/apache/access_log" \
"/home/$user/logs/php-fpm/error.log" 2>/dev/null &
## ---- 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

135
scripts/entrypoint-lsphp.sh Normal file
View File

@@ -0,0 +1,135 @@
#!/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}"
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP_LSAPI_CHILDREN=${PHP_LSAPI_CHILDREN} | LSAPI_MAX_IDLE=${LSAPI_MAX_IDLE} | PHPVER=${PHPVER} | bind=${LSPHP_BIND}"
## ---- 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

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
export CONTAINER_ROLE="httpd_only"
if [ -z "$environment" ]; then
environment="PROD"
fi
# Generate self-signed SSL cert if not already present
if [ ! -f /etc/pki/tls/certs/localhost.crt ]; then
openssl req -newkey rsa:2048 -nodes \
-keyout /etc/pki/tls/private/localhost.key \
-x509 -days 3650 -subj "/CN=localhost" \
-out /etc/pki/tls/certs/localhost.crt
fi
# Create log directory
mkdir -p /var/log/httpd
# Remove default configs that conflict
rm -f /etc/httpd/conf.d/userdir.conf
# Configure RemoteIP for Docker network
docker_network=$(ip addr show | grep eth0 | grep inet | awk -F " " '{print $2}')
if [ -n "$docker_network" ]; then
echo "RemoteIPInternalProxy $docker_network" >> /etc/httpd/conf.d/remoteip.conf
fi
# Detect memory and calculate Apache MPM tuning
source /scripts/detect-memory.sh
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | Apache workers=${APACHE_MAX_REQUEST_WORKERS} | Role=${CONTAINER_ROLE}"
# Generate MPM tuning config
/scripts/create-apache-mpm-config.sh
# Write SSL global config (matches standalone CAC behavior)
cat <<'EOF' > /etc/httpd/conf.d/ssl-global.conf
Listen 443 https
SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog
SSLSessionCache shmcb:/run/httpd/sslcache(512000)
SSLSessionCacheTimeout 300
SSLCryptoDevice builtin
EOF
# Disable the default ssl.conf if present (we use per-vhost SSL)
if [ -f /etc/httpd/conf.d/ssl.conf ]; then
mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.bak
fi
# Ensure vhosts directory exists and is included
mkdir -p /etc/httpd/conf.d/vhosts
if ! grep -q 'IncludeOptional conf.d/vhosts/' /etc/httpd/conf/httpd.conf; then
echo 'IncludeOptional conf.d/vhosts/*.conf' >> /etc/httpd/conf/httpd.conf
fi
# Start Apache
/usr/sbin/httpd -k start
# Start cron for log rotation
/usr/sbin/crond
# Tail Apache logs (becomes PID 1 process)
# Use a loop to pick up new log files as vhosts are added.
# tail -f only watches files that exist at start time.
touch /var/log/httpd/error_log
TAIL_PID=""
while true; do
LOG_FILES=$(find /var/log/httpd/ -name '*.log' -o -name '*_log' 2>/dev/null | sort)
if [ -n "$TAIL_PID" ]; then
kill "$TAIL_PID" 2>/dev/null
wait "$TAIL_PID" 2>/dev/null
fi
tail -f $LOG_FILES &
TAIL_PID=$!
# Re-check for new log files every 60 seconds
sleep 60
done

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

View File

@@ -1,29 +1,56 @@
#!/bin/bash
#!/usr/bin/env bash
if [ -z "$PHPVER" ]; then
PHPVER="81";
PHPVER="83";
fi
if [ -z "$environment" ]; then
environment="PROD"
fi
adduser -u $uid $user
mkdir -p /home/$user/public_html
mkdir -p /home/$user/logs/{apache,php-fpm}
chown -R $user:$user /home/$user
chmod -R 755 /home/$user
mv /var/log/httpd /var/log/httpd.bak
/scripts/install-php$PHPVER.sh
ln -s /home/$user/logs/apache /var/log/httpd
ln -s /home/$user/logs/php-fpm /var/log/php-fpm
rm -f /etc/httpd/conf.d/userdir.conf
docker_network=$(ip addr show |grep eth0 |grep inet |awk -F " " {'print $2'})
echo "RemoteIPInternalProxy $docker_network" >> /etc/httpd/conf.d/remoteip.conf
# /scripts/install-php$PHPVER.sh
source /scripts/detect-memory.sh
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP-FPM pm=${PHP_FPM_PM} max_children=${PHP_FPM_MAX_CHILDREN} | Apache workers=${APACHE_MAX_REQUEST_WORKERS}"
/scripts/create-vhost.sh
/scripts/create-php-config.sh
/scripts/create-apache-mpm-config.sh
if [ -f /etc/httpd/conf.d/ssl.conf ]; then
mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.bak
fi
/usr/sbin/httpd -k start
/usr/sbin/php-fpm -y /etc/php-fpm.conf
chown -R $user:$user /home/$user
chmod -R 755 /home/$user
if [[ $environment == 'DEV' ]]; then
echo "Starting Dev Deployment"
dnf install -y MariaDB-server MariaDB-client memcached
mkdir -p /home/$user/_db_backups
# Ensure microdnf is available for installing MariaDB and memcached in DEV mode
if ! command -v microdnf &> /dev/null; then
echo "microdnf not found, installing with dnf..."
dnf install -y microdnf && dnf clean all
fi
microdnf install -y MariaDB-server MariaDB-client memcached
sed -r -i 's/session.save_path="memcache:11211/session.save_path="localhost:11211/' /etc/php.ini
nohup mysqld -umysql &
if [ ! -f /var/lib/mysql/creds ]; then
if [ ! -f /home/$user/mysql_creds ]; then
echo "Give MySQL a chance to finish starting..."
sleep 10
mysql_user=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13 ; echo '')
@@ -33,14 +60,37 @@ if [[ $environment == 'DEV' ]]; then
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
mysql -e "FLUSH PRIVILEGES;"
echo "MySQL User: "$mysql_user > /var/lib/mysql/creds
echo "MySQL Password: "$mysql_password >> /var/lib/mysql/creds
echo "MySQL Database: devdb_"$mysql_db >> /var/lib/mysql/creds
cat /var/lib/mysql/creds
# Create user crontab with MySQL backup job
echo "# User crontab for $user" > /home/$user/crontab
echo "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" >> /home/$user/crontab
chown $user:$user /home/$user/crontab
echo "MySQL User: "$mysql_user > /home/$user/mysql_creds
echo "MySQL Password: "$mysql_password >> /home/$user/mysql_creds
echo "MySQL Database: devdb_"$mysql_db >> /home/$user/mysql_creds
cat /home/$user/mysql_creds
fi
/usr/bin/memcached -d -u $user
fi
tail -f /etc/httpd/logs/*
if [[ $environment == 'PROD' ]]; then
sed -r -i 's/;session.save_path="localhost:11211/session.save_path="memcache:11211/' /etc/php.d/50-memcached.ini
fi
# Set up user crontab
if [ ! -f /home/$user/crontab ]; then
echo "# User crontab for $user" > /home/$user/crontab
echo "# Add your cron jobs here" >> /home/$user/crontab
echo "# Example: */5 * * * * /home/$user/scripts/my-script.sh" >> /home/$user/crontab
chown $user:$user /home/$user/crontab
fi
# Load user crontab
crontab -u $user /home/$user/crontab
/usr/sbin/crond
tail -f /var/log/httpd/*
exit 0

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

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
## install-lscache-wp.sh — auto-install the official litespeed-cache plugin
## on first boot if WP is detected and plugin not already managed.
## Idempotent: re-runs are no-ops. Honors LSCACHE_AUTOINSTALL=0 escape hatch.
##
## Args: $1 = $user (customer system user)
set -euo pipefail
user="${1:?usage: install-lscache-wp.sh <user>}"
home="/home/${user}"
if [ "${LSCACHE_AUTOINSTALL:-1}" = "0" ]; then
echo "[lscache] LSCACHE_AUTOINSTALL=0 — skipping plugin install."
exit 0
fi
if [ ! -f "$home/public_html/wp-config.php" ]; then
echo "[lscache] No wp-config.php in $home/public_html — skipping (not a WP site)."
exit 0
fi
## With setUIDMode 2, lsphp runs as the customer, and customer owns their
## home tree — wp-cli also runs as the customer, files end up correctly owned.
if ! command -v wp >/dev/null 2>&1; then
echo "[lscache] wp-cli not on PATH — skipping (image build issue, not fatal)."
exit 0
fi
if sudo -u "$user" -- wp --path="$home/public_html" plugin is-installed litespeed-cache 2>/dev/null; then
echo "[lscache] litespeed-cache already installed — leaving customer's settings alone."
exit 0
fi
echo "[lscache] Installing litespeed-cache plugin for $user"
sudo -u "$user" -- wp --path="$home/public_html" plugin install litespeed-cache --activate \
|| { echo "[lscache] plugin install failed (network? wp-cli? perms?) — non-fatal."; exit 0; }
echo "[lscache] Done."

View File

@@ -1,6 +1,6 @@
#!/bin/bash
#!/usr/bin/env bash
dnf module enable php:remi-7.4 -y
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-xmlrpc \
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
php-mysqlnd php-mbstring php-ioncube-loader php-intl php-gd libzip php-cli
php-mysqlnd php-mbstring php-ioncube-loader php-intl php-gd php-pgsql libzip php-cli
exit 0

View File

@@ -1,6 +1,6 @@
#!/bin/bash
#!/usr/bin/env bash
dnf module enable php:remi-8.0 -y
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
php-mysqlnd php-mbstring php-ioncube-loader php-intl php-gd libzip php-cli
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
exit 0

View File

@@ -1,6 +1,6 @@
#!/bin/bash
#!/usr/bin/env bash
dnf module enable php:remi-8.1 -y
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
php-mysqlnd php-mbstring php-ioncube-loader php-intl php-gd libzip php-cli
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
exit 0

View File

@@ -1,6 +1,6 @@
#!/bin/bash
#!/usr/bin/env bash
dnf module enable php:remi-8.2 -y
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
php-mysqlnd php-mbstring php-intl php-gd libzip php-cli
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
exit 0

6
scripts/install-php83.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
dnf module enable php:remi-8.3 -y
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
exit 0

6
scripts/install-php84.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
dnf module enable php:remi-8.4 -y
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
exit 0

6
scripts/install-php85.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
dnf module enable php:remi-8.5 -y
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
exit 0

26
scripts/log-rotate.sh Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Set the log directory
LOG_DIR="/var/log/httpd"
# Get current date
DATE=$(date +%Y%m%d)
# Rotate access log
if [ -f "$LOG_DIR/access_log" ]; then
cp "$LOG_DIR/access_log" "$LOG_DIR/access_log.$DATE"
cat /dev/null > "$LOG_DIR/access_log"
fi
# Rotate error log
if [ -f "$LOG_DIR/error_log" ]; then
cp "$LOG_DIR/error_log" "$LOG_DIR/error_log.$DATE"
cat /dev/null > "$LOG_DIR/error_log"
fi
# Compress logs older than 3 days
find "$LOG_DIR" -name "*.log.*" -type f -mtime +3 -exec gzip {} \;
# Delete logs older than 7 days
find "$LOG_DIR" -name "*.log.*" -type f -mtime +7 -delete

14
scripts/mysql-backup.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
user=$1
mysql_db=$2
dt=$(date +%y%m%d-%T)
if [ ! -d /home/$user/_db_backups ]; then
mkdir -p /home/$user/_db_backups
fi
/usr/bin/mysqldump $mysql_db > /home/$user/_db_backups/$mysql_db.$dt.sql
chown -R $user:$user /home/$user/_db_backups
/usr/bin/find /home/$user/_db_backups/ -type f -mmin +360 -delete
exit 0

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

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)"

79
scripts/tune-mpm.sh Executable file
View File

@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Hot-adjust Apache MPM Event settings and graceful reload.
# Usage: tune-mpm.sh [--max-workers N] [--server-limit N] [--start-servers N]
# [--min-spare-threads N] [--max-spare-threads N]
# [--max-connections-per-child N]
set -euo pipefail
# Read current values from the config as defaults
CONFIG_FILE="/etc/httpd/conf.d/mpm-tuning.conf"
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: $CONFIG_FILE not found. Run detect-memory.sh first."
exit 1
fi
# Parse current values from config
current_start=$(grep -oP 'StartServers\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "1")
current_min_spare=$(grep -oP 'MinSpareThreads\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "5")
current_max_spare=$(grep -oP 'MaxSpareThreads\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "15")
current_max_workers=$(grep -oP 'MaxRequestWorkers\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "50")
current_server_limit=$(grep -oP 'ServerLimit\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "2")
current_max_conn=$(grep -oP 'MaxConnectionsPerChild\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "3000")
# Parse arguments
START_SERVERS=$current_start
MIN_SPARE_THREADS=$current_min_spare
MAX_SPARE_THREADS=$current_max_spare
MAX_REQUEST_WORKERS=$current_max_workers
SERVER_LIMIT=$current_server_limit
MAX_CONNECTIONS_PER_CHILD=$current_max_conn
while [[ $# -gt 0 ]]; do
case $1 in
--max-workers) MAX_REQUEST_WORKERS="$2"; shift 2 ;;
--server-limit) SERVER_LIMIT="$2"; shift 2 ;;
--start-servers) START_SERVERS="$2"; shift 2 ;;
--min-spare-threads) MIN_SPARE_THREADS="$2"; shift 2 ;;
--max-spare-threads) MAX_SPARE_THREADS="$2"; shift 2 ;;
--max-connections-per-child) MAX_CONNECTIONS_PER_CHILD="$2"; shift 2 ;;
--help|-h)
echo "Usage: $0 [--max-workers N] [--server-limit N] [--start-servers N]"
echo " [--min-spare-threads N] [--max-spare-threads N]"
echo " [--max-connections-per-child N]"
echo ""
echo "Current values:"
echo " StartServers: $current_start"
echo " MinSpareThreads: $current_min_spare"
echo " MaxSpareThreads: $current_max_spare"
echo " MaxRequestWorkers: $current_max_workers"
echo " ServerLimit: $current_server_limit"
echo " MaxConnectionsPerChild: $current_max_conn"
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
# Write updated config
cat <<EOF > "$CONFIG_FILE"
<IfModule mpm_event_module>
StartServers ${START_SERVERS}
MinSpareThreads ${MIN_SPARE_THREADS}
MaxSpareThreads ${MAX_SPARE_THREADS}
ThreadLimit 64
ThreadsPerChild 25
MaxRequestWorkers ${MAX_REQUEST_WORKERS}
ServerLimit ${SERVER_LIMIT}
MaxConnectionsPerChild ${MAX_CONNECTIONS_PER_CHILD}
</IfModule>
EOF
echo "MPM config updated:"
echo " StartServers=$START_SERVERS ServerLimit=$SERVER_LIMIT MaxRequestWorkers=$MAX_REQUEST_WORKERS"
echo " MinSpareThreads=$MIN_SPARE_THREADS MaxSpareThreads=$MAX_SPARE_THREADS MaxConnectionsPerChild=$MAX_CONNECTIONS_PER_CHILD"
# Graceful reload
/usr/sbin/httpd -k graceful
echo "Apache graceful reload triggered."