## 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 :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 ## ---- stage 1: build the cac_path_parity extension -------------------------- ## $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] parity with cac-fpm, enforced ## from RINIT so a customer's .user.ini cannot displace it — see ## ext/cac-path-parity/cac_path_parity.c for why this is an extension and not an ## auto_prepend_file. ## ## WHICH lsphp THE .so IS BUILT AGAINST — read this before touching the apt lines. ## `lsphp${PHPVER}-dev` is NOT available at the version the base image ships: ## the LiteSpeed apt repo carries only the CURRENT release, and every prebuilt ## OLS base image is behind it (measured 2026-08-05 on OLS 1.8.4: ## lsphp81 8.1.33 base / 8.1.34 repo, lsphp83 8.3.28 / 8.3.32, ## lsphp85 8.5.0 / 8.5.8). ## Pinning -dev to the base version fails on all three with ## `E: Version '' for 'lsphp-dev' was not found`. ## ## So installing -dev necessarily UPGRADES lsphp in this stage. The parity we can ## have — and the one this file now guarantees — is the other direction: the ## shipped runtime is pinned to whatever version this stage compiled against. ## That version is recorded here and consumed by stage 2, so the two apt layers ## are cache-locked to each other. Without this, `COPY ./ext` invalidating only ## THIS stage while stage 2's apt layer stayed cached produced a real, repeatable ## skew (reviewer measured a .so built on 8.3.32 shipped next to an 8.3.30 ## runtime, with lsphp83-common at 8.3.31 — the vendor family is not always ## uniformly versioned either). ## ## Benign in practice (PHP holds ABI stable across a patch series) but it is the ## riskier direction — headers NEWER than the runtime — and the runtime assertion ## in stage 2 catches only load failure, never silent struct-layout drift. ## ## Separate stage on purpose: the compiler + headers (~400MB) stay out of the ## shipped image, which gains only the ~40KB .so. Costs ~1-2 min of CI per PHP ## version; both stages share the same base layer, so no extra pull. FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} AS ext-build ARG PHPVER=83 ## Toolchain layer, deliberately BEFORE the source COPY so editing the extension ## does not re-resolve the PHP version (which is what caused the skew above). ## Records the exact lsphp version the headers belong to; stage 2 pins to it. RUN set -e; \ apt-get update; \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential autoconf pkg-config \ lsphp${PHPVER}-dev; \ RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \ DEVV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}-dev); \ if [ "$RTV" != "$DEVV" ]; then \ echo "FATAL: lsphp${PHPVER}=$RTV but lsphp${PHPVER}-dev=$DEVV — the headers" >&2; \ echo " do not belong to the PHP in this stage. Refusing to build." >&2; \ exit 1; \ fi; \ mkdir -p /build-out; \ printf '%s' "$RTV" > /build-out/lsphp.version; \ echo "cac_path_parity will be compiled against lsphp${PHPVER} $RTV" COPY ./ext/cac-path-parity /usr/src/cac-path-parity RUN set -e; \ cd /usr/src/cac-path-parity; \ /usr/local/lsws/lsphp${PHPVER}/bin/phpize; \ ./configure --enable-cac-path-parity \ --with-php-config=/usr/local/lsws/lsphp${PHPVER}/bin/php-config; \ make -j"$(nproc)"; \ cp modules/cac_path_parity.so /build-out/ ## ---- stage 2: the shipped sidecar image ------------------------------------ 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. ## ## VERSION LOCKSTEP: `apt-get install lsphpNN-ldap` pulls lsphpNN-common forward, ## which drags the whole lsphpNN family to the repo's current release — the same ## upgrade the ext-build stage gets. Left implicit, the two stages resolve that ## independently and Docker caches them independently, so they drift apart (see ## the long comment on stage 1). Copying stage 1's recorded version in BEFORE ## this layer makes the version part of this layer's cache key: same version => ## cache hit, new version => this layer re-runs and lands on the same one. The ## explicit `=$V` pins then make a mid-build repo roll a LOUD apt failure instead ## of a silent skew. Verified satisfiable on PHP 8.1/8.3/8.5 (2026-08-05). COPY --from=ext-build /build-out/lsphp.version /etc/cac-lsphp-build.version RUN set -e; \ V=$(cat /etc/cac-lsphp-build.version); \ apt-get update; \ if ! DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ ca-certificates \ lsphp${PHPVER}="$V" lsphp${PHPVER}-common="$V" lsphp${PHPVER}-ldap="$V"; then \ echo "FATAL: lsphp${PHPVER} $V is what cac_path_parity was compiled against," >&2; \ echo " but the LiteSpeed repo no longer offers it (it keeps only the" >&2; \ echo " current release). The ext-build stage is almost certainly a stale" >&2; \ echo " cache hit — rebuild with --no-cache." >&2; \ exit 1; \ fi; \ apt-get clean; \ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \ RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \ CMV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}-common); \ if [ "$RTV" != "$V" ] || [ "$CMV" != "$V" ]; then \ echo "FATAL: cac_path_parity.so was compiled against lsphp${PHPVER} $V but this" >&2; \ echo " image would ship lsphp${PHPVER}=$RTV / -common=$CMV." >&2; \ echo " Rebuild with --no-cache so both stages resolve the same release." >&2; \ exit 1; \ fi; \ echo "runtime lsphp${PHPVER} pinned to $V (the version cac_path_parity was built against)" ## 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"' ## Install the cac_path_parity extension into lsphp's own extension_dir and load ## it unconditionally. It is INERT until the entrypoint writes the per-site ## cac_path_parity.from/.to mapping, so it is safe in any context (including ## wp-cli runs, where $_SERVER carries no filesystem paths). ## ## The trailing `lsphp -i | grep` is a BUILD-TIME ASSERTION: if the .so fails to ## load (ABI drift after a base-image PHP bump, bad build) the image build fails ## here rather than shipping a sidecar that silently lost path parity. Note its ## limit: it proves the .so LOADS, not that it was built against these exact ## structs — silent layout drift would sail straight through. The version lockstep ## above is what actually removes that possibility; this stays as the backstop. ## NOTE: probe lsphp with `-i` ONLY. The lsphp binary is the LSAPI SAPI, not the ## CLI — it accepts just -[b|c|n|h|i|q|s|v|?] and answers anything else (`-m`, ## `-r`) by printing its usage text and exiting 0. A `lsphp -m | grep` check ## therefore never matches AND never fails, which is exactly the kind of silent ## always-false assertion this whole change exists to eliminate. COPY --from=ext-build /build-out/cac_path_parity.so /tmp/cac_path_parity.so RUN bash -c 'set -e; \ LSPHP="/usr/local/lsws/lsphp${PHPVER}/bin/lsphp"; \ EXT_DIR=$("$LSPHP" -i 2>/dev/null | awk -F" => " "/^extension_dir/ {print \$2; exit}"); \ SCAN_DIR=$("$LSPHP" -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \ mkdir -p "$EXT_DIR" "$SCAN_DIR"; \ mv /tmp/cac_path_parity.so "$EXT_DIR/"; \ printf "; installed by Dockerfile.lsphp\nextension=cac_path_parity.so\n" \ > "$SCAN_DIR/00-cac-path-parity.ini"; \ "$LSPHP" -i 2>/dev/null | grep -q "^cac_path_parity support => enabled$"; \ echo "cac_path_parity installed into $EXT_DIR and verified loadable"' ## php-lsapi gates .user.ini parsing behind this env var (see entrypoint-lsphp.sh ## for the full explanation). Set here so the value is visible in `docker inspect` ## and survives an entrypoint override; the entrypoint re-exports it with the same ## default so the runuser exec path can't drop it. ENV LSPHP_ENABLE_USER_INI=on EXPOSE 9000 ## TCP-connect + lsphp-alive check (LSAPI isn't FastCGI, so no cgi-fcgi ping). HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD /scripts/healthcheck-lsphp.sh ENTRYPOINT ["/scripts/entrypoint-lsphp.sh"]