Merge pull request 'feat: OLS tier images — cac-lsphp (detached lsphp) + shared-ols' (#19) from feature/cac-lsphp-image into trunk
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 2m15s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m18s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m24s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m23s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m16s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m22s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m18s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 2m17s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m18s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m22s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m15s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m13s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 1m8s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 47s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 30s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 31s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 1m11s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 1m30s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 35s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 29s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 51s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 59s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 45s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 1m3s
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 2m15s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m18s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 2m24s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 2m23s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 2m16s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 2m22s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m18s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 2m17s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 2m18s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 2m22s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 2m15s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 2m13s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 1m8s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 47s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 30s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 31s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 1m11s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 1m30s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 35s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 29s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 51s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 59s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 45s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 1m3s
Reviewed-on: #19
This commit was merged in pull request #19.
This commit is contained in:
@@ -117,6 +117,47 @@ jobs:
|
|||||||
repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
|
repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
|
||||||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:latest' || '' }}
|
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:latest' || '' }}
|
||||||
|
|
||||||
|
Build-LSPHP-Images:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
|
||||||
|
# backend for the shared-ols tier and shares the litespeed prebuilt
|
||||||
|
# base, which only ships lsphp for 8.1+. Keep this matrix in lockstep
|
||||||
|
# with Build-LiteSpeed-Images.
|
||||||
|
phpver: [81, 82, 83, 84, 85]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: repo.anhonesthost.net
|
||||||
|
username: ${{ secrets.CI_USER }}
|
||||||
|
password: ${{ secrets.CI_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push lsphp Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
file: ./Dockerfile.lsphp
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
PHPVER=${{ matrix.phpver }}
|
||||||
|
OLS_VERSION=1.8.4
|
||||||
|
# OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same
|
||||||
|
# prebuilt base, same lsphp binaries. Bump both together.
|
||||||
|
tags: |
|
||||||
|
repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${{ matrix.phpver }}
|
||||||
|
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:latest' || '' }}
|
||||||
|
|
||||||
Build-Shared-httpd:
|
Build-Shared-httpd:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -144,3 +185,36 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
|
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
|
||||||
|
|
||||||
|
Build-Shared-OLS:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: repo.anhonesthost.net
|
||||||
|
username: ${{ secrets.CI_USER }}
|
||||||
|
password: ${{ secrets.CI_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Shared OLS Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
file: ./Dockerfile.shared-ols
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
# Single image (runs no PHP). PHPVER just selects the OLS base tag;
|
||||||
|
# pinned to 83 / OLS 1.8.4 to match the rest of the litespeed family.
|
||||||
|
build-args: |
|
||||||
|
PHPVER=83
|
||||||
|
OLS_VERSION=1.8.4
|
||||||
|
tags: |
|
||||||
|
repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest
|
||||||
|
|||||||
63
Dockerfile.lsphp
Normal file
63
Dockerfile.lsphp
Normal 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"]
|
||||||
57
Dockerfile.shared-ols
Normal file
57
Dockerfile.shared-ols
Normal 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"]
|
||||||
38
configs/shared-ols/httpd_config_base.tpl
Normal file
38
configs/shared-ols/httpd_config_base.tpl
Normal 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 ----
|
||||||
30
scripts/cac-lsphp-normalize.php
Normal file
30
scripts/cac-lsphp-normalize.php
Normal 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);
|
||||||
75
scripts/detect-memory-lsphp.sh
Normal file
75
scripts/detect-memory-lsphp.sh
Normal 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
|
||||||
135
scripts/entrypoint-lsphp.sh
Normal file
135
scripts/entrypoint-lsphp.sh
Normal 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
|
||||||
126
scripts/entrypoint-shared-ols.sh
Normal file
126
scripts/entrypoint-shared-ols.sh
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
## entrypoint-shared-ols.sh — PID 1 for the shared-ols tier.
|
||||||
|
##
|
||||||
|
## One OpenLiteSpeed container fronting MANY tenants' detached cac-lsphp
|
||||||
|
## sidecars (the OLS analogue of the shared-httpd container). Webserver ONLY —
|
||||||
|
## it runs NO PHP locally (render-shared-ols-config.sh strips the stock local
|
||||||
|
## lsphp; every site's PHP goes to its own sidecar over LSAPI). HAProxy stays
|
||||||
|
## the TLS/WAF/SNI edge and routes OLS-type hostnames here on :443.
|
||||||
|
##
|
||||||
|
## Reuses cac-litespeed's hard-won DAEMON-MODE supervision (NOT `openlitespeed
|
||||||
|
## -n` + wait): OLS self-restarts on QUIC.cloud IP refresh would otherwise exit
|
||||||
|
## PID 1 cleanly and tear the container down. See entrypoint-litespeed.sh and
|
||||||
|
## feedback_ols_quiccloud_restart_kills_container.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${environment:=PROD}"
|
||||||
|
export CONTAINER_ROLE="shared_ols"
|
||||||
|
|
||||||
|
LSWS_CONF=/usr/local/lsws/conf
|
||||||
|
CERT_DIR="$LSWS_CONF/cert"
|
||||||
|
HEALTH_DIR=/usr/local/lsws/shared-ols-health
|
||||||
|
export SITES_ROOT="${SITES_ROOT:-$LSWS_CONF/shared-sites}"
|
||||||
|
export LSCACHE_ROOT="${LSCACHE_ROOT:-/var/lscache}"
|
||||||
|
export CERT_FILE="$CERT_DIR/shared-ols.crt"
|
||||||
|
export KEY_FILE="$CERT_DIR/shared-ols.key"
|
||||||
|
|
||||||
|
mkdir -p "$SITES_ROOT" "$LSCACHE_ROOT" "$CERT_DIR" "$HEALTH_DIR/html"
|
||||||
|
|
||||||
|
## ---- self-signed cert for the :443 listener (HAProxy verifies none) ----
|
||||||
|
if [ ! -f "$CERT_FILE" ]; then
|
||||||
|
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
||||||
|
-keyout "$KEY_FILE" -out "$CERT_FILE" -subj "/CN=shared-ols" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
## ---- health vhost (catch-all): valid server with zero customer sites +
|
||||||
|
## answers HAProxy health checks that hit by IP / unknown Host with a 200 ----
|
||||||
|
cat > "$HEALTH_DIR/vhconf.conf" <<'EOF'
|
||||||
|
docRoot $VH_ROOT/html
|
||||||
|
enableScript 0
|
||||||
|
context / {
|
||||||
|
allowBrowse 1
|
||||||
|
location $DOC_ROOT/
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
printf 'ok\n' > "$HEALTH_DIR/html/healthz"
|
||||||
|
printf 'shared-ols\n' > "$HEALTH_DIR/html/index.html"
|
||||||
|
|
||||||
|
## ---- ownership: OLS reads conf/ as lsadm. chown the base conf dir + health dir
|
||||||
|
## NON-recursively (the per-site files under conf/shared-sites are written by the
|
||||||
|
## panel and are world-readable; a recursive chown here would be O(N-sites) on
|
||||||
|
## every container (re)start, delaying first-listen after a crash). The render
|
||||||
|
## script chowns the httpd_config.conf it produces. ----
|
||||||
|
chown lsadm:nogroup "$LSWS_CONF" "$HEALTH_DIR" "$HEALTH_DIR/html" 2>/dev/null || true
|
||||||
|
chown lsadm:nogroup "$HEALTH_DIR/vhconf.conf" "$HEALTH_DIR/html/healthz" "$HEALTH_DIR/html/index.html" 2>/dev/null || true
|
||||||
|
|
||||||
|
## ---- assemble httpd_config.conf from the panel's per-site files ----
|
||||||
|
/scripts/render-shared-ols-config.sh
|
||||||
|
|
||||||
|
## ---- stream OLS logs to PID-1 stdout (follows across restarts) ----
|
||||||
|
mkdir -p /usr/local/lsws/logs
|
||||||
|
touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log
|
||||||
|
tail -F /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log 2>/dev/null &
|
||||||
|
|
||||||
|
## ---- .htaccess watcher (required; spec 5.3). Background; the panel monitors
|
||||||
|
## that it stays alive (its death silently stops rewrite changes applying). ----
|
||||||
|
/scripts/ols-htaccess-watcher.sh &
|
||||||
|
WATCHER_PID=$!
|
||||||
|
|
||||||
|
## ---- supervise OLS in DAEMON mode (verbatim model from entrypoint-litespeed.sh) ----
|
||||||
|
STOP_REQUESTED=0
|
||||||
|
term_handler() {
|
||||||
|
STOP_REQUESTED=1
|
||||||
|
kill "$WATCHER_PID" 2>/dev/null || true
|
||||||
|
/usr/local/lsws/bin/lswsctrl stop >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap term_handler TERM INT
|
||||||
|
|
||||||
|
ols_running() { /usr/local/lsws/bin/lswsctrl status 2>/dev/null | grep -qi 'running with pid'; }
|
||||||
|
|
||||||
|
MAX_STARTS=5
|
||||||
|
WINDOW=60
|
||||||
|
starts=""
|
||||||
|
|
||||||
|
start_ols() {
|
||||||
|
/usr/local/lsws/bin/lswsctrl start >/dev/null 2>&1 || true
|
||||||
|
for _ in $(seq 1 20); do
|
||||||
|
ols_running && return 0
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! start_ols; then
|
||||||
|
echo "entrypoint-shared-ols: OLS failed to start (not running after 10s)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "entrypoint-shared-ols: OLS started in daemon mode — $(/usr/local/lsws/bin/lswsctrl status 2>/dev/null || true)"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if ols_running; then
|
||||||
|
sleep 3
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
if [ "$STOP_REQUESTED" -eq 0 ] && ols_running; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ "$STOP_REQUESTED" -eq 1 ]; then
|
||||||
|
echo "entrypoint-shared-ols: SIGTERM received, OLS stopped — exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
now=$(date +%s)
|
||||||
|
starts="$starts $now"
|
||||||
|
pruned=""
|
||||||
|
for t in $starts; do
|
||||||
|
[ $((now - t)) -lt "$WINDOW" ] && pruned="$pruned $t"
|
||||||
|
done
|
||||||
|
starts="$pruned"
|
||||||
|
n=$(echo $starts | wc -w)
|
||||||
|
echo "entrypoint-shared-ols: OLS not running — relaunching (attempt $n/$MAX_STARTS within ${WINDOW}s)." >&2
|
||||||
|
if [ "$n" -ge "$MAX_STARTS" ]; then
|
||||||
|
echo "entrypoint-shared-ols: OLS crash-looping — bailing for Docker restart policy / monitor." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
start_ols || true
|
||||||
|
done
|
||||||
17
scripts/healthcheck-lsphp.sh
Normal file
17
scripts/healthcheck-lsphp.sh
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
## healthcheck-lsphp.sh — liveness for the detached-lsphp sidecar.
|
||||||
|
##
|
||||||
|
## LSAPI is not FastCGI, so the cac-fpm `cgi-fcgi ... | grep pong` ping doesn't
|
||||||
|
## apply here. Minimum viable check (spec §5.1 fallback): the LSAPI listener is
|
||||||
|
## accepting TCP connections on :9000 AND at least one lsphp process is alive.
|
||||||
|
## A bound-but-wedged listener with no lsphp would fail the pgrep; a crashed
|
||||||
|
## listener fails the connect.
|
||||||
|
|
||||||
|
PORT="${LSPHP_HEALTH_PORT:-9000}"
|
||||||
|
|
||||||
|
# bash /dev/tcp connect test (bash is present on the litespeedtech base).
|
||||||
|
exec 3<>"/dev/tcp/127.0.0.1/${PORT}" 2>/dev/null || exit 1
|
||||||
|
exec 3>&- 3<&-
|
||||||
|
|
||||||
|
pgrep -x lsphp >/dev/null 2>&1 || exit 1
|
||||||
|
exit 0
|
||||||
73
scripts/ols-htaccess-watcher.sh
Normal file
73
scripts/ols-htaccess-watcher.sh
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
## ols-htaccess-watcher.sh — graceful-restart the shared OLS when any tenant's
|
||||||
|
## .htaccess changes. OLS reads .htaccess (RewriteFile) at (re)start, NOT per
|
||||||
|
## request, so without this a WordPress permalink/LiteSpeed-Cache change would
|
||||||
|
## silently not take effect. Required by spec 5.3.
|
||||||
|
##
|
||||||
|
## Watches all docroots for .htaccess writes, COALESCES bursts (a WP plugin save
|
||||||
|
## touches the file several times) within a debounce window, and RATE-LIMITS to
|
||||||
|
## a floor (one restart per FLOOR seconds) so many tenants saving at once can't
|
||||||
|
## trigger a restart storm. Debounce/floor are env-tunable (panel discloses the
|
||||||
|
## resulting "~60s" window to customers).
|
||||||
|
##
|
||||||
|
## Failure of THIS process is the silent-ticket failure mode (spec 7): if it
|
||||||
|
## dies, tenants' rewrite changes stop applying with no error. The entrypoint
|
||||||
|
## runs it and the panel monitors it (check-ols-htaccess-watcher.php).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
WATCH_ROOT="${OLS_WATCH_ROOT:-/mnt/users}"
|
||||||
|
DEBOUNCE="${OLS_HTACCESS_DEBOUNCE:-15}" # coalesce window (s)
|
||||||
|
FLOOR="${OLS_HTACCESS_FLOOR:-60}" # min seconds between restarts
|
||||||
|
LSWSCTRL=/usr/local/lsws/bin/lswsctrl
|
||||||
|
last_restart=0
|
||||||
|
|
||||||
|
log() { echo "ols-htaccess-watcher: $*" >&2; }
|
||||||
|
|
||||||
|
do_restart() {
|
||||||
|
now=$(date +%s)
|
||||||
|
if [ $((now - last_restart)) -lt "$FLOOR" ]; then
|
||||||
|
log "within ${FLOOR}s floor — coalescing, skipping restart"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if "$LSWSCTRL" restart >/dev/null 2>&1; then
|
||||||
|
last_restart=$now
|
||||||
|
log "graceful restart issued (.htaccess change)"
|
||||||
|
else
|
||||||
|
log "WARNING: lswsctrl restart failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v inotifywait >/dev/null 2>&1; then
|
||||||
|
log "FATAL: inotifywait not installed (inotify-tools)"; exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$WATCH_ROOT"
|
||||||
|
log "watching $WATCH_ROOT for .htaccess changes (debounce=${DEBOUNCE}s floor=${FLOOR}s)"
|
||||||
|
|
||||||
|
## -m monitor, -r recursive. We filter to .htaccess in the read loop rather than
|
||||||
|
## --include so this works on older inotify-tools too. modify/create/delete/move
|
||||||
|
## all matter (delete of .htaccess also changes rewrite behavior).
|
||||||
|
inotifywait -m -r -e modify,create,delete,move "$WATCH_ROOT" --format '%f' 2>/dev/null |
|
||||||
|
while read -r fname; do
|
||||||
|
case "$fname" in
|
||||||
|
.htaccess) ;;
|
||||||
|
*) continue ;;
|
||||||
|
esac
|
||||||
|
## A tenant .htaccess changed. Coalesce the save-burst, then restart ONCE.
|
||||||
|
##
|
||||||
|
## The coalesce is HARD-BOUNDED to DEBOUNCE seconds: a previous version blocked
|
||||||
|
## on `read -t DEBOUNCE` which, on a busy multi-tenant server, never timed out
|
||||||
|
## (unrelated file writes under $WATCH_ROOT kept resetting it) — so the restart
|
||||||
|
## was starved and rewrite changes silently never applied. Here we read further
|
||||||
|
## events only until the deadline OR ~2s of total quiet, whichever comes first,
|
||||||
|
## so continuous activity can delay us by at most DEBOUNCE. do_restart's FLOOR
|
||||||
|
## then rate-limits across consecutive bursts.
|
||||||
|
deadline=$(( $(date +%s) + DEBOUNCE ))
|
||||||
|
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||||
|
if read -r -t 2 _; then
|
||||||
|
continue # more activity — keep coalescing toward the deadline
|
||||||
|
else
|
||||||
|
break # ~2s of total quiet — the burst has settled
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
do_restart
|
||||||
|
done
|
||||||
160
scripts/render-shared-ols-config.sh
Normal file
160
scripts/render-shared-ols-config.sh
Normal 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)"
|
||||||
Reference in New Issue
Block a user