A site moved from cac-fpm to cac-lsphp must see byte-identical $_SERVER['DOCUMENT_ROOT'] and ['SCRIPT_FILENAME'] (/home/<user>/...). The auto_prepend_file normaliser that did this was PHP_INI_PERDIR, so any site with its own .user.ini auto_prepend_file silently displaced it — the state 7 live shared_ols sites (Wordfence, cPanel imports) are actually in. Hardening the hook was not an option either: making our prepend win would have disabled those Wordfence WAFs. Replace it with cac_path_parity, a small PHP extension that rewrites the filesystem-path $_SERVER keys from RINIT. RINIT cannot be displaced by .user.ini, and it occupies no userland hook, so the customer's own auto_prepend_file stays the only prepend in play and keeps working. The mapping lives in two PHP_INI_SYSTEM settings, which .user.ini (PERDIR / USER only) and ini_set() cannot reach. Mechanism is a path-component-bounded string prefix swap, not realpath(): byte-identical to cac-fpm by construction (realpath would resolve a customer's own symlinked public_html to some third path), no syscall, and no failure path. Every guard fails open and leaves $_SERVER untouched; nothing here can warn, throw or 500 a site. Unconfigured it is fully inert, so cac-fpm and cac-litespeed are unaffected. Built in a separate Dockerfile stage keyed off the existing ARG PHPVER — gcc/phpize/headers never reach the shipped image (verified absent; the image grows ~155kB), and a base-image PHP bump recompiles with no human step. A `lsphp -i | grep` assertion fails the build if the .so does not load, so an image can never ship having silently lost parity. The entrypoint selects the extension when present and removes any stale prepend ini left by an older image; if the extension is somehow not loadable it falls back to the old normaliser and logs a WARNING rather than losing normalisation entirely. It also now logs the active parity mode, and warns when lsphp reports no ini scan dir (previously silent). Probe lsphp with `-i` only: it is the LSAPI SAPI, not the CLI, and answers `-m`/`-r` by printing usage and exiting 0 — a `lsphp -m | grep` check never matches and never errors, which is the exact class of silent always-false assertion this change exists to remove. Verified: 6 .phpt tests; tests/fpm-parity-check.sh proves under the FPM SAPI that with a customer .user.ini auto_prepend_file present both keys are still corrected AND the customer's prepend still runs, and that the old mechanism does not; and in a real built cac-lsphp:php83 container that SCRIPT_FILENAME is rewritten, the customer prepend still fires, and another tenant's path is left untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
123 lines
6.5 KiB
Docker
123 lines
6.5 KiB
Docker
## 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
|
|
|
|
## ---- 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. Built against THIS image's own lsphp so the API/ABI
|
|
## (`PHP API` / extension_dir) always match; a PHP version bump in the base
|
|
## image therefore recompiles rather than silently loading a stale .so.
|
|
##
|
|
## 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
|
|
COPY ./ext/cac-path-parity /usr/src/cac-path-parity
|
|
RUN apt-get update && \
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
build-essential autoconf pkg-config \
|
|
lsphp${PHPVER}-dev && \
|
|
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)" && \
|
|
mkdir -p /build-out && \
|
|
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.
|
|
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"'
|
|
|
|
## 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 -m | 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: 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"]
|