Add cac-litespeed image family (OpenLiteSpeed, native LSAPI)

New paid-tier per-customer image built on litespeedtech/openlitespeed:1.8.4-lsphpNN.
Matrix: 8.1-8.5. Native LSAPI suexec to customer uid, server-level LSCache,
all WP/WooCommerce extensions (memcached, redis, imagick, mbstring, etc.) baked in.

Files:
- Dockerfile.litespeed (FROM prebuilt LiteSpeed base, layers wp-cli/composer/mariadb)
- configs/litespeed/{httpd_config,site-template,lsphp-overrides}.tpl
- scripts/{entrypoint,create-vhost,detect-memory}-litespeed.sh + install-lscache-wp.sh

CI: new Build-LiteSpeed-Images matrix job. OLS_VERSION pinned to 1.8.4 (only
release with prebuilt images for all 5 PHP versions on Docker Hub).

Spec: whp/docs/superpowers/specs/2026-06-01-cac-litespeed-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 07:32:47 -07:00
parent 1756d496e5
commit 55c28a0c11
10 changed files with 711 additions and 0 deletions

View File

@@ -73,6 +73,50 @@ jobs:
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }}
Build-LiteSpeed-Images:
runs-on: ubuntu-latest
strategy:
matrix:
# PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base
# images stop at older OLS releases for those PHP versions, and the
# cac-litespeed tier is a paid premium offering: 8.1+ is the
# modernization story we're selling.
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 LiteSpeed Image
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.litespeed
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
OLS_VERSION=1.8.4
# OLS_VERSION pinned to 1.8.4 — only release with prebuilt images
# for every PHP version we ship (1.8.5 and 1.9.0 don't have an
# lsphp81 variant on Docker Hub). Bump alongside a local rebuild
# test when LiteSpeed publishes lsphp81 on a newer OLS release.
# See spec: docs/superpowers/specs/2026-06-01-cac-litespeed-design.md
tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:latest' || '' }}
Build-Shared-httpd:
runs-on: ubuntu-latest
steps:

92
Dockerfile.litespeed Normal file
View File

