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:
78
scripts/create-vhost-litespeed.sh
Normal file
78
scripts/create-vhost-litespeed.sh
Normal 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
|
||||
76
scripts/detect-memory-litespeed.sh
Normal file
76
scripts/detect-memory-litespeed.sh
Normal 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
|
||||
128
scripts/entrypoint-litespeed.sh
Normal file
128
scripts/entrypoint-litespeed.sh
Normal 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"
|
||||
38
scripts/install-lscache-wp.sh
Normal file
38
scripts/install-lscache-wp.sh
Normal 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."
|
||||
Reference in New Issue
Block a user