The comment claimed the extension was "built against THIS image's own lsphp so
the API/ABI always match". It was not, and could not be: `lsphp<NN>-dev` is
absent from the LiteSpeed apt repo at the version every prebuilt OLS base image
ships, because that repo carries only the current release. Measured on
OLS 1.8.4 (2026-08-05), base image vs repo candidate:
lsphp81 8.1.33-5+noble -> 8.1.34-1+noble
lsphp83 8.3.28-1+noble -> 8.3.32-1+noble
lsphp85 8.5.0-3+noble -> 8.5.8-1+noble
and the literal fix — `apt-get install lsphp<NN>-dev="$(dpkg-query lsphp<NN>)"` —
fails on all three with `E: Version '<base>' for 'lsphp<NN>-dev' was not found`
(apt exit 100). So installing -dev necessarily upgrades lsphp in the build stage;
the only satisfiable direction is to move the runtime to meet it.
The skew the reviewer measured (a .so built on 8.3.32 shipped beside an 8.3.30
runtime, lsphp83-common at 8.3.31) was not vendor randomness: BOTH stages resolve
"repo latest" independently and Docker caches them independently. `COPY ./ext`
sits at the top of the ext-build stage, so editing the extension invalidated that
stage's apt layer while stage 2's stayed cached — i.e. every extension edit
rebuilt the .so against fresh headers and shipped it next to a stale runtime.
Fixed by making them one system:
- the toolchain layer moves ABOVE the source COPY, so editing the extension no
longer re-resolves the PHP version;
- it records the resolved version to /build-out/lsphp.version and asserts
lsphp<NN> == lsphp<NN>-dev in that stage;
- stage 2 COPYs that file in BEFORE its apt layer (so the version is part of
that layer's cache key) and pins lsphp/-common/-ldap to it, then asserts the
installed versions match. Unsatisfiable pin => loud apt failure with the
remediation, never a silent fallback.
Verified: builds clean on PHP 8.1/8.3/8.5; the shipped php83 image now reports a
uniform lsphp83 family at 8.3.32 (the previous image shipped lsphp83 8.3.30 /
-common 8.3.31 / -ldap 8.3.30). Mutation-tested by recording a version the repo
no longer has: build fails at stage 2 rather than shipping the skew.
The `lsphp -i | grep` build assertion is kept but its comment now says what it
does and does not prove: it catches a .so that will not LOAD, never silent
struct-layout drift.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
191 lines
10 KiB
Docker
191 lines
10 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.
|
|
##
|
|
## 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 '<base>' for 'lsphp<NN>-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"]
|