@@ -0,0 +1,92 @@
## cac-litespeed — OpenLiteSpeed customer container, LSAPI-native.
##
## Built on top of the LiteSpeed-maintained prebuilt image rather than
## installed-from-RPM on AlmaLinux 10. Rationale:
## - The EL10 RPM ships an empty /usr/local/lsws/cgid/ directory (the
## lscgid suexec helper is built by the upstream tarball install.sh,
## not packaged), which makes LSAPI unusable.
## - The prebuilt image is Ubuntu 24.04-based and includes lsphp +
## everything WP/WooCommerce needs out of the box (memcached, redis,
## imagick, mbstring, mysqlnd, intl, gd, soap, bcmath, gmp, sodium,
## opcache, ...) — saves us a dozen explicit installs and avoids the
## libonig.so.105 packaging bug entirely.
## - LiteSpeed Inc maintains it; OLS upgrades become a base-image bump.
##
## Tradeoff vs the rest of the CAC family: this image is Ubuntu-based,
## not AlmaLinux. The "cac" naming is now slightly misleading (it's no
## longer Cloud *Apache* Container, it's Cloud LiteSpeed Container) but
## the panel doesn't care and the customer-facing contract is identical.
## ARG before FROM is special — it can be used in the FROM line, but the
## value goes out of scope inside the image, so we re-declare ARG PHPVER
## after FROM for any RUN steps that need it.
ARG OLS_VERSION=1.8.5
ARG PHPVER=83
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
ARG PHPVER=83
ENV PHPVER=${PHPVER}
## Tooling we layer on top of the base:
## - gettext-base: envsubst for runtime template rendering
## - sudo: install-lscache-wp.sh runs wp-cli as the customer user
## - composer: not in the base image (wp-cli is)
## - cron: customer crontab support (mirror cac:phpXX behaviour)
## - mariadb-server + memcached: DEV-mode parity with the existing CAC
## entrypoints. PROD mode never starts these.
## - lsphp83-ldap: not in base image, useful for some WP plugins
##
## All apt cache is cleaned in the same layer to keep image size down.
## lsphp${PHPVER}-ldap is the only extra ext we add (everything else WP needs
## ships in the prebuilt base). lsphp84 + lsphp85 don't ship imap or pspell
## from LiteSpeed — customers needing imap should pin to 8.3 or earlier.
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gettext-base sudo cron \
ca-certificates curl wget \
mariadb-server memcached \
lsphp${PHPVER}-ldap && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
## Composer (matches the wp-cli pattern in the existing CAC Dockerfile —
## phar download, no `php install` pipe since lsphp's CLI mode is fine).
RUN curl -fsSL -o /usr/local/bin/composer https://getcomposer.org/download/latest-stable/composer.phar && \
chmod +x /usr/local/bin/composer
## Our scripts + config templates layer in last (they change most often,
## keep them off the slow apt layer).
COPY ./scripts/entrypoint-litespeed.sh \
./scripts/create-vhost-litespeed.sh \
./scripts/detect-memory-litespeed.sh \
./scripts/install-lscache-wp.sh \
./scripts/log-rotate.sh \
/scripts/
RUN chmod +x /scripts/*
COPY ./configs/litespeed/ /etc/lsws-templates/
## Apply our production lsphp ini overrides. Ask lsphp for its scan dir
## directly (varies by PHP minor version: 8.3/8.4/8.5 each have their own
## /usr/local/lsws/lsphpNN/etc/php/8.M/mods-available/). Dockerfile RUN uses
## /bin/sh so we explicitly `bash -c` for safer scripting.
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"'
## Disable the OLS WebAdmin port for customer-facing containers. Bind admin
## listener to loopback so it's unreachable even from the docker network.
RUN sed -i 's|^[[:space:]]*address[[:space:]]\+\*:| address 127.0.0.1:|' \
/usr/local/lsws/admin/conf/admin_config.conf 2>/dev/null || true
## Cron entry for log rotation (mirrors cac:phpXX).
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
EXPOSE 80 443
## Healthcheck: the entrypoint drops a static /healthz into the customer
## docroot at boot, so this passes even before any customer files exist.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsSk https://127.0.0.1/healthz || exit 1
ENTRYPOINT ["/scripts/entrypoint-litespeed.sh"]

View File

@@ -0,0 +1,48 @@
## OpenLiteSpeed APPEND fragment — added to the stock httpd_config.conf
## that ships with litespeedtech/openlitespeed. Keeping the stock config
## intact preserves all the cgid/lscgid plumbing (CGIRLimit defaults,
## fileAccessControl defaults, etc.) — when we tried writing a fully
## custom httpd_config.conf, lscgid never created its IPC socket and
## every PHP request 503'd. The upstream OLS docker template uses this
## append pattern too (see setup_docker.sh in litespeedtech/ols-dockerfiles).
##
## Rendered at container start by scripts/create-vhost-litespeed.sh via
## envsubst. Templated vars: $user $domain $vhost_map_aliases.
## --- our listeners (replace stock Default :8088) ---
listener HTTP {
address *:80
secure 0
map siteVH *
## NB: HTTPHTTPS redirect is in site-template.tpl's rewrite{} block,
## NOT here — OLS 1.8 listener-level rewrites are inert for vhTemplate
## members. Don't move it back to this listener.
}
listener HTTPS {
address *:443
secure 1
keyFile /usr/local/lsws/conf/cert/self.key
certFile /usr/local/lsws/conf/cert/self.crt
sslProtocol 24
enableSpdy 15
enableQuic 0
map siteVH *
}
## --- our vhost via vhTemplate (upstream's working pattern) ---
## The template file is /usr/local/lsws/conf/templates/site.conf — written
## by create-vhost-litespeed.sh at the same time as this fragment.
vhTemplate site {
templateFile conf/templates/site.conf
listeners HTTP, HTTPS
note cac-litespeed per-customer vhost
## vhDomain: customer's domain + serveralias list + `*` catchall so
## ip-only requests (e.g. HAProxy backend health check by container_name)
## still resolve. WHP/HAProxy filters hostnames upstream no risk to
## allowing the catchall here.
member siteVH {
vhDomain ${domain}${vhost_map_aliases}, *
}
}

View File

@@ -0,0 +1,43 @@
; Production lsphp overrides — mirrors configs/prod-php.ini for the FPM
; image, adapted for LSAPI defaults. Dropped into /usr/local/lsws/lsphpNN/etc/php.d/
memory_limit = 256M
post_max_size = 384M
upload_max_filesize = 256M
max_input_vars = 2000
max_execution_time = 60
max_input_time = 120
expose_php = Off
short_open_tag = Off
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
zend.exception_ignore_args = On
session.save_handler = files
session.use_cookies = 1
session.use_only_cookies = 1
session.use_strict_mode = 0
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 60
opcache.enable_cli = Off
output_buffering = 4096
default_charset = "UTF-8"
file_uploads = On
max_file_uploads = 20
soap.wsdl_cache_enabled = 1
soap.wsdl_cache_dir = "/tmp"
soap.wsdl_cache_ttl = 86400
soap.wsdl_cache_limit = 5

View File

@@ -0,0 +1,87 @@
## OLS vhTemplate for the per-customer vhost. Mirrors the structure of the
## upstream docker.conf template but with our paths and LSCache wiring.
## Templated vars (envsubst): $user
##
## $VH_NAME, $VH_ROOT, $DOC_ROOT, $SERVER_ROOT are OLS macros — they MUST
## stay literal in the output (not in the envsubst allow-list).
allowSymbolLink 1
enableScript 1
restrained 1
## setUIDMode 2 = DocRoot UID — lsphp suexec's to the OWNER of vhRoot.
## We chown /home/${user} to ${user}:${user} in the entrypoint, so PHP
## runs as the customer per request. Container is still the privsep
## boundary; this is the clean "scripts run as user" model.
setUIDMode 2
vhRoot /home/${user}/public_html/
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhconf.conf
virtualHostConfig {
docRoot $VH_ROOT
errorlog /home/${user}/logs/litespeed/error.log {
useServer 0
logLevel WARN
rollingSize 10M
keepDays 14
compressArchive 1
}
accesslog /home/${user}/logs/litespeed/access.log {
useServer 0
rollingSize 10M
keepDays 7
compressArchive 1
}
index {
useServer 0
indexFiles index.php, index.html
autoIndex 0
}
## LSCache plugin owns Cache-Control / Expires entirely server-level
## expires off so we don't double-emit headers.
expires {
enableExpires 0
}
accessControl {
allow *
}
context / {
location $DOC_ROOT/
allowBrowse 1
rewrite {
enable 1
inherit 0
autoLoadHtaccess 1
RewriteFile .htaccess
}
addDefaultCharset off
}
rewrite {
enable 1
autoLoadHtaccess 1
logLevel 0
## Force HTTPS — OLS 1.8 listener-level rewrites don't apply per-vhost,
## so the redirect lives here. The RewriteCond guards against an infinite
## loop (SERVER_PORT=80 means "this request came in on the HTTP listener,
## not HTTPS"). Per-customer .htaccess rules still apply (autoLoadHtaccess).
RewriteCond %{SERVER_PORT} 80
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [L,R=301]
}
## Per-vhost LSCache storage. The server-level `module cache` block in
## stock httpd_config.conf is already enabled (ls_enabled 1); the LSCWP
## plugin flips cache on/off per request via X-LiteSpeed-Cache-Control.
module cache {
storagePath /home/${user}/lscache
checkPrivateCache 1
checkPublicCache 1
enableCache 0
enablePrivateCache 0
}
}

View File

@@ -0,0 +1,77 @@
## Per-vhost config — rendered at container start.
## Templated vars (envsubst allow-list): $user $domain
## Anything that looks like $DOC_ROOT, $VH_ROOT, $HTTP_HOST etc. is an OLS
## runtime macro — intentionally NOT in the envsubst allow-list so it
## passes through unchanged for OLS to expand at request time.
docRoot /home/${user}/public_html
enableGzip 1
enableBr 1
errorlog /home/${user}/logs/litespeed/error.log {
useServer 0
logLevel WARN
rollingSize 10M
keepDays 14
compressArchive 1
}
accesslog /home/${user}/logs/litespeed/access.log {
useServer 0
rollingSize 10M
keepDays 7
compressArchive 1
}
index {
useServer 0
indexFiles index.php, index.html
autoIndex 0
}
scripthandler {
add lsapi:lsphp php
}
## LSCache plugin owns Cache-Control / Expires entirely — keep server-level
## expires off so we don't double-emit headers.
expires {
enableExpires 0
}
accessControl {
allow *
}
context / {
## $DOC_ROOT is an OLS macro (not a shell var). Don't add it to the
## envsubst allow-list in create-vhost-litespeed.sh or it'll expand to
## empty and break docroot resolution.
location $DOC_ROOT/
allowBrowse 1
rewrite {
enable 1
inherit 0
autoLoadHtaccess 1
RewriteFile .htaccess
}
addDefaultCharset off
}
rewrite {
enable 1
autoLoadHtaccess 1
logLevel 0
}
## Per-vhost LSCache storage. Server module cache{} block enables the engine;
## these lines tell the vhost WHERE to cache. The LSCWP plugin flips the
## cache on/off at request time via X-LiteSpeed-Cache-Control headers.
module cache {
storagePath /home/${user}/lscache
checkPrivateCache 1
checkPublicCache 1
enableCache 0
enablePrivateCache 0
}

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
## create-vhost-litespeed.sh — sets up OLS config for one customer site.
##
## Approach: keep the stock LiteSpeed-shipped httpd_config.conf VERBATIM
## (it has all the cgid/lscgid plumbing that lscgid needs to actually
## create its IPC socket), and just APPEND our listeners + vhTemplate.
## The custom vhost template lives at conf/templates/site.conf and points
## at /home/${user}/public_html. envsubst renders our user/domain into
## both files at container start.
##
## Expects in env: user, domain, serveralias (optional).
set -euo pipefail
TPL_DIR=${TPL_DIR:-/etc/lsws-templates}
LSWS_CONF=/usr/local/lsws/conf
## Ensure the conf dir has stock config to append to. On first boot with
## a fresh image this is a no-op (image ships with conf/ populated). With
## a future volume mount of conf/, the upstream entrypoint pattern would
## copy from .conf/* — keep parity:
if [ -z "$(ls -A -- "$LSWS_CONF/" 2>/dev/null)" ]; then
cp -R /usr/local/lsws/.conf/* "$LSWS_CONF/"
fi
## Build the serveralias suffix for vhDomain. Empty for none, else
## ",alias1,alias2" prepended to the comma list.
vhost_map_aliases=""
if [ -n "${serveralias:-}" ]; then
for alias in $(echo "$serveralias" | tr ',' ' '); do
[ -z "$alias" ] && continue
vhost_map_aliases="${vhost_map_aliases},${alias}"
done
fi
export vhost_map_aliases user domain
## --- prep the stock httpd_config.conf before appending ours ---
## Stock ships with `listener HTTP {*:80}`, `listener HTTPS {*:443}`, and
## a `vhTemplate docker` mapped to /var/www/vhosts/$VH_NAME/html — these
## conflict with our ports and would shadow our siteVH vhost. Strip them
## and the demo `virtualHost Example`, but KEEP `listener Default` (it's
## bound to 8088 — harmless internally, removing risks unrelated breakage).
## Always restart from a stock copy so re-runs are idempotent (otherwise
## a second sed pass on already-stripped config corrupts it).
cp /usr/local/lsws/.conf/httpd_config.conf "$LSWS_CONF/httpd_config.conf"
## Strip the stock blocks we replace. Use awk: easier than sed range-deletes
## to skip a NAMED block of arbitrary length terminated by a top-level `}`.
awk '
BEGIN { skip = 0 }
/^listener HTTP \{/ || /^listener HTTPS \{/ || /^vhTemplate docker \{/ { skip = 1; next }
skip && /^\}/ { skip = 0; next }
!skip { print }
' "$LSWS_CONF/httpd_config.conf" > "$LSWS_CONF/httpd_config.conf.new"
mv "$LSWS_CONF/httpd_config.conf.new" "$LSWS_CONF/httpd_config.conf"
## --- append our listeners + vhTemplate ---
SENTINEL="## ---- cac-litespeed append (do not edit below) ----"
{
echo ""
echo "$SENTINEL"
envsubst '${user} ${domain} ${vhost_map_aliases}' < "$TPL_DIR/httpd_config.tpl"
} >> "$LSWS_CONF/httpd_config.conf"
## --- write our vhost template to /usr/local/lsws/conf/templates/site.conf ---
envsubst '${user}' < "$TPL_DIR/site-template.tpl" \
> "$LSWS_CONF/templates/site.conf"
## --- per-vhost config file the vhTemplate will reference ---
## OLS creates conf/vhosts/$VH_NAME/ at template-instantiation time, but
## we pre-create it to satisfy the configFile path and write a minimal
## vhconf.conf (empty body — all real config is inline in the template's
## virtualHostConfig{} block).
mkdir -p "$LSWS_CONF/vhosts/siteVH"
echo "## auto-generated; real vhost config is in templates/site.conf" \
> "$LSWS_CONF/vhosts/siteVH/vhconf.conf"
## Permissions: OLS reads conf/ as lsadm. Don't break that.
chown -R lsadm:nogroup "$LSWS_CONF" 2>/dev/null || true

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
## detect-memory-litespeed.sh — sibling to detect-memory.sh.
## Computes LSAPI_CHILDREN + extprocessor memSoftLimit/memHardLimit from
## container memory cap. Sourced by entrypoint-litespeed.sh.
## ---- container memory detection (mirrors detect-memory.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 (LSAPI workers get the lion's share) ----
OS_RESERVE_MB=50
OLS_RESERVE_MB=40 # OpenLiteSpeed daemon footprint
DEV_OVERHEAD_MB=0
if [ "${environment:-PROD}" = "DEV" ]; then
DEV_OVERHEAD_MB=125
fi
AVAILABLE_MB=$((CONTAINER_MEMORY_MB - OS_RESERVE_MB - OLS_RESERVE_MB - DEV_OVERHEAD_MB))
if [ "$AVAILABLE_MB" -lt 60 ]; then
AVAILABLE_MB=60
fi
## ---- LSAPI children (analogous to PHP_FPM_MAX_CHILDREN) ----
## LSAPI is more memory-efficient than FPM; estimate 96 MB / worker
## (vs 128 MB for FPM after the 2026-06-01 bump). Floor 2, cap 50.
LSPHP_WORKER_ESTIMATE_MB=${LSPHP_WORKER_ESTIMATE_MB:-96}
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 knobs — site-pool-env.php still passes FPM_MAX_CHILDREN
## for backward compat, so prefer LSAPI_CHILDREN if set, else FPM_MAX_CHILDREN,
## else the calculated value.
LSAPI_CHILDREN=${LSAPI_CHILDREN:-${FPM_MAX_CHILDREN:-$calc_lsapi_children}}
## extprocessor mem limits — total LSAPI heap should fit AVAILABLE_MB with
## some breathing room. Soft = budget/children, hard = soft * 1.5, both capped
## at 2047 (OLS interprets > 2047 oddly in some 1.x builds).
LSAPI_MEM_SOFT=$((AVAILABLE_MB / LSAPI_CHILDREN))
if [ "$LSAPI_MEM_SOFT" -lt 64 ]; then LSAPI_MEM_SOFT=64; fi
if [ "$LSAPI_MEM_SOFT" -gt 2047 ]; then LSAPI_MEM_SOFT=2047; fi
LSAPI_MEM_HARD=$((LSAPI_MEM_SOFT * 3 / 2))
if [ "$LSAPI_MEM_HARD" -gt 2047 ]; then LSAPI_MEM_HARD=2047; fi
export CONTAINER_MEMORY_MB LSAPI_CHILDREN LSAPI_MEM_SOFT LSAPI_MEM_HARD

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env bash
## entrypoint-litespeed.sh — PID 1 for cac-litespeed:phpNN.
## Built on litespeedtech/openlitespeed:1.8.x-lsphp83 prebuilt base. Native
## LSAPI (no FPM proxy), one customer per container.
##
## Process supervision: starts OLS via `openlitespeed -n` (no-daemon +
## crash-guard, per OLS source: lshttpdmain.cpp). SIGTERM is forwarded.
## crond runs in the background for customer crontabs; OLS itself is the
## process we wait on (if OLS dies, the container exits and Docker
## restarts it per its restart policy).
set -euo pipefail
: "${PHPVER:=83}"
: "${environment:=PROD}"
: "${LSCACHE_AUTOINSTALL:=1}"
export CONTAINER_ROLE="litespeed_only"
export PHPVER environment LSCACHE_AUTOINSTALL
## ---- env validation ----
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
## ---- user + directories ----
if ! id -u "$user" >/dev/null 2>&1; then
## Ubuntu's useradd; mirror what the AL10 entrypoints do with adduser
useradd -u "$uid" -m -s /bin/bash "$user"
fi
mkdir -p "/home/$user/public_html"
mkdir -p "/home/$user/logs/litespeed"
mkdir -p "/home/$user/lscache"
mkdir -p /tmp/lshttpd/swap
chmod 1777 /tmp/lshttpd
## ---- memory + lsphp pool sizing ----
# shellcheck source=/dev/null
source /scripts/detect-memory-litespeed.sh
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | LSAPI_CHILDREN=${LSAPI_CHILDREN} | memSoft=${LSAPI_MEM_SOFT}M memHard=${LSAPI_MEM_HARD}M | PHPVER=${PHPVER}"
## ---- self-signed cert (idempotent) ----
mkdir -p /usr/local/lsws/conf/cert
if [ ! -f /usr/local/lsws/conf/cert/self.crt ]; then
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout /usr/local/lsws/conf/cert/self.key \
-out /usr/local/lsws/conf/cert/self.crt \
-subj "/CN=${domain}" 2>/dev/null
fi
## ---- render httpd_config + vhconf from templates ----
/scripts/create-vhost-litespeed.sh
## ---- ownership: OLS master/workers run as nobody; lsphp suexecs to the
## customer per request (setUIDMode 2 in httpd_config.tpl). So the customer
## owns everything under /home/$user — clean ownership model, no nobody
## chowning. OLS's own runtime dirs stay nobody-owned.
chown -R nobody:nogroup /usr/local/lsws/logs /usr/local/lsws/conf/cert /tmp/lshttpd 2>/dev/null || true
chown -R "$user:$user" "/home/$user"
chmod 755 "/home/$user"
## ---- drop healthz so docker HEALTHCHECK passes before customer files
## Always rewrite as customer; suexec lsphp will read it as that uid too.
sudo -u "$user" sh -c "echo ok > /home/$user/public_html/healthz"
## ---- DEV: local mariadb + memcached for parity with cac entrypoints ----
if [ "$environment" = "DEV" ]; then
echo "Starting Dev Deployment (litespeed)"
mkdir -p "/home/$user/_db_backups"
## mariadb-server + memcached are already in the image (apt-installed
## in the Dockerfile). Just start them.
mkdir -p /run/mysqld && chown mysql:mysql /run/mysqld
nohup mysqld --user=mysql &>/dev/null &
if [ ! -f "/home/$user/mysql_creds" ]; then
sleep 10
mysql_user=$(openssl rand -hex 7)
mysql_password=$(openssl rand -hex 12)
mysql_db="devdb_$(openssl rand -hex 3)"
mysql -e "CREATE DATABASE $mysql_db;"
mysql -e "CREATE USER '$mysql_user'@'localhost' IDENTIFIED BY '$mysql_password';"
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '$mysql_user'@'localhost' WITH GRANT OPTION;"
mysql -e "FLUSH PRIVILEGES;"
{
echo "MySQL User: $mysql_user"
echo "MySQL Password: $mysql_password"
echo "MySQL Database: $mysql_db"
} > "/home/$user/mysql_creds"
cat "/home/$user/mysql_creds"
fi
/usr/bin/memcached -d -u "$user"
fi
## ---- user crontab ----
if [ ! -f "/home/$user/crontab" ]; then
{
echo "# User crontab for $user"
echo "# Add your cron jobs here"
} > "/home/$user/crontab"
chown "$user:$user" "/home/$user/crontab"
fi
crontab -u "$user" "/home/$user/crontab"
service cron start >/dev/null 2>&1 || /usr/sbin/cron
## ---- LSCache plugin (background, non-fatal) ----
( /scripts/install-lscache-wp.sh "$user" >>/var/log/lscache-install.log 2>&1 || true ) &
## ---- start OLS in foreground with crash-guard ----
## openlitespeed -n = no-daemon + supervisor. Trap forwards SIGTERM cleanly.
OLS_PID=""
trap '[ -n "$OLS_PID" ] && kill -TERM "$OLS_PID" 2>/dev/null; wait "$OLS_PID" 2>/dev/null || true' TERM INT
/usr/local/lsws/bin/openlitespeed -n &
OLS_PID=$!
## Stream OLS + customer logs to PID-1 stdout so `docker logs` works.
touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log
touch "/home/$user/logs/litespeed/error.log" "/home/$user/logs/litespeed/access.log"
tail -F /usr/local/lsws/logs/error.log \
/usr/local/lsws/logs/access.log \
"/home/$user/logs/litespeed/error.log" \
"/home/$user/logs/litespeed/access.log" 2>/dev/null &
wait "$OLS_PID"

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
## install-lscache-wp.sh — auto-install the official litespeed-cache plugin
## on first boot if WP is detected and plugin not already managed.
## Idempotent: re-runs are no-ops. Honors LSCACHE_AUTOINSTALL=0 escape hatch.
##
## Args: $1 = $user (customer system user)
set -euo pipefail
user="${1:?usage: install-lscache-wp.sh <user>}"
home="/home/${user}"
if [ "${LSCACHE_AUTOINSTALL:-1}" = "0" ]; then
echo "[lscache] LSCACHE_AUTOINSTALL=0 — skipping plugin install."
exit 0
fi
if [ ! -f "$home/public_html/wp-config.php" ]; then
echo "[lscache] No wp-config.php in $home/public_html — skipping (not a WP site)."
exit 0
fi
## With setUIDMode 2, lsphp runs as the customer, and customer owns their
## home tree — wp-cli also runs as the customer, files end up correctly owned.
if ! command -v wp >/dev/null 2>&1; then
echo "[lscache] wp-cli not on PATH — skipping (image build issue, not fatal)."
exit 0
fi
if sudo -u "$user" -- wp --path="$home/public_html" plugin is-installed litespeed-cache 2>/dev/null; then
echo "[lscache] litespeed-cache already installed — leaving customer's settings alone."
exit 0
fi
echo "[lscache] Installing litespeed-cache plugin for $user"
sudo -u "$user" -- wp --path="$home/public_html" plugin install litespeed-cache --activate \
|| { echo "[lscache] plugin install failed (network? wp-cli? perms?) — non-fatal."; exit 0; }
echo "[lscache] Done."