From 55c28a0c119b64c5e79da4f2e3d489922095ed1e Mon Sep 17 00:00:00 2001 From: jknapp Date: Tue, 2 Jun 2026 07:32:47 -0700 Subject: [PATCH] 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) --- .gitea/workflows/build-push.yaml | 44 +++++++++ Dockerfile.litespeed | 92 ++++++++++++++++++ configs/litespeed/httpd_config.tpl | 48 ++++++++++ configs/litespeed/lsphp-overrides.ini | 43 +++++++++ configs/litespeed/site-template.tpl | 87 +++++++++++++++++ configs/litespeed/vhconf.tpl | 77 ++++++++++++++++ scripts/create-vhost-litespeed.sh | 78 ++++++++++++++++ scripts/detect-memory-litespeed.sh | 76 +++++++++++++++ scripts/entrypoint-litespeed.sh | 128 ++++++++++++++++++++++++++ scripts/install-lscache-wp.sh | 38 ++++++++ 10 files changed, 711 insertions(+) create mode 100644 Dockerfile.litespeed create mode 100644 configs/litespeed/httpd_config.tpl create mode 100644 configs/litespeed/lsphp-overrides.ini create mode 100644 configs/litespeed/site-template.tpl create mode 100644 configs/litespeed/vhconf.tpl create mode 100644 scripts/create-vhost-litespeed.sh create mode 100644 scripts/detect-memory-litespeed.sh create mode 100644 scripts/entrypoint-litespeed.sh create mode 100644 scripts/install-lscache-wp.sh diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index 297f894..e2a708c 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -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: diff --git a/Dockerfile.litespeed b/Dockerfile.litespeed new file mode 100644 index 0000000..cbf7dbc --- /dev/null +++ b/Dockerfile.litespeed @@ -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"] diff --git a/configs/litespeed/httpd_config.tpl b/configs/litespeed/httpd_config.tpl new file mode 100644 index 0000000..9e1a3d4 --- /dev/null +++ b/configs/litespeed/httpd_config.tpl @@ -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: HTTP→HTTPS 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}, * + } +} diff --git a/configs/litespeed/lsphp-overrides.ini b/configs/litespeed/lsphp-overrides.ini new file mode 100644 index 0000000..f9cd4c8 --- /dev/null +++ b/configs/litespeed/lsphp-overrides.ini @@ -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 diff --git a/configs/litespeed/site-template.tpl b/configs/litespeed/site-template.tpl new file mode 100644 index 0000000..973a3b9 --- /dev/null +++ b/configs/litespeed/site-template.tpl @@ -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 + } +} diff --git a/configs/litespeed/vhconf.tpl b/configs/litespeed/vhconf.tpl new file mode 100644 index 0000000..a7297bc --- /dev/null +++ b/configs/litespeed/vhconf.tpl @@ -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 +} diff --git a/scripts/create-vhost-litespeed.sh b/scripts/create-vhost-litespeed.sh new file mode 100644 index 0000000..e67c023 --- /dev/null +++ b/scripts/create-vhost-litespeed.sh @@ -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 diff --git a/scripts/detect-memory-litespeed.sh b/scripts/detect-memory-litespeed.sh new file mode 100644 index 0000000..3283eb9 --- /dev/null +++ b/scripts/detect-memory-litespeed.sh @@ -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 diff --git a/scripts/entrypoint-litespeed.sh b/scripts/entrypoint-litespeed.sh new file mode 100644 index 0000000..5d145f4 --- /dev/null +++ b/scripts/entrypoint-litespeed.sh @@ -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" diff --git a/scripts/install-lscache-wp.sh b/scripts/install-lscache-wp.sh new file mode 100644 index 0000000..a28df0a --- /dev/null +++ b/scripts/install-lscache-wp.sh @@ -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 }" +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."