2026-06-09 18:28:34 -07:00
#!/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.
##
2026-06-10 06:54:28 -07:00
## 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.
2026-06-09 18:28:34 -07:00
##
2026-06-10 06:54:28 -07:00
## 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.
2026-08-05 11:38:19 -07:00
##
## THE $_SERVER STRINGS: the symlink makes paths RESOLVE, but the raw strings OLS
## put in $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] still read /mnt/users.
## The cac_path_parity extension (baked into the image, configured per-site
## below) rewrites those two at request start, so a site moved from cac-fpm to
## cac-lsphp sees byte-identical values. It replaced an auto_prepend_file
## normaliser that any site's own .user.ini silently displaced — see
## ext/cac-path-parity/cac_path_parity.c.
2026-06-09 18:28:34 -07:00
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
2026-06-10 06:54:28 -07:00
: " ${ domain :=localhost } "
export user domain
2026-06-09 18:28:34 -07:00
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
2026-06-10 06:54:28 -07:00
## ---- user + directories (identical to entrypoint-litespeed.sh: docroot at
## /home/$user, the customer's bind-mounted domain dir) ----
2026-06-09 18:28:34 -07:00
if ! id -u " $user " >/dev/null 2>& 1; then
useradd -u " $uid " -m -s /bin/bash " $user "
fi
2026-06-10 06:54:28 -07:00
mkdir -p "/home/ $user /public_html" "/home/ $user /logs/php-fpm"
2026-06-10 06:42:31 -07:00
2026-06-10 06:54:28 -07:00
## ---- 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
2026-08-05 13:05:57 -07:00
## Both of these get interpolated into generated php.ini fragments below. They
## are panel-validated and both already feed `ln -sfn` and the shared-ols vhost
## config, so a hostile value is not reachable today — this is the belt to that
## brace. A newline in $domain is an INI-DIRECTIVE INJECTION into the generated
## fragment (measured against the pre-fix script: domain=$'evil.com\nprecision =
## 7\n; ' put that directive in 99-cac-path-parity.ini and lsphp reported
## `precision => 7`); `$(...)` yields an ini parse error and `"` an empty value,
## and BOTH of those leave the
## path-parity extension INERT — the exact silent parity loss this whole change
## exists to eliminate. Quoting the emitted values (done below) neutralises
## newlines and quotes; it does NOT neutralise php.ini's own `${VAR}`
## interpolation, which is why the character class is checked as well.
INI_TOKENS_OK = yes
case " $user " in '' | *[ !A-Za-z0-9._-] *) INI_TOKENS_OK = no ;; esac
case " $SAFE_DOMAIN " in '' | *[ !A-Za-z0-9._-] *) INI_TOKENS_OK = no ;; esac
if [ " $INI_TOKENS_OK " != yes ] ; then
2026-08-05 13:46:52 -07:00
echo "WARNING: entrypoint-lsphp: user/domain contain characters outside [A-Za-z0-9._-] — refusing to generate php.ini fragments from them, so the \$_SERVER path-parity mapping and the per-site error_log are BOTH skipped (the extension stays inert, log_errors stays On from the image defaults and PHP logs to stderr i.e. \`docker logs\`; requests are unaffected). user= $( printf '%q' " $user " ) domain= $( printf '%q' " $domain " ) " >& 2
2026-08-05 13:05:57 -07:00
fi
2026-08-05 13:46:52 -07:00
2026-08-05 11:38:19 -07:00
## The exact path prefix the shared-ols container serves this site from — the
## string OLS puts in SCRIPT_FILENAME/DOCUMENT_ROOT. Used twice: for the symlink
## that makes it RESOLVE, and for the cac_path_parity mapping that makes it READ
## like cac-fpm. Deriving both from one variable keeps them in lockstep.
OLS_SITE_PATH = "/mnt/users/ $user / $SAFE_DOMAIN "
2026-06-10 06:54:28 -07:00
mkdir -p "/mnt/users/ $user "
2026-08-05 11:38:19 -07:00
ln -sfn "/home/ $user " " $OLS_SITE_PATH "
2026-06-09 18:28:34 -07:00
## ---- 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 } "
2026-08-02 15:23:05 -07:00
## ---- .user.ini support ----
## php-lsapi compiles .user.ini support in but leaves it DISABLED by default:
## sapi/litespeed/lsapi_main.c has `static int parse_user_ini = 0;` and only
## sets it in PHP_MINIT_FUNCTION(litespeed) when the PROCESS ENV contains
## LSPHP_ENABLE_USER_INI=on. Without it, lsphp never enters the user-ini chain
## at all — and does so SILENTLY, because `user_ini.filename` / `user_ini.cache_ttl`
## still report their core defaults in phpinfo(). Every other WHP PHP tier
## (cac, cac-fpm, cac-litespeed) honors .user.ini, so leaving it off here made
## the shared-ols tier quietly inconsistent: customer memory_limit /
## max_input_vars overrides were ignored, and — the reason this was found —
## Wordfence's `auto_prepend_file` WAF never loaded on ANY shared-ols site.
##
## Exported here rather than relying solely on the Dockerfile ENV because the
## runuser fallback below resets the environment; an export survives all three
## exec paths. Still overridable per-container (set LSPHP_ENABLE_USER_INI=off in
## the site's env) as an escape hatch for a site whose legacy cPanel-generated
## .user.ini has not been remediated yet.
export LSPHP_ENABLE_USER_INI = " ${ LSPHP_ENABLE_USER_INI :- on } "
echo "Container memory: ${ CONTAINER_MEMORY_MB } MB | PHP_LSAPI_CHILDREN= ${ PHP_LSAPI_CHILDREN } | LSAPI_MAX_IDLE= ${ LSAPI_MAX_IDLE } | PHPVER= ${ PHPVER } | bind= ${ LSPHP_BIND } | user_ini= ${ LSPHP_ENABLE_USER_INI } "
2026-06-09 18:28:34 -07:00
2026-08-05 13:46:52 -07:00
## Validate a numeric value destined for a generated php.ini fragment.
## Sets INI_NUM to the value when it is acceptable, and to "" (plus a WARNING)
## when it is not. Never fatal: a rejected override just leaves the image
## default in place, and the site serves either way.
##
## Digits-only is what closes the injection: no newline, quote, `$` or `{` can
## survive it, so neither an ini-directive injection nor php.ini's `${VAR}`
2026-08-05 14:08:09 -07:00
## interpolation is reachable regardless of what the caller sent.
##
## The range bound is a separate, weaker concern: it is a sanity check, NOT a
## guarantee that the value works. Measured — with `99-prod-overrides.ini`
## setting `opcache.interned_strings_buffer = 16`, a memory_consumption of 8 or
## 16 is ACCEPTED here and still aborts opcache at startup ("Insufficient shared
## memory for interned strings buffer"), loading no opcache at all. The floor is
## not raised to cover that because doing so would forfeit the superset property
## below; the panel clamps at 32, well clear of it.
2026-08-05 13:46:52 -07:00
validate_ini_num() {
local name = " $1 " val = " $2 " min = " $3 " max = " $4 "
INI_NUM = ""
case " $val " in
'' | *[ !0-9] *) ;;
*)
## Length-cap first: `[ -lt ]` on a 25-digit string is an arithmetic
## error, not a comparison. 7 digits covers every max below.
if [ " ${# val } " -le 7 ] && [ " $val " -ge " $min " ] && [ " $val " -le " $max " ] ; then
INI_NUM = " $val "
return 0
fi
;;
esac
echo "WARNING: entrypoint-lsphp: ${ name } = $( printf '%q' " $val " ) is not a plain integer in ${ min } - ${ max } — ignoring it; the image default from 99-prod-overrides.ini applies." >& 2
return 0
}
2026-06-09 18:28:34 -07:00
## ---- 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.
2026-08-05 11:38:19 -07:00
## Capture lsphp's own info once and read both answers out of it. Probe with
## `-i` ONLY: lsphp is the LSAPI SAPI, not the CLI — it accepts just
## -[b|c|n|h|i|q|s|v|?] and answers `-m`/`-r` by printing usage and exiting 0, so
## a `lsphp -m | grep` test never matches and never errors either.
2026-08-05 15:23:56 -07:00
##
## ---- CAC-TEST: probe helpers BEGIN ----
## Everything between these two markers is extracted verbatim and executed by
## scripts/tests/lsphp-info-probe.test.sh — the markers are inert comments with
## no runtime effect, and they exist so the test exercises THE SHIPPED CODE
## rather than a copy of it that can drift away from it.
##
## WHY THESE READ `$1` FROM A HERE-STRING AND NOT A PIPELINE. Both probes used
## to be `printf '%s\n' "$LSPHP_INFO" | <reader>`. `lsphp -i` is ~40 KB and both
## readers stop early — `grep -q` on first match, `awk` at `exit` — so the
## reader can close the pipe while printf is still writing to it. printf then
## takes SIGPIPE and dies 141, `set -o pipefail` (line 34) adopts 141 as the
## PIPELINE's status, and the test reads FALSE **because the thing it was
## looking for was present early enough to stop the reader**. Measured on whp02
## against the published cac-lsphp:php83: 5/5 runs status=141 with pipefail,
## 0 without.
##
## It reproduces on some hosts and not others, and the reason is the PIPE
## CAPACITY, not the payload alone. While the writer's whole output fits in the
## pipe it never blocks and always finishes before the reader can act; once it
## does not fit, the early exit is a guaranteed SIGPIPE. Linux gives a pipe
## 64 KiB by default — 40 KB fits, which is why this same image measured 0/10
## on the build host here — but drops NEW pipes to a single page once a user
## passes fs.pipe-user-pages-soft, which is the state a busy production host
## lives in. Forcing the payload over the limit makes it deterministic
## everywhere: 3x this output = 122100 bytes gave 141 141 141 in this very
## image. "It worked when I ran it" was never evidence about this bug.
##
## A here-string is not a pipeline at all: the shell materialises the whole
## string first (temp file, or a pipe only when it provably fits the pipe
## buffer) and the command's status is the reader's own status, so there is no
## second status for pipefail to prefer and no writer left alive to signal.
## `case`/`[[ ]]` would also avoid the pipeline, but would mean re-expressing an
## anchored line match as a glob over embedded newlines; keeping grep/awk with
## the SAME patterns makes this a plumbing change and nothing else.
##
## Both return the reader's status, so a genuinely-absent extension is still a
## clean 1 and a genuinely-missing "Scan this dir" line is still empty output.
lsphp_info_has_parity_ext() {
grep -q '^cac_path_parity support => enabled$' <<< " $1 "
}
lsphp_info_scan_dir() {
awk -F'=> ' '/^Scan this dir/ {print $2; exit}' <<< " $1 "
}
## Did `lsphp -i` answer at all? Separates "the extension is not there" from
## "our probe produced nothing to look in", so neither gets reported as the
## other. Keyed on the phpinfo banner, which is line 2 of every `lsphp -i`
## (verified against lsphp83 8.3.32) and is not something LSPHP_INFO could
## contain from any other source.
lsphp_info_is_usable() {
grep -q '^PHP Version => ' <<< " $1 "
}
## ---- CAC-TEST: probe helpers END ----
2026-08-05 11:38:19 -07:00
PATH_PARITY_MODE = "none"
LSPHP_INFO = $( " $LSPHP_BIN " -i 2>/dev/null || true )
2026-08-05 15:23:56 -07:00
SCAN_DIR = $( lsphp_info_scan_dir " $LSPHP_INFO " )
## `|| true` above is what keeps a broken probe survivable: fail-open is
## deliberate here and below — the site serves either way, only the $_SERVER
## strings differ. What the failure gets REPORTED as is handled at each of the
## two places it changes the outcome (the parity branch, and the no-scan-dir
## else at the bottom of this block).
2026-06-09 18:28:34 -07:00
if [ -n " $SCAN_DIR " ] ; then
mkdir -p " $SCAN_DIR "
2026-08-05 13:05:57 -07:00
## Values emitted double-quoted via printf rather than interpolated into an
## unquoted heredoc — see the INI_TOKENS_OK note above for what that prevents.
2026-08-05 13:46:52 -07:00
##
## Gated on INI_TOKENS_OK for the same reason the mapping below is: this
## fragment interpolates $user into generated ini too. Leaving it ungated was
## an INCONSISTENCY, not a live hole — a newline is inert inside the quotes,
## and a `${`-bearing $user cannot exist because the useradd above would have
## failed under `set -euo pipefail`. But "this particular unvetted value
## happens to be contained" is the reasoning this branch already rejected one
## screenful up, so it is not the reasoning that guards this line either.
##
## Rejecting costs such a user nothing it needs: `log_errors = On` is already
## baked in by 99-prod-overrides.ini, so PHP still logs — to stderr, i.e.
## `docker logs`, which is MORE visible than a per-site file, not less. No
## legitimate user reaches this branch (verified fleet-wide: 30 shared_ols
## sites across 4 hosts, none rejected by the charset check).
if [ " $INI_TOKENS_OK " = yes ] ; then
{
echo '; rendered at container start by entrypoint-lsphp.sh'
printf 'error_log = "%s"\n' "/home/ $user /logs/php-fpm/error.log"
echo 'log_errors = On'
} > " $SCAN_DIR /99-user-error-log.ini"
else
## The container filesystem survives `docker restart`, so a fragment an
## earlier boot wrote from a different env must not outlive the rejection.
rm -f " $SCAN_DIR /99-user-error-log.ini"
fi
2026-08-05 11:38:19 -07:00
## ---- $_SERVER path parity with cac-fpm ----
## Point the cac_path_parity extension at THIS site's mapping. Same two
## values the compatibility symlink above is built from, so the rewrite and
## the symlink can never disagree.
##
## Both settings are PHP_INI_SYSTEM: a customer's .user.ini (PHP_INI_PERDIR /
## PHP_INI_USER only) cannot redirect or disable them, and the extension
## occupies no userland hook — so the customer's own auto_prepend_file (the
## Wordfence WAF on several live sites) keeps working untouched. That
## combination is why this is an extension: the previous auto_prepend_file
## normaliser was itself PHP_INI_PERDIR and any site with its own prepend
## silently displaced it, while making OUR prepend win would have disabled
## THEIRS. See ext/cac-path-parity/cac_path_parity.c.
2026-08-05 13:05:57 -07:00
if [ " $INI_TOKENS_OK " != yes ] ; then
## Already warned above. Write NOTHING: neither the mapping (we will not
## generate ini from an unvetted string) nor the auto_prepend fallback (which
## would not be correct for such a site either). The extension stays inert,
## the request path is unaffected.
rm -f " $SCAN_DIR /99-cac-path-parity.ini" " $SCAN_DIR /99-cac-lsphp-normalize.ini"
PATH_PARITY_MODE = "none (user/domain rejected)"
2026-08-05 15:23:56 -07:00
elif lsphp_info_has_parity_ext " $LSPHP_INFO " ; then
2026-08-05 13:05:57 -07:00
{
echo '; rendered at container start by entrypoint-lsphp.sh'
printf 'cac_path_parity.from = "%s"\n' " $OLS_SITE_PATH "
printf 'cac_path_parity.to = "%s"\n' "/home/ $user "
} > " $SCAN_DIR /99-cac-path-parity.ini"
2026-08-05 11:38:19 -07:00
## Drop the pre-extension fallback if an older image left one here — the
## container filesystem survives a "docker restart", so an in-place upgrade
## must not keep a stale auto_prepend pointing at the old normaliser.
rm -f " $SCAN_DIR /99-cac-lsphp-normalize.ini"
PATH_PARITY_MODE = "extension"
else
## Degraded fallback for an image built before the extension existed (or one
## where it failed to load). Restores the old, .user.ini-defeatable
## behaviour rather than losing normalisation entirely — but say so loudly,
## because in this mode parity is NOT guaranteed.
2026-08-05 15:23:56 -07:00
##
## FAIL-OPEN, DELIBERATELY: a probe that cannot answer must never stop the
## container. The site serves either way; only the $_SERVER strings differ.
2026-08-05 11:38:19 -07:00
cat > " $SCAN_DIR /99-cac-lsphp-normalize.ini" <<'EOF'
; rendered at container start by entrypoint-lsphp.sh (DEGRADED FALLBACK)
2026-06-10 07:02:54 -07:00
auto_prepend_file = /scripts/cac-lsphp-normalize.php
2026-06-09 18:28:34 -07:00
EOF
2026-08-05 15:23:56 -07:00
## ...but do not DIAGNOSE more than was established. The old wording said
## "extension not loadable in this image" for EVERY reason this branch is
## reached — including the probe breaking on its own, which is exactly what
## happened (see the SIGPIPE note on the helpers above): a false verdict
## that sent operators to rebuild an image whose extension was fine and
## whose build gate had passed. The claim now carries its evidence, and the
## evidence is real: reaching here at all means SCAN_DIR was parsed out of
## this same output, so `lsphp -i` did answer and its module list is
## authoritative. The case where it did NOT answer never gets here — it is
## caught and reported honestly at the `lsphp_info_is_usable` check above.
2026-08-05 11:38:19 -07:00
PATH_PARITY_MODE = "auto_prepend (DEGRADED)"
2026-08-05 15:23:56 -07:00
echo "WARNING: entrypoint-lsphp: cac_path_parity extension not loadable in this image — ' ${ LSPHP_BIN } -i' answered ( ${# LSPHP_INFO } bytes, scan dir ${ SCAN_DIR } ) and does not list it — falling back to the auto_prepend normaliser, which a site's own .user.ini auto_prepend_file will silently displace. Rebuild/repull cac-lsphp:php ${ PHPVER } ." >& 2
2026-08-05 11:38:19 -07:00
fi
2026-06-09 18:28:34 -07:00
## Per-site opcache override (panel: Advanced Tuning → OpCache size); falls
## back to the baked lsphp-overrides.ini defaults when unset.
2026-08-05 13:46:52 -07:00
##
## SAME INJECTION CLASS AS THE MAPPING ABOVE, and it was left open when that
## one was closed. These two lines interpolated the raw env into an UNQUOTED
## `echo`, so (measured against the pre-fix script on this branch's image)
## OPCACHE_MEMORY_MB=$'128\nprecision = 7\n; ' put `precision = 7` into
## 99-user-opcache.ini and lsphp duly reported `precision => 7`.
##
## WHP does cast (int) and clamp these before setting the env
## (web-files/libs/site-pool-env.php: 32-512 MB, 2000-32000 files) — but
## "the panel validates it" is exactly the argument this branch rejected for
## `domain`, and the panel is a different repo on a different release cadence.
## Validate at the point of use, where the ini is actually generated.
##
2026-08-05 14:08:09 -07:00
## The accepted ranges below are deliberately a strict SUPERSET of the panel's
## clamps (32-512 and 2000-32000), so widening a panel clamp later can never
## start silently rejecting real sites here.
##
## Provenance, stated honestly: max_accelerated_files [200, 1000000] IS PHP's
## own clamp. For memory_consumption, 8 is PHP's documented floor but 4096 is
## OURS — PHP imposes no upper bound on that directive. It is a typo guard, not
## a vendor limit. An out-of-range value is not merely ignored: PHP resets the
## directive to its COMPILED default, discarding the image's own
## `99-prod-overrides` value, which is a further reason to reject rather than
## pass such a value through.
2026-08-05 13:46:52 -07:00
OPCACHE_LINES =()
if [ -n " ${ OPCACHE_MEMORY_MB :- } " ] ; then
validate_ini_num OPCACHE_MEMORY_MB " $OPCACHE_MEMORY_MB " 8 4096
if [ -n " $INI_NUM " ] ; then
OPCACHE_LINES +=( " $( printf 'opcache.memory_consumption = "%s"' " $INI_NUM " ) " )
fi
fi
if [ -n " ${ OPCACHE_MAX_FILES :- } " ] ; then
validate_ini_num OPCACHE_MAX_FILES " $OPCACHE_MAX_FILES " 200 1000000
if [ -n " $INI_NUM " ] ; then
OPCACHE_LINES +=( " $( printf 'opcache.max_accelerated_files = "%s"' " $INI_NUM " ) " )
fi
fi
if [ " ${# OPCACHE_LINES [@] } " -gt 0 ] ; then
2026-06-09 18:28:34 -07:00
{
echo "; rendered at container start by entrypoint-lsphp.sh"
echo "; per-site override from WHP whp.sites.opcache_*_override"
2026-08-05 13:46:52 -07:00
printf '%s\n' " ${ OPCACHE_LINES [@] } "
2026-06-09 18:28:34 -07:00
} > " $SCAN_DIR /99-user-opcache.ini"
2026-08-05 13:46:52 -07:00
else
## Nothing valid to say. Remove rather than leave whatever a previous boot
2026-08-05 14:08:09 -07:00
## wrote. Defensive only — do not read this as fixing a reachable bug: the
## writable layer does outlive a `docker restart`, but so does the
## environment, and changing these vars requires a RECREATE, which starts
## from a fresh layer with no stale fragment. Kept because it is free, and
## because it makes "no valid override" mean the same thing on every boot
## regardless of how the container got here.
2026-08-05 13:46:52 -07:00
rm -f " $SCAN_DIR /99-user-opcache.ini"
2026-06-09 18:28:34 -07:00
fi
2026-08-05 11:38:19 -07:00
else
## No scan dir means none of the per-site ini drop-ins land — including the
## path-parity mapping. Previously this failed silently; it must not, because
## the tier's cac-fpm parity guarantee is one of the things lost.
2026-08-05 15:23:56 -07:00
##
## Two different things land here and they are not the same report. "lsphp
## reports no additional-ini scan dir" ASSERTS that lsphp answered us, which
## is false when the probe produced nothing at all — and that was the wrong
## half of the same mistake the parity branch above made: describing a probe
## that could not answer as a finding about the image. Say which one it was.
if lsphp_info_is_usable " $LSPHP_INFO " ; then
echo "WARNING: entrypoint-lsphp: lsphp reports no additional-ini scan dir — per-site error_log, opcache and \$_SERVER path-parity settings were NOT applied." >& 2
PATH_PARITY_MODE = "none (no scan dir)"
else
echo "WARNING: entrypoint-lsphp: ' ${ LSPHP_BIN } -i' produced no usable phpinfo output ( ${# LSPHP_INFO } bytes) — per-site error_log, opcache and \$_SERVER path-parity settings were NOT applied. This is a PROBE failure and establishes nothing about what the image contains; run ' ${ LSPHP_BIN } -i' in this container before concluding anything about it." >& 2
PATH_PARITY_MODE = "none (lsphp -i unusable)"
fi
2026-06-09 18:28:34 -07:00
fi
2026-08-05 11:38:19 -07:00
echo "entrypoint-lsphp: \$_SERVER path parity = ${ PATH_PARITY_MODE } ( ${ OLS_SITE_PATH } -> /home/ ${ user } )"
2026-06-09 18:28:34 -07:00
## ---- ownership ----
2026-06-10 06:54:28 -07:00
## 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
2026-06-09 18:28:34 -07:00
## ---- 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