Compare commits
89 Commits
b2b3d284a6
...
feature/ca
| Author | SHA1 | Date | |
|---|---|---|---|
| 08f35032c5 | |||
| 6bb494c72f | |||
| 7552760ba0 | |||
| fc65b68bd6 | |||
| e99b8cb2d1 | |||
| 19db8f170a | |||
| 19092911a3 | |||
| 50202538e4 | |||
| 2837d40f00 | |||
| cfdaae116a | |||
| 87f154cdc8 | |||
| f463519998 | |||
| 03cca745f7 | |||
| d1c3cfadc0 | |||
| 80fa06592b | |||
| 9e13571d61 | |||
| 55c28a0c11 | |||
|
|
1756d496e5 | ||
| d5d027c0ab | |||
| 28bb1055da | |||
| e9604b8721 | |||
| e81b0df5b8 | |||
| c65f533dcc | |||
| c6f1f42987 | |||
| e20f5620d7 | |||
| 1490bde56e | |||
| e5e055d198 | |||
| c68b555a5f | |||
| 7f7cb456f0 | |||
| dc6ce2bf12 | |||
| fc55752379 | |||
| 367da7806c | |||
| a5cb45a386 | |||
| c78167871c | |||
| 87c4f2befc | |||
| a153385d8f | |||
| 468bc7b088 | |||
| 8b9708e351 | |||
| 92ed9885ec | |||
| 844b21bd7c | |||
| 3d903b437f | |||
| 152dd413ef | |||
| 617fdbcd21 | |||
| 154f42ae09 | |||
| b5857d73c2 | |||
| b1de7021a3 | |||
| 9f8beb45b8 | |||
| 88f462eb04 | |||
| e7b0bce666 | |||
| 5a097034c4 | |||
| a41157fad0 | |||
| 4fd7ee465a | |||
| 8a7490ef98 | |||
|
|
9df776ef08 | ||
| 7bab6d39fc | |||
|
|
9630408ca0 | ||
| 49c5438866 | |||
|
|
885deb5979 | ||
| 23253e9f37 | |||
|
|
fde567d5f9 | ||
| b2675abc30 | |||
|
|
aab89a7412 | ||
| 527ba5cf58 | |||
| bbd2de6792 | |||
|
|
ed9ba0118b | ||
|
|
715b998404 | ||
|
|
7d988b338c | ||
| b3e284a547 | |||
|
|
565482764d | ||
| 3d3e353c66 | |||
|
|
0373eb4ea8 | ||
| 36757fac8f | |||
| 0c8bdc4f04 | |||
|
|
f1ab086228 | ||
| 520af5b3a8 | |||
|
|
06a7cbc88d | ||
| b1ec63617a | |||
|
|
5ead6ed456 | ||
| b38b80e6fc | |||
|
|
b53a4999bf | ||
| 49f2266974 | |||
|
|
abb1da3a0f | ||
| ac5c70d26b | |||
|
|
1d4d440a88 | ||
| 5108689aa4 | |||
|
|
3d51a63ae4 | ||
| 4ba4b7ae1e | |||
|
|
07999c4252 | ||
|
|
90841ada03 |
39
.dockerignore
Normal file
39
.dockerignore
Normal file
@@ -0,0 +1,39 @@
|
||||
# Ignore version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Ignore CI/CD and workflow files
|
||||
.gitea/
|
||||
.github/
|
||||
.gitlab/
|
||||
|
||||
# Ignore local development files
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.tmp
|
||||
*.log
|
||||
|
||||
# Ignore OS and editor files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Ignore test and documentation files
|
||||
tests/
|
||||
docs/
|
||||
README*
|
||||
|
||||
# Ignore node and Python artifacts (if present)
|
||||
node_modules/
|
||||
__pycache__/
|
||||
|
||||
# Ignore build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Ignore secrets and configs
|
||||
*.env
|
||||
.env.*
|
||||
secrets/
|
||||
220
.gitea/workflows/build-push.yaml
Normal file
220
.gitea/workflows/build-push.yaml
Normal file
@@ -0,0 +1,220 @@
|
||||
name: Cloud Apache Container
|
||||
run-name: ${{ gitea.actor }} pushed a change to trunk
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- trunk
|
||||
|
||||
jobs:
|
||||
Build-and-Push:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
phpver: [74, 80, 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 Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
PHPVER=${{ matrix.phpver }}
|
||||
tags: |
|
||||
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
|
||||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }}
|
||||
|
||||
Build-FPM-Images:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
phpver: [74, 80, 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 FPM Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./Dockerfile.fpm
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
PHPVER=${{ matrix.phpver }}
|
||||
tags: |
|
||||
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-LSPHP-Images:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
|
||||
# backend for the shared-ols tier and shares the litespeed prebuilt
|
||||
# base, which only ships lsphp for 8.1+. Keep this matrix in lockstep
|
||||
# with Build-LiteSpeed-Images.
|
||||
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 lsphp Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./Dockerfile.lsphp
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
PHPVER=${{ matrix.phpver }}
|
||||
OLS_VERSION=1.8.4
|
||||
# OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same
|
||||
# prebuilt base, same lsphp binaries. Bump both together.
|
||||
tags: |
|
||||
repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${{ matrix.phpver }}
|
||||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:latest' || '' }}
|
||||
|
||||
Build-Shared-httpd:
|
||||
runs-on: ubuntu-latest
|
||||
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 Shared httpd Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./Dockerfile.shared-httpd
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: |
|
||||
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
|
||||
|
||||
Build-Shared-OLS:
|
||||
runs-on: ubuntu-latest
|
||||
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 Shared OLS Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./Dockerfile.shared-ols
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
# Single image (runs no PHP). PHPVER just selects the OLS base tag;
|
||||
# pinned to 83 / OLS 1.8.4 to match the rest of the litespeed family.
|
||||
build-args: |
|
||||
PHPVER=83
|
||||
OLS_VERSION=1.8.4
|
||||
tags: |
|
||||
repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest
|
||||
86
CLAUDE.md
Normal file
86
CLAUDE.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Cloud Apache Container (CAC) is a Docker-based PHP web hosting environment that supports multiple PHP versions (7.4 through 8.4) with Apache, designed for both local development and production deployment.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Local Development Setup
|
||||
```bash
|
||||
# Quick start with automated setup (creates helper scripts)
|
||||
./local-dev.sh -n local-dev
|
||||
|
||||
# With specific PHP version
|
||||
./local-dev.sh -n myproject -a 84 # PHP 8.4
|
||||
|
||||
# Helper scripts created by local-dev.sh:
|
||||
./instance_start # Start container
|
||||
./instance_stop # Stop container
|
||||
./instance_logs # Tail Apache logs
|
||||
./instance_db_info # Show MySQL credentials
|
||||
```
|
||||
|
||||
### Building and Testing
|
||||
```bash
|
||||
# Build container locally
|
||||
docker build -t cac:latest .
|
||||
|
||||
# Build with specific PHP version
|
||||
docker build --build-arg PHP_VER=83 -t cac:php83 .
|
||||
|
||||
# Run container manually
|
||||
docker run -d -p 80:80 -p 443:443 \
|
||||
-e PHPVER=83 -e environment=DEV \
|
||||
-e uid=$(id -u) -e user=$(whoami) -e domain=localhost \
|
||||
-v"$(pwd)/user":/home/$(whoami) \
|
||||
--name test-container cac:latest
|
||||
```
|
||||
|
||||
### Server Deployment
|
||||
- Production git directory: `/root/whp`
|
||||
- After `git pull`, sync web files: `rsync -av web-files/ /docker/whp/web/`
|
||||
|
||||
## Architecture and Key Components
|
||||
|
||||
### Directory Structure
|
||||
- `/scripts/` - Container setup scripts (entrypoint, PHP installers, vhost creation)
|
||||
- `/config/` - Apache and PHP configuration files
|
||||
- `/web-files/` - Default web content (ping endpoint)
|
||||
- `/.gitea/workflows/` - CI/CD pipeline for multi-PHP version builds
|
||||
|
||||
### Container Behavior
|
||||
1. **Entrypoint Flow** (`scripts/entrypoint.sh`):
|
||||
- Creates user with specified UID
|
||||
- Sets up directory structure
|
||||
- Configures Apache vhost based on environment variables
|
||||
- In DEV mode: starts MariaDB and Memcached
|
||||
- Starts Apache and PHP-FPM
|
||||
|
||||
2. **Environment Modes**:
|
||||
- **DEV** (`environment=DEV`): Local database, memcached, automatic backups
|
||||
- **PROD** (default): Expects external database/cache services
|
||||
|
||||
3. **PHP Version Management**:
|
||||
- Controlled via `PHPVER` environment variable (74, 80, 81, 82, 83, 84)
|
||||
- Each version has dedicated install script in `/scripts/`
|
||||
- PHP-FPM configuration dynamically created based on version
|
||||
|
||||
### Key Environment Variables
|
||||
- `uid` (required): User ID for file permissions
|
||||
- `user` (required): Username for container user
|
||||
- `domain` (required): Primary domain for Apache vhost
|
||||
- `serveralias`: Additional domains (comma-separated)
|
||||
- `PHPVER`: PHP version to use (default: 83)
|
||||
- `environment`: DEV or PROD mode
|
||||
|
||||
## Important Technical Details
|
||||
|
||||
1. **Health Check**: Available at `/ping` endpoint
|
||||
2. **Logs Location**: `/home/$user/logs/apache/` and `/home/$user/logs/php-fpm/`
|
||||
3. **Database Backups** (DEV mode): Every 15 minutes to `/home/$user/_db_backups/`
|
||||
4. **Log Rotation**: Compress after 3 days, delete after 7 days
|
||||
5. **SSL**: Self-signed certificate auto-generated, proper SSL configured
|
||||
6. **WordPress**: WP-CLI pre-installed for WordPress development
|
||||
59
Dockerfile
59
Dockerfile
@@ -1,23 +1,50 @@
|
||||
FROM almalinux/9-base
|
||||
ARG PHPVER=81
|
||||
#RUN dnf update -y && dnf upgrade -y
|
||||
RUN dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm -y
|
||||
RUN dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
|
||||
#RUN dnf update -y && dnf upgrade -y
|
||||
RUN dnf install -y httpd mod_ssl wget procps
|
||||
RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt
|
||||
RUN mkdir /run/php-fpm/
|
||||
RUN mkdir /scripts
|
||||
COPY ./scripts/* /scripts/
|
||||
FROM almalinux/10-base
|
||||
ARG PHPVER=83
|
||||
|
||||
# Install repos, update, install only needed packages, clean up in one layer
|
||||
RUN dnf install -y \
|
||||
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
|
||||
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
|
||||
dnf update -y && \
|
||||
dnf install -y httpd mod_ssl openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
||||
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix && \
|
||||
dnf clean all && \
|
||||
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
|
||||
|
||||
# Copy scripts into the image and set permissions
|
||||
COPY ./scripts/ /scripts/
|
||||
RUN chmod +x /scripts/*
|
||||
#RUN /scripts/install-php$PHPVER.sh
|
||||
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
|
||||
RUN chmod +x wp-cli.phar
|
||||
RUN mv wp-cli.phar /usr/local/bin/wp
|
||||
|
||||
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
|
||||
RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
|
||||
dnf clean all
|
||||
|
||||
# Generate self-signed cert, create needed dirs, install PHP, clean up
|
||||
RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt && \
|
||||
mkdir -p /run/php-fpm/ && \
|
||||
/scripts/install-php$PHPVER.sh && \
|
||||
rm -rf /tmp/*
|
||||
|
||||
# Download and install wp-cli (consider pinning version for reproducibility)
|
||||
RUN curl -L -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
|
||||
chmod +x /usr/local/bin/wp
|
||||
|
||||
# Download and install Composer
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \
|
||||
chmod +x /usr/local/bin/composer
|
||||
|
||||
# Copy configs and web files
|
||||
COPY ./configs/default-index.conf /etc/httpd/conf.d/
|
||||
COPY ./configs/prod-php.ini /etc/php.ini
|
||||
COPY ./configs/phpinfo.php /var/www/html/
|
||||
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
||||
COPY ./configs/index.php /var/www/html/
|
||||
RUN yum clean all
|
||||
COPY ./configs/remote_ip.conf /etc/httpd/conf.d/
|
||||
|
||||
# Set up cron job in a single layer
|
||||
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost/ || exit 1
|
||||
|
||||
ENTRYPOINT [ "/scripts/entrypoint.sh" ]
|
||||
|
||||
47
Dockerfile.fpm
Normal file
47
Dockerfile.fpm
Normal file
@@ -0,0 +1,47 @@
|
||||
FROM almalinux/10-base
|
||||
ARG PHPVER=83
|
||||
|
||||
# Install repos, update, install only needed packages (no httpd/mod_ssl), clean up in one layer
|
||||
RUN dnf install -y \
|
||||
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
|
||||
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
|
||||
dnf update -y && \
|
||||
dnf install -y openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
||||
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix fcgi && \
|
||||
dnf clean all && \
|
||||
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
|
||||
|
||||
# Copy scripts into the image and set permissions
|
||||
COPY ./scripts/ /scripts/
|
||||
RUN chmod +x /scripts/*
|
||||
|
||||
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
|
||||
RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
|
||||
dnf clean all
|
||||
|
||||
# Create needed dirs, install PHP, clean up (no SSL cert, no httpd)
|
||||
RUN mkdir -p /run/php-fpm/ && \
|
||||
/scripts/install-php$PHPVER.sh && \
|
||||
rm -rf /tmp/*
|
||||
|
||||
# Download and install wp-cli
|
||||
RUN curl -L -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
|
||||
chmod +x /usr/local/bin/wp
|
||||
|
||||
# Download and install Composer
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \
|
||||
chmod +x /usr/local/bin/composer
|
||||
|
||||
# Copy configs (PHP only, no Apache configs)
|
||||
COPY ./configs/prod-php.ini /etc/php.ini
|
||||
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
||||
|
||||
# Set up cron job for log rotation
|
||||
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
|
||||
CMD SCRIPT_FILENAME=/fpm-ping SCRIPT_NAME=/fpm-ping REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 | grep -q pong || exit 1
|
||||
|
||||
ENTRYPOINT [ "/scripts/entrypoint-fpm.sh" ]
|
||||
97
Dockerfile.litespeed
Normal file
97
Dockerfile.litespeed
Normal file
@@ -0,0 +1,97 @@
|
||||
## 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)
|
||||
## - lsphp83-ldap: not in base image, useful for some WP plugins
|
||||
##
|
||||
## NOTE: mariadb-server + memcached were previously installed here for
|
||||
## DEV-mode parity but bloated the PROD image by ~500MB. They are now
|
||||
## installed at runtime by entrypoint-litespeed.sh ONLY when
|
||||
## environment=DEV, mirroring the cac:phpNN pattern. The mysql CLI
|
||||
## client (used by the DEV creds-bootstrap and by wp-cli) is already
|
||||
## present in the litespeedtech/openlitespeed base via the mysql-client
|
||||
## package, so no client-side install is needed at build time.
|
||||
##
|
||||
## 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 \
|
||||
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"]
|
||||
63
Dockerfile.lsphp
Normal file
63
Dockerfile.lsphp
Normal file
@@ -0,0 +1,63 @@
|
||||
## cac-lsphp — per-site DETACHED lsphp (LSAPI) backend for the shared-ols tier.
|
||||
##
|
||||
## The LiteSpeed analogue of cac-fpm: a slim, single-tenant PHP backend that
|
||||
## runs `lsphp -b 0.0.0.0:9000` (detached LSAPI mode) and NOTHING ELSE — no
|
||||
## webserver. The shared OpenLiteSpeed container (shared-ols) sits in front and
|
||||
## reaches this over the docker network via an extProcessor of type lsapi,
|
||||
## address <this-container>:9000 — structurally identical to how shared-httpd
|
||||
## reaches a cac-fpm container's php-fpm on :9000.
|
||||
##
|
||||
## Built on the SAME LiteSpeed prebuilt base as cac-litespeed so the lsphp
|
||||
## binary + extension set are byte-for-byte the runtime customers already get
|
||||
## on the litespeed tier (memcached, redis, imagick, mbstring, mysqlnd, intl,
|
||||
## gd, soap, bcmath, gmp, sodium, opcache, ... + lsphpNN-ldap added below).
|
||||
## We do NOT strip the bundled OpenLiteSpeed binaries: the "no webserver"
|
||||
## guarantee comes from the ENTRYPOINT (it only ever execs lsphp), and deleting
|
||||
## OLS files from the upstream image risks breaking lsphp's shared libs for no
|
||||
## real benefit. Only :9000 is EXPOSEd, and OLS is never started.
|
||||
##
|
||||
## See the design spec + PoC: whp docs/superpowers/plans/2026-06-09-ols-lsphp-tier.md
|
||||
## and the LSAPI path-parity finding (feedback_ols_lsapi_no_script_filename_remap).
|
||||
|
||||
ARG OLS_VERSION=1.8.4
|
||||
ARG PHPVER=83
|
||||
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
|
||||
ARG PHPVER=83
|
||||
ENV PHPVER=${PHPVER}
|
||||
|
||||
## Match the cac-litespeed extension surface exactly: the only ext the prebuilt
|
||||
## base lacks is lsphpNN-ldap. setpriv (util-linux) is already on the Ubuntu
|
||||
## base; we add nothing else the sidecar doesn't need. All apt cache cleaned in
|
||||
## the same layer to keep the image small.
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
lsphp${PHPVER}-ldap && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
||||
|
||||
## Scripts + the SHARED production lsphp ini (reused verbatim from the litespeed
|
||||
## image — same runtime, same tuning). Scripts layer last (they change most).
|
||||
COPY ./scripts/entrypoint-lsphp.sh \
|
||||
./scripts/detect-memory-lsphp.sh \
|
||||
./scripts/healthcheck-lsphp.sh \
|
||||
./scripts/cac-lsphp-normalize.php \
|
||||
/scripts/
|
||||
RUN chmod +x /scripts/entrypoint-lsphp.sh /scripts/detect-memory-lsphp.sh /scripts/healthcheck-lsphp.sh
|
||||
|
||||
## Apply production lsphp ini overrides into lsphp's scan dir (path varies by
|
||||
## PHP minor version; ask lsphp directly — same idiom as Dockerfile.litespeed).
|
||||
COPY ./configs/litespeed/lsphp-overrides.ini /etc/lsws-templates/lsphp-overrides.ini
|
||||
RUN bash -c 'set -e; \
|
||||
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
||||
mkdir -p "$SCAN_DIR"; \
|
||||
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
||||
echo "wrote overrides to $SCAN_DIR"'
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
## TCP-connect + lsphp-alive check (LSAPI isn't FastCGI, so no cgi-fcgi ping).
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD /scripts/healthcheck-lsphp.sh
|
||||
|
||||
ENTRYPOINT ["/scripts/entrypoint-lsphp.sh"]
|
||||
40
Dockerfile.shared-httpd
Normal file
40
Dockerfile.shared-httpd
Normal file
@@ -0,0 +1,40 @@
|
||||
FROM almalinux/10-base
|
||||
|
||||
# Install Apache and minimal dependencies (no PHP at all)
|
||||
RUN dnf install -y \
|
||||
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm && \
|
||||
dnf update -y && \
|
||||
dnf install -y httpd mod_ssl openssl iproute cronie procps curl && \
|
||||
dnf clean all && \
|
||||
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
|
||||
|
||||
# Copy scripts and set permissions
|
||||
COPY ./scripts/detect-memory.sh /scripts/detect-memory.sh
|
||||
COPY ./scripts/create-apache-mpm-config.sh /scripts/create-apache-mpm-config.sh
|
||||
COPY ./scripts/log-rotate.sh /scripts/log-rotate.sh
|
||||
COPY ./scripts/entrypoint-shared-httpd.sh /scripts/entrypoint-shared-httpd.sh
|
||||
COPY ./scripts/tune-mpm.sh /scripts/tune-mpm.sh
|
||||
RUN chmod +x /scripts/*
|
||||
|
||||
# Generate self-signed SSL cert (same as main CAC image)
|
||||
RUN openssl req -newkey rsa:2048 -nodes \
|
||||
-keyout /etc/pki/tls/private/localhost.key \
|
||||
-x509 -days 3650 -subj "/CN=localhost" \
|
||||
-out /etc/pki/tls/certs/localhost.crt
|
||||
|
||||
# Copy Apache configs
|
||||
COPY ./configs/remote_ip.conf /etc/httpd/conf.d/
|
||||
COPY ./configs/default-index.conf /etc/httpd/conf.d/
|
||||
|
||||
# Create vhosts directory (will be volume-mounted from host)
|
||||
RUN mkdir -p /etc/httpd/conf.d/vhosts
|
||||
|
||||
# Set up cron job for log rotation
|
||||
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
|
||||
CMD curl -sfk https://localhost/ping || exit 1
|
||||
|
||||
ENTRYPOINT [ "/scripts/entrypoint-shared-httpd.sh" ]
|
||||
57
Dockerfile.shared-ols
Normal file
57
Dockerfile.shared-ols
Normal file
@@ -0,0 +1,57 @@
|
||||
## shared-ols — the shared OpenLiteSpeed webserver tier.
|
||||
##
|
||||
## One OLS container fronting MANY tenants' detached cac-lsphp sidecars — the
|
||||
## OLS analogue of the shared-httpd container. Runs NO PHP locally: every site's
|
||||
## PHP goes to its own cac-lsphp:phpNN sidecar over LSAPI (extProcessor type
|
||||
## lsapi, address <sidecar>:9000). HAProxy stays the TLS/WAF/SNI edge and routes
|
||||
## OLS-type hostnames here on :443.
|
||||
##
|
||||
## Built on the SAME litespeedtech prebuilt base as cac-litespeed / cac-lsphp so
|
||||
## the OLS build + plumbing (lscgid, cgid socket — see feedback_ols_packaging_landmines)
|
||||
## are the proven ones. The base is lsphp-tagged but we never run that lsphp;
|
||||
## the tag just selects the OLS build. Pinned to lsphp83 / OLS 1.8.4.
|
||||
##
|
||||
## Config model (established by PoC 2026-06-10): OLS has NO top-level `include`,
|
||||
## so render-shared-ols-config.sh assembles httpd_config.conf from the panel's
|
||||
## per-site files at boot + on every change. See that script + the plan.
|
||||
|
||||
ARG OLS_VERSION=1.8.4
|
||||
ARG PHPVER=83
|
||||
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
|
||||
|
||||
## Tooling the shared tier needs on top of the base:
|
||||
## - inotify-tools: the .htaccess watcher (spec 5.3)
|
||||
## - gettext-base: envsubst for render-shared-ols-config.sh
|
||||
## - openssl: self-signed cert for the :443 listener (HAProxy verifies none)
|
||||
## - curl/ca-certificates: HEALTHCHECK
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
inotify-tools gettext-base openssl ca-certificates curl && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
||||
|
||||
## Snapshot the stock httpd_config.conf so render-shared-ols-config.sh always has
|
||||
## a pristine base to strip-and-rebuild from (the base image keeps it at conf/).
|
||||
RUN mkdir -p /usr/local/lsws/.conf && \
|
||||
cp /usr/local/lsws/conf/httpd_config.conf /usr/local/lsws/.conf/httpd_config.conf
|
||||
|
||||
COPY ./scripts/entrypoint-shared-ols.sh \
|
||||
./scripts/render-shared-ols-config.sh \
|
||||
./scripts/ols-htaccess-watcher.sh \
|
||||
/scripts/
|
||||
RUN chmod +x /scripts/entrypoint-shared-ols.sh /scripts/render-shared-ols-config.sh /scripts/ols-htaccess-watcher.sh
|
||||
COPY ./configs/shared-ols/ /etc/shared-ols-templates/
|
||||
|
||||
## Admin console unreachable from tenant/edge networks (spec 5.2): bind the
|
||||
## WebAdmin listener to loopback. Same sed as Dockerfile.litespeed.
|
||||
RUN sed -i 's|^[[:space:]]*address[[:space:]]\+\*:| address 127.0.0.1:|' \
|
||||
/usr/local/lsws/admin/conf/admin_config.conf 2>/dev/null || true
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
## Health: the entrypoint renders a catch-all _health vhost serving /healthz, so
|
||||
## this passes from boot (zero customer sites) onward. Self-signed :443.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD curl -fsSk https://127.0.0.1/healthz || exit 1
|
||||
|
||||
ENTRYPOINT ["/scripts/entrypoint-shared-ols.sh"]
|
||||
151
README.md
151
README.md
@@ -1,71 +1,134 @@
|
||||
# Cloud Apache Container #
|
||||
This is the base container for running PHP based applications. Select the PHP version environment variables. PHP Version Defaults to PHP 8.1
|
||||
# Cloud Apache Container
|
||||
|
||||
*__You mush have docker or compatable containerization software running.__*
|
||||
This is a base container for running PHP-based applications, supporting multiple PHP versions (7.4, 8.0, 8.1, 8.2, 8.3, 8.4). The default is PHP 8.3. The container is based on AlmaLinux 9 and uses Apache with mod_ssl. It is designed for both development and production use.
|
||||
|
||||
__You can pull this image locally by running:__
|
||||
**You must have Docker or compatible containerization software running.**
|
||||
|
||||
```console
|
||||
docker pull public.ecr.aws/s1f6k4w4/cac:latest
|
||||
---
|
||||
|
||||
## What's New?
|
||||
|
||||
- **Optimized Image:** The Dockerfile has been refactored for smaller size, faster builds, and improved security. Unnecessary files and caches are removed during build.
|
||||
- **Pre-built Images for Each PHP Version:** On every push, images for all supported PHP versions are built and pushed to the registry. You can pull the exact version you need (e.g., `cac:php74`, `cac:php84`, or `cac:latest`).
|
||||
- **.dockerignore Added:** The build context is now minimized, making builds faster and more secure.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start: Local Development with `local-dev.sh`
|
||||
|
||||
The easiest way to start a local development environment is with the provided `local-dev.sh` script. This script automates container setup, volume creation, log directories, and WordPress installation.
|
||||
|
||||
### Usage Example
|
||||
|
||||
```bash
|
||||
./local-dev.sh -n local-dev
|
||||
```
|
||||
|
||||
__You can then run a development version of the server by running the following commands:__
|
||||
*Note this is an example, you can modify the command(s) to fit your needs.*
|
||||
**Flags:**
|
||||
- `-n` Name of the container (required)
|
||||
- `-p` HTTP port (default: 80)
|
||||
- `-s` HTTPS port (default: 443)
|
||||
- `-r` Root path for files and database (default: current directory)
|
||||
- `-a` PHP version (default: 8.3; options: 74, 80, 81, 82, 83, 84)
|
||||
- `-v` Enable verbose mode
|
||||
- `-h` Show help
|
||||
|
||||
```console
|
||||
The script will:
|
||||
- Create a user directory and log folders
|
||||
- Create a Docker volume for MySQL
|
||||
- Start the container with the correct environment variables
|
||||
- Generate helper scripts in your root path:
|
||||
- `instance_start` – Start the container
|
||||
- `instance_stop` – Stop the container
|
||||
- `instance_logs` – Tail Apache logs
|
||||
- `instance_db_info` – Show MySQL credentials
|
||||
- Install WordPress in your web root
|
||||
- Print MySQL credentials
|
||||
|
||||
---
|
||||
|
||||
## Manual Docker Usage
|
||||
|
||||
You can also run the container manually:
|
||||
|
||||
```bash
|
||||
mkdir -p local-development/domain.tld
|
||||
cd local-development/domain.tld
|
||||
mkdir {web,db}
|
||||
docker run -it -p 80:80 -p 443:443 -e PHPVER=81 -e environment=DEV --mount type=bind,source="$(pwd)"/web,target=/home/myuser/public_html --mount type=bind,source="$(pwd)"/db,target=/var/lib/mysql -e uid=30001 -e user=myuser -e domain=domain.tld -e serveralias=www.domain.tld --name local-dev public.ecr.aws/s1f6k4w4/cac:latest
|
||||
mkdir user
|
||||
mkdir -p user/logs/{apache,system}
|
||||
docker run -d -it -p 80:80 -p 443:443 -e PHPVER=84 -e environment=DEV --mount type=bind,source="$(pwd)"/user,target=/home/myuser -v"$name-mysql":/var/lib/mysql -e uid=30001 -e user=myuser -e domain=localhost --name local-dev repo.anhonesthost.net/cloud-hosting-platform/cac:latest
|
||||
```
|
||||
|
||||
*This will start the processes needed to run sites locally.*
|
||||
---
|
||||
|
||||
The first time you start the container, it will take some time as it is installing all the required software to run the dev instance.
|
||||
## Accessing the Container
|
||||
|
||||
__If you need to get into the container you can run:__
|
||||
|
||||
```console
|
||||
```bash
|
||||
docker exec -it local-dev /bin/bash
|
||||
```
|
||||
|
||||
__To install WordPress for your site__
|
||||
---
|
||||
|
||||
```console
|
||||
cat /var/lib/mysql/creds
|
||||
## WordPress Installation
|
||||
|
||||
If using `local-dev.sh`, WordPress is installed automatically. For manual setup:
|
||||
|
||||
```bash
|
||||
cat /home/myuser/mysql_creds
|
||||
su - myuser
|
||||
cd ~/public_html
|
||||
wp core download
|
||||
```
|
||||
|
||||
You should be able to then go into your browser and go to https://localhost (accept the SSL warning if it appears) and follow the prompts to setup the site.
|
||||
Then visit https://localhost (accept the SSL warning) to complete setup.
|
||||
|
||||
The database credentials are shown in the /var/lib/mysql/creds file, which we had *cat* in the commands above.
|
||||
---
|
||||
|
||||
### PHPVER ###
|
||||
*74* - PHP 7.4
|
||||
*80* - PHP 8.0
|
||||
*81* - PHP 8.1
|
||||
*82* - PHP 8.2
|
||||
## Features
|
||||
|
||||
### Environment Variables ###
|
||||
__Required Tags__
|
||||
*uid* - User ID for File Permissions
|
||||
*user* - Username for File Permissions
|
||||
*domain* - Primary Domain for configuration
|
||||
- **Multiple PHP Versions:** 7.4, 8.0, 8.1, 8.2, 8.3, 8.4 (set with `PHPVER` or `-a` flag)
|
||||
- **Pre-built Images:** Pull the image for your desired PHP version directly from the registry. No need to build locally unless customizing.
|
||||
- **Optimized Build:** Smaller, faster, and more secure images thanks to the improved Dockerfile and `.dockerignore`.
|
||||
- **Automatic Database Setup:** MariaDB is started in DEV mode, credentials are auto-generated and stored in `/home/$user/mysql_creds`.
|
||||
- **Database Backups:** Cron job backs up the database every 15 minutes to `/home/$user/_db_backups`.
|
||||
- **Log Management:** Log rotation compresses logs older than 3 days and deletes those older than 7 days.
|
||||
- **Memcached:** Started automatically in DEV mode.
|
||||
- **SSL:** Self-signed certificate enabled by default.
|
||||
- **Default Web Content:** `/home/$user/public_html` is the web root. `/ping` endpoint and `phpinfo.php` are available for diagnostics.
|
||||
- **Helper Scripts:** `instance_start`, `instance_stop`, `instance_logs`, `instance_db_info` (created by `local-dev.sh`).
|
||||
|
||||
__Optional Tags__
|
||||
*environment* - Set to DEV to start memcached and mysql locally for development purposes
|
||||
*serveralias* - Set to allow alternative hostnames for a site.
|
||||
*PHPVER* - Set to use a different version of PHP [refer to versions here.](#phpver)
|
||||
---
|
||||
|
||||
### Helpful Notes ###
|
||||
## Environment Variables
|
||||
|
||||
* On your first creation of a dev instance, you will be dumped to the logs output. Hit ```ctrl + c``` to exit the running process.
|
||||
* If you want to restart the instance again, run ```docker start {name-of-your-container}``` in the example, *name-of-your-cintainer* is *local-dev*
|
||||
* To stop a restarted instance, run ```docker stop {name-of-your-container}```
|
||||
* To view log stream from container, run ```docker logs -f {name-of-your-container}```
|
||||
* To delete a container, run ```docker rm {name-of-your-container}``` *__Note:__ this does not delete the files in public_html or database, as those are store in your system*
|
||||
* To view running containers, run ```docker ps```
|
||||
* To view all created containers, run ```docker ps --all``
|
||||
* To view all container images downloaded on your system, run ```docker images```
|
||||
**Required:**
|
||||
- `uid` – User ID for file permissions
|
||||
- `user` – Username for file permissions
|
||||
- `domain` – Primary domain for configuration
|
||||
|
||||
**Optional:**
|
||||
- `environment` – Set to `DEV` to start memcached and MySQL locally for development
|
||||
- `serveralias` – Comma-separated list of alternative hostnames
|
||||
- `PHPVER` – PHP version (see above)
|
||||
|
||||
---
|
||||
|
||||
## Helpful Notes
|
||||
|
||||
- To restart the instance: `./instance_start` or `docker start {container-name}`
|
||||
- To stop: `./instance_stop` or `docker stop {container-name}`
|
||||
- To view logs: `./instance_logs` or `docker logs -f {container-name}`
|
||||
- To get DB credentials: `./instance_db_info` or `cat /home/$user/mysql_creds`
|
||||
- To delete a container: `docker rm {container-name}` (does not delete user files or DB volume)
|
||||
- To view running containers: `docker ps`
|
||||
- To view all containers: `docker ps --all`
|
||||
- To view images: `docker images`
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
- The first run may take several minutes as dependencies are installed.
|
||||
- If you need to change PHP version, stop and remove the container, then recreate with the desired version.
|
||||
- For advanced configuration, see the scripts in the `scripts/` directory.
|
||||
- The image is optimized for size and speed, but local development in DEV mode may install additional packages (MariaDB, memcached) at runtime using microdnf.
|
||||
- The build context is minimized by the included `.dockerignore` file.
|
||||
@@ -1,13 +1,2 @@
|
||||
DirectoryIndex index.html index.htm index.php
|
||||
Alias "/ping" "/var/www/html"
|
||||
|
||||
<IfModule mpm_event_module>
|
||||
StartServers 10
|
||||
MinSpareThreads 25
|
||||
MaxSpareThreads 75
|
||||
ThreadLimit 64
|
||||
ThreadsPerChild 25
|
||||
MaxRequestWorkers 800
|
||||
ServerLimit 32
|
||||
MaxConnectionsPerChild 1500
|
||||
</IfModule>
|
||||
Alias "/ping" "/var/www/html"
|
||||
111
configs/litespeed/httpd_config.tpl
Normal file
111
configs/litespeed/httpd_config.tpl
Normal file
@@ -0,0 +1,111 @@
|
||||
## 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 $PHPVER
|
||||
## $LSAPI_CHILDREN (computed by detect-memory-litespeed.sh)
|
||||
|
||||
## --- real client IP behind HAProxy ---
|
||||
## OLS equivalent of the Apache cac:phpNN mod_remoteip wiring
|
||||
## (configs/remote_ip.conf + RemoteIPInternalProxy in entrypoint.sh). Without
|
||||
## this, OLS records HAProxy's docker-bridge IP as the peer: every site's
|
||||
## access_log and lsphp $_SERVER['REMOTE_ADDR'] collapse to one internal IP,
|
||||
## silently breaking traffic analytics, WP security plugins, brute-force
|
||||
## detection, Coraza source-IP correlation, geo, and rate-limiting.
|
||||
## 1 = trust X-Forwarded-For (the container is only reachable via HAProxy;
|
||||
## it is never bound to a public address). Mirrors the Apache side, which
|
||||
## trusts the whole docker subnet via RemoteIPInternalProxy $docker_network.
|
||||
## When enabled, OLS rewrites the remote IP for BOTH logging and the LSAPI
|
||||
## REMOTE_ADDR before PHP sees it — so the default access_log format already
|
||||
## records the real visitor; no LogFormat change needed.
|
||||
useIpInProxyHeader 1
|
||||
|
||||
## --- 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 *
|
||||
}
|
||||
|
||||
## --- lsphp extProcessor (overrides the stock one which is hard-coded to
|
||||
## PHP_LSAPI_CHILDREN=10 regardless of container memory).
|
||||
##
|
||||
## Sized dynamically by detect-memory-litespeed.sh based on the cgroup cap:
|
||||
## 2 GiB container → LSAPI_CHILDREN ≈ 17 (was stuck at 10)
|
||||
## 1 GiB container → LSAPI_CHILDREN ≈ 8
|
||||
## 512 MiB → LSAPI_CHILDREN ≈ 3
|
||||
##
|
||||
## Idle-reduction knobs (the question that motivated this whole block):
|
||||
## LSAPI_MAX_IDLE_CHILDREN=2 default was CHILDREN/2 (so 10/2=5)
|
||||
## LSAPI_MAX_IDLE=60 default was 300 (5 min)
|
||||
## Together: max 2 idle workers kept alive, anything idle >60s gets reaped.
|
||||
## Trade-off: cold-start of an extra worker after idle reaping costs ~50-100ms
|
||||
## on the first request to it. Worth it for shadowdao-sized low-traffic sites
|
||||
## where the difference is "30 MB idle" vs "200 MB idle".
|
||||
##
|
||||
## memSoftLimit/memHardLimit: per-worker RLIMIT_AS catches a runaway PHP
|
||||
## script before it hogs the whole pool's memory. Cgroup is still the host
|
||||
## backstop (one-customer-per-container), but the per-worker cap protects
|
||||
## the OTHER workers in the same pool from a bad-actor script. 1024M soft
|
||||
## comfortably accommodates typical Divi/WooCommerce VSZ (~280-365 MB).
|
||||
extProcessor lsphp {
|
||||
type lsapi
|
||||
address uds://tmp/lshttpd/lsphp.sock
|
||||
maxConns ${LSAPI_CHILDREN}
|
||||
env PHP_LSAPI_CHILDREN=${LSAPI_CHILDREN}
|
||||
env LSAPI_MAX_IDLE_CHILDREN=2
|
||||
env LSAPI_MAX_IDLE=60
|
||||
env PHP_LSAPI_MAX_REQUESTS=500
|
||||
env LSAPI_AVOID_FORK=200M
|
||||
initTimeout 60
|
||||
retryTimeout 0
|
||||
persistConn 1
|
||||
pcKeepAliveTimeout 30
|
||||
respBuffer 0
|
||||
autoStart 1
|
||||
path /usr/local/lsws/lsphp${PHPVER}/bin/lsphp
|
||||
backlog 100
|
||||
instances 1
|
||||
runOnStartUp 1
|
||||
priority 0
|
||||
memSoftLimit 1024M
|
||||
memHardLimit 1500M
|
||||
procSoftLimit 400
|
||||
procHardLimit 500
|
||||
}
|
||||
|
||||
## --- 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}, *
|
||||
}
|
||||
}
|
||||
60
configs/litespeed/lsphp-overrides.ini
Normal file
60
configs/litespeed/lsphp-overrides.ini
Normal file
@@ -0,0 +1,60 @@
|
||||
; 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
|
||||
; Sized to fit Divi + WooCommerce + WP core comfortably without eviction
|
||||
; thrash. Per-instance because shmem is per-process-RSS on Linux cgroups
|
||||
; (vs PHP-FPM's COW-shared model — one lsphp PARENT per httpd worker in
|
||||
; OLS, each with its own opcache segment).
|
||||
;
|
||||
; Sizing history:
|
||||
; 128 MB / 10000 files (original): blew 800+ MiB shmem under setUIDMode 2
|
||||
; because that gave 8+ lsphp instances each at 128 MB → 1+ GiB shmem.
|
||||
; 32 MB / 4000 files (2026-06-02): solved the shmem problem but caused
|
||||
; opcache eviction thrash on Divi/WC sites (3000+4000 unique PHP files
|
||||
; each); manifested as ~40% sustained CPU on alphaoneaminos and
|
||||
; elevated OOM cycling on brain-jar (5378 oom_kills/9h on 2026-06-03).
|
||||
; 64 MB / 8000 files (current): fits Divi+WC bytecode without eviction;
|
||||
; N lsphp × 64 MB ≈ 512 MiB shmem worst case, still acceptable.
|
||||
;
|
||||
; Override per-site via OPCACHE_MEMORY_MB / OPCACHE_MAX_FILES env vars
|
||||
; (panel: Advanced Tuning → OpCache size) for outliers.
|
||||
opcache.memory_consumption = 64
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.max_accelerated_files = 8000
|
||||
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
|
||||
97
configs/litespeed/site-template.tpl
Normal file
97
configs/litespeed/site-template.tpl
Normal file
@@ -0,0 +1,97 @@
|
||||
## 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
|
||||
## No setUIDMode — OLS itself runs as ${user} (set at server level by
|
||||
## create-vhost-litespeed.sh), so lsphp inherits that uid without needing
|
||||
## suEXEC per request. This is the key to single-lsphp-instance topology:
|
||||
## with setUIDMode 2, each httpd worker had to lscgid-spawn its own lsphp
|
||||
## (= N opcache shmem segments). Without it, ONE persistent lsphp parent
|
||||
## serves all httpd workers via the shared socket, and LSAPI children-mode
|
||||
## actually works (1 parent + N children = 1 shmem segment).
|
||||
##
|
||||
## Safe because cac-litespeed is one-customer-per-container — the container
|
||||
## boundary IS the privsep boundary.
|
||||
vhRoot /home/${user}/public_html/
|
||||
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhconf.conf
|
||||
|
||||
virtualHostConfig {
|
||||
docRoot $VH_ROOT
|
||||
|
||||
## Drop-in log paths matching cac:phpNN (Apache+FPM bundled) so existing
|
||||
## WHP log-gathering code (whp-traffic-aggregator.php, process-log-review.php,
|
||||
## customer-facing log views) keeps working unchanged for migrated sites.
|
||||
## Customer's "Apache access log" is just OLS's access log under the same
|
||||
## filename. No `.log` suffix — matches the bundled cac convention.
|
||||
errorlog /home/${user}/logs/apache/error_log {
|
||||
useServer 0
|
||||
logLevel WARN
|
||||
rollingSize 10M
|
||||
keepDays 14
|
||||
compressArchive 1
|
||||
}
|
||||
|
||||
accesslog /home/${user}/logs/apache/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
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
# MariaDB 10.11 CentOS repository list - created 2023-04-03 23:52 UTC
|
||||
# MariaDB 11.4.5 CentOS repository list - created 2023-04-03 23:52 UTC
|
||||
# https://mariadb.org/download/
|
||||
[mariadb]
|
||||
name = MariaDB
|
||||
# rpm.mariadb.org is a dynamic mirror if your preferred mirror goes offline. See https://mariadb.org/mirrorbits/ for details.
|
||||
# baseurl = https://rpm.mariadb.org/10.11/centos/$releasever/$basearch
|
||||
baseurl = https://mirrors.xtom.com/mariadb/yum/10.11/centos/$releasever/$basearch
|
||||
baseurl = https://mirror.mariadb.org/yum/11.4/rhel$releasever-amd64
|
||||
module_hotfixes = 1
|
||||
# gpgkey = https://rpm.mariadb.org/RPM-GPG-KEY-MariaDB
|
||||
gpgkey = https://mirrors.xtom.com/mariadb/yum/RPM-GPG-KEY-MariaDB
|
||||
gpgcheck = 1
|
||||
gpgcheck = 1
|
||||
|
||||
@@ -1091,7 +1091,7 @@ session.save_handler = memcache
|
||||
; RPM note : session directory must be owned by process owner
|
||||
; for mod_php, see /etc/httpd/conf.d/php.conf
|
||||
; for php-fpm, see /etc/php-fpm.d/*conf
|
||||
session.save_path = "tcp://localhost:11211"
|
||||
session.save_path = "tcp://memcache:11211"
|
||||
|
||||
; Whether to use strict session mode.
|
||||
; Strict session mode does not accept an uninitialized session ID, and
|
||||
@@ -1496,7 +1496,15 @@ ldap.max_links = -1
|
||||
;dba.default_handler=
|
||||
|
||||
[opcache]
|
||||
; see /etc/php.d/10-opcache.ini
|
||||
; Optimized for shared hosting — reduce idle memory footprint
|
||||
; Default 128MB is excessive for most WordPress sites
|
||||
opcache.memory_consumption = 64
|
||||
opcache.interned_strings_buffer = 8
|
||||
opcache.max_accelerated_files = 4000
|
||||
; Revalidate files every 60s in production (reduces stat() calls)
|
||||
opcache.revalidate_freq = 60
|
||||
; Don't waste memory on CLI scripts
|
||||
opcache.enable_cli = Off
|
||||
|
||||
[curl]
|
||||
; A default value for the CURLOPT_CAINFO option. This is required to be an
|
||||
|
||||
2
configs/remote_ip.conf
Normal file
2
configs/remote_ip.conf
Normal file
@@ -0,0 +1,2 @@
|
||||
RemoteIPHeader X-Forwarded-For
|
||||
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
|
||||
38
configs/shared-ols/httpd_config_base.tpl
Normal file
38
configs/shared-ols/httpd_config_base.tpl
Normal file
@@ -0,0 +1,38 @@
|
||||
## ---- shared-ols append (do not edit below) ----
|
||||
## Server-level config for the SHARED OpenLiteSpeed tier. Appended to the
|
||||
## stock httpd_config.conf AFTER render-shared-ols-config.sh strips the stock
|
||||
## listeners, vhTemplate docker, AND the stock `extProcessor lsphp` +
|
||||
## `scriptHandler` (so this server NEVER runs PHP locally — every site's PHP
|
||||
## goes to its own detached cac-lsphp sidecar over LSAPI). Rendered with
|
||||
## envsubst; only ${LSCACHE_ROOT} is substituted here.
|
||||
|
||||
serverName shared-ols
|
||||
|
||||
## Real client IP behind HAProxy. HAProxy sets X-Forwarded-For (the real
|
||||
## client) and X-Forwarded-Proto. Mode 2 = trust the proxy header. HAProxy is
|
||||
## the only thing that ever connects to this tier (it's not publicly exposed),
|
||||
## so trusting the header from the docker-network peer is safe — same trust
|
||||
## model as the shared httpd's RemoteIPInternalProxy.
|
||||
useIpInProxyHeader 2
|
||||
|
||||
## LSCache enabled at MODULE scope for the whole tier (dedicated cache volume,
|
||||
## ephemeral across rebuilds; OLS auto-keys a per-vhost subdir under storagePath).
|
||||
## enableCache/enablePrivateCache ON here means the cache module is ACTIVE, but a
|
||||
## response is only cached if it's marked cacheable — the LiteSpeed Cache WP
|
||||
## plugin sets X-LiteSpeed-Cache-Control headers, and checkPublic/PrivateCache +
|
||||
## ignoreRespCacheCtrl=0 make OLS honor them. No plugin → nothing cached (safe).
|
||||
module cache {
|
||||
storagePath ${LSCACHE_ROOT}
|
||||
checkPrivateCache 1
|
||||
checkPublicCache 1
|
||||
maxCacheObjSize 10000000
|
||||
maxStaleAge 200
|
||||
qsCache 1
|
||||
reqCookieCache 1
|
||||
respCookieCache 1
|
||||
ignoreReqCacheCtrl 0
|
||||
ignoreRespCacheCtrl 0
|
||||
enableCache 1
|
||||
enablePrivateCache 1
|
||||
}
|
||||
## ---- end shared-ols server append ----
|
||||
37
configs/shared-vhost-template.tpl
Normal file
37
configs/shared-vhost-template.tpl
Normal file
@@ -0,0 +1,37 @@
|
||||
<Directory "/mnt/users/~~user~~/~~domain~~">
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory "/mnt/users/~~user~~/~~domain~~/public_html">
|
||||
Options All MultiViews
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName "~~domain~~"
|
||||
~~alias_block~~
|
||||
DocumentRoot "/mnt/users/~~user~~/~~domain~~/public_html"
|
||||
RewriteEngine on
|
||||
RewriteCond %{SERVER_NAME} =~~domain~~
|
||||
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
|
||||
</VirtualHost>
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost *:443>
|
||||
ServerName "~~domain~~"
|
||||
~~alias_block~~
|
||||
DocumentRoot "/mnt/users/~~user~~/~~domain~~/public_html"
|
||||
|
||||
SSLCertificateFile /etc/pki/tls/certs/localhost.crt
|
||||
SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
|
||||
|
||||
~~proxy_block~~
|
||||
|
||||
DirectoryIndex index.php index.html index.htm
|
||||
|
||||
ErrorLog "/var/log/httpd/~~domain~~-error.log"
|
||||
CustomLog "/var/log/httpd/~~domain~~-access.log" combined
|
||||
</VirtualHost>
|
||||
</IfModule>
|
||||
@@ -40,6 +40,42 @@
|
||||
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
|
||||
</VirtualHost>
|
||||
|
||||
#
|
||||
# When we also provide SSL we have to listen to the
|
||||
# standard HTTPS port in addition.
|
||||
#
|
||||
Listen 443 https
|
||||
|
||||
##
|
||||
## SSL Global Context
|
||||
##
|
||||
## All SSL configuration in this context applies both to
|
||||
## the main server and all SSL-enabled virtual hosts.
|
||||
##
|
||||
|
||||
# Pass Phrase Dialog:
|
||||
# Configure the pass phrase gathering process.
|
||||
# The filtering dialog program (`builtin' is a internal
|
||||
# terminal dialog) has to provide the pass phrase on stdout.
|
||||
SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog
|
||||
|
||||
# Inter-Process Session Cache:
|
||||
# Configure the SSL Session Cache: First the mechanism
|
||||
# to use and second the expiring timeout (in seconds).
|
||||
SSLSessionCache shmcb:/run/httpd/sslcache(512000)
|
||||
SSLSessionCacheTimeout 300
|
||||
|
||||
#
|
||||
# Use "SSLCryptoDevice" to enable any supported hardware
|
||||
# accelerators. Use "openssl engine -v" to list supported
|
||||
# engine names. NOTE: If you enable an accelerator and the
|
||||
# server does not start, consult the error logs and ensure
|
||||
# your accelerator is functioning properly.
|
||||
#
|
||||
SSLCryptoDevice builtin
|
||||
|
||||
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost _default_:443>
|
||||
ServerName "~~domain~~"
|
||||
|
||||
24
local-dev.sh
24
local-dev.sh
@@ -5,18 +5,20 @@ https_port='443'
|
||||
root_path="$(pwd)"
|
||||
verbose='false'
|
||||
|
||||
while getopts 'n:p:s:r:vh' flag; do
|
||||
while getopts 'n:p:s:r:a:vh' flag; do
|
||||
case "${flag}" in
|
||||
n) name="${OPTARG}" ;;
|
||||
p) http_port="${OPTARG}" ;;
|
||||
s) https_port="${OPTARG}" ;;
|
||||
r) root_path="${OPTARG}" ;;
|
||||
a) phpver="${OPTARG}" ;;
|
||||
v) verbose='true' ;;
|
||||
h) echo "Variables"
|
||||
echo "-n = Name of Container, Required"
|
||||
echo "-p = Non-https Port Override, default 80"
|
||||
echo "-s = Https Port Override, default 443"
|
||||
echo "-r = Root Path for files and database, defaults to current working path"
|
||||
echo "-a = PHP App Version, Default to 8.3"
|
||||
echo "-v = Enable Verbose Mode"
|
||||
exit 1 ;;
|
||||
esac
|
||||
@@ -34,16 +36,20 @@ if [ -z "$name" ]; then
|
||||
echo "Name not set, please set it with -n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$phpver" ]; then
|
||||
phpver=83;
|
||||
fi
|
||||
|
||||
echo "Building Docker Image..."
|
||||
user=$(whoami)
|
||||
uid=$(id -u)
|
||||
if [ ! -d "$root_path/db" ]; then
|
||||
mkdir -p "$root_path/db";
|
||||
if [ ! -d "$root_path/user" ]; then
|
||||
mkdir -p "$root_path/user";
|
||||
mkdir -p "$root_path/user/logs/{apache,system}";
|
||||
fi
|
||||
if [ ! -d "$root_path/web" ]; then
|
||||
mkdir -p "$root_path/web";
|
||||
fi
|
||||
$check_docker run -d -p "$http_port":80 -p "$https_port":443 -e PHPVER=82 -e environment=DEV --mount type=bind,source="$root_path"/web,target=/home/"$user"/public_html --mount type=bind,source="$root_path"/db,target=/var/lib/mysql -e uid="$uid" -e user="$user" -e domain="$name-local.dev" --name "$name" public.ecr.aws/s1f6k4w4/cac
|
||||
$check_docker volume create "$name-mysql"
|
||||
$check_docker run --pull=always -d -p "$http_port":80 -p "$https_port":443 -e PHPVER=$phpver -e environment=DEV --mount type=bind,source="$root_path"/user,target=/home/"$user" --mount type=bind,source="$(pwd)"/user/logs/apache,target=/etc/httpd/logs --mount type=bind,source="$(pwd)"/user/logs/system,target=/var/log -v"$name-mysql":/var/lib/mysql -e uid="$uid" -e user="$user" -e domain="$name-local.dev" --name "$name" repo.anhonesthost.net/cloud-hosting-platform/cac:latest
|
||||
echo "Creating management scripts in root directory..."
|
||||
echo "#!/usr/bin/env bash" > "$root_path/instance_start"
|
||||
echo "docker start $name" >> "$root_path/instance_start"
|
||||
@@ -54,8 +60,8 @@ echo "docker exec $name bash -c 'tail -f /etc/httpd/logs/*'" >> "$root_path/inst
|
||||
echo "#!/usr/bin/env bash" > "$root_path/instance_db_info"
|
||||
echo "docker exec $name cat /var/lib/mysql/creds" >> "$root_path/instance_db_info"
|
||||
chmod +x $root_path/instance_*
|
||||
echo "Waiting 120 seconds for setup to finish"
|
||||
sleep 120;
|
||||
echo "Waiting 160 seconds for setup to finish"
|
||||
sleep 160;
|
||||
echo "Installing WordPress..."
|
||||
wpdbuser=$(docker exec $name cat /var/lib/mysql/creds |grep User| awk -F ": " {'print $2'})
|
||||
wpdbpass=$(docker exec $name cat /var/lib/mysql/creds |grep Password| awk -F ": " {'print $2'})
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
resources:
|
||||
- name: cac
|
||||
type: git
|
||||
source:
|
||||
uri: https://repo.anhonesthost.net/cloud-hosting-platform/cloud-apache-container.git
|
||||
branch: trunk
|
||||
|
||||
- name: build-cac-74
|
||||
type: docker-image
|
||||
source:
|
||||
repository: registry.dnspegasus.net/cac
|
||||
tag: 74
|
||||
|
||||
- name: build-cac-80
|
||||
type: docker-image
|
||||
source:
|
||||
repository: registry.dnspegasus.net/cac
|
||||
tag: 80
|
||||
|
||||
- name: build-cac-81
|
||||
type: docker-image
|
||||
source:
|
||||
repository: registry.dnspegasus.net/cac
|
||||
tag: 81
|
||||
|
||||
- name: build-cac-82
|
||||
type: docker-image
|
||||
source:
|
||||
repository: registry.dnspegasus.net/cac
|
||||
tag: 82
|
||||
|
||||
jobs:
|
||||
- name: publish-cac-74
|
||||
plan:
|
||||
- get: cac
|
||||
trigger: true
|
||||
- put: build-cac-74
|
||||
params:
|
||||
build: cac
|
||||
build_args:
|
||||
PHPVER: 74
|
||||
- name: publish-cac-80
|
||||
plan:
|
||||
- get: cac
|
||||
trigger: true
|
||||
- put: build-cac-80
|
||||
params:
|
||||
build: cac
|
||||
build_args:
|
||||
PHPVER: 80
|
||||
- name: publish-cac-81
|
||||
plan:
|
||||
- get: cac
|
||||
trigger: true
|
||||
- put: build-cac-81
|
||||
params:
|
||||
build: cac
|
||||
build_args:
|
||||
PHPVER: 81
|
||||
- name: publish-cac-82
|
||||
plan:
|
||||
- get: cac
|
||||
trigger: true
|
||||
- put: build-cac-82
|
||||
params:
|
||||
build: cac
|
||||
build_args:
|
||||
PHPVER: 82
|
||||
|
||||
30
scripts/cac-lsphp-normalize.php
Normal file
30
scripts/cac-lsphp-normalize.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* cac-lsphp $_SERVER path normaliser (auto_prepend).
|
||||
*
|
||||
* The shared-ols container serves from its bulk /docker/users->/mnt/users mount,
|
||||
* so OLS sends lsphp $_SERVER['DOCUMENT_ROOT'] / ['SCRIPT_FILENAME'] under
|
||||
* /mnt/users/<user>/<domain>/... . The sidecar symlinks that back to the real
|
||||
* /home/<user> mount, so file operations resolve and PHP's own __FILE__/__DIR__/
|
||||
* realpath()/getcwd() already report /home/<user>/public_html. But the RAW env
|
||||
* strings OLS set still read /mnt/users, which would leak to the (uncommon) apps
|
||||
* that build or compare paths from $_SERVER['DOCUMENT_ROOT'].
|
||||
*
|
||||
* Canonicalise those two via realpath() so cac-lsphp is byte-for-byte 1:1 with
|
||||
* cac-fpm/cac-litespeed (where DOCUMENT_ROOT is natively /home/<user>/public_html).
|
||||
* Cheap (two realpath calls, cached by realpath_cache) and side-effect-free.
|
||||
*
|
||||
* Customer sites have no auto_prepend by default, so this is the only prepend in
|
||||
* play. If a site sets its own auto_prepend_file via .user.ini it overrides this
|
||||
* (theirs wins) — acceptable: paths still resolve via the symlink, only the raw
|
||||
* string differs.
|
||||
*/
|
||||
foreach (array('DOCUMENT_ROOT', 'SCRIPT_FILENAME') as $__cl_key) {
|
||||
if (!empty($_SERVER[$__cl_key]) && strncmp($_SERVER[$__cl_key], '/mnt/users/', 11) === 0) {
|
||||
$__cl_real = realpath($_SERVER[$__cl_key]);
|
||||
if ($__cl_real !== false) {
|
||||
$_SERVER[$__cl_key] = $__cl_real;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($__cl_key, $__cl_real);
|
||||
17
scripts/create-apache-mpm-config.sh
Executable file
17
scripts/create-apache-mpm-config.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Generate Apache MPM event tuning config at runtime using detect-memory.sh values.
|
||||
|
||||
cat <<EOF > /etc/httpd/conf.d/mpm-tuning.conf
|
||||
<IfModule mpm_event_module>
|
||||
StartServers ${APACHE_START_SERVERS}
|
||||
MinSpareThreads ${APACHE_MIN_SPARE_THREADS}
|
||||
MaxSpareThreads ${APACHE_MAX_SPARE_THREADS}
|
||||
ThreadLimit 64
|
||||
ThreadsPerChild 25
|
||||
MaxRequestWorkers ${APACHE_MAX_REQUEST_WORKERS}
|
||||
ServerLimit ${APACHE_SERVER_LIMIT}
|
||||
MaxConnectionsPerChild ${APACHE_MAX_CONNECTIONS_PER_CHILD}
|
||||
</IfModule>
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
@@ -2,27 +2,57 @@
|
||||
|
||||
rm /etc/php-fpm.d/www.conf
|
||||
|
||||
FPM_LISTEN=${FPM_LISTEN:-/run/php-fpm/www.sock}
|
||||
|
||||
# Determine listen directive and ownership based on socket vs TCP
|
||||
if echo "$FPM_LISTEN" | grep -q '/'; then
|
||||
# Unix socket mode (standalone — Apache and FPM in same container)
|
||||
listen_directive="$FPM_LISTEN"
|
||||
listen_owner_block="listen.owner = apache
|
||||
listen.group = apache"
|
||||
env_block=""
|
||||
else
|
||||
# TCP port mode (shared httpd — FPM in separate container)
|
||||
listen_directive="0.0.0.0:${FPM_LISTEN}"
|
||||
listen_owner_block=""
|
||||
# Override DOCUMENT_ROOT so PHP plugins (e.g., WordFence) that use
|
||||
# $_SERVER['DOCUMENT_ROOT'] find files at the FPM container's path,
|
||||
# not the shared httpd's /mnt/users/ mount path.
|
||||
env_block="env[DOCUMENT_ROOT] = /home/$user/public_html"
|
||||
fi
|
||||
|
||||
cat <<EOF > /etc/php-fpm.d/$user.conf
|
||||
|
||||
[$user]
|
||||
|
||||
user = $user
|
||||
group = $user
|
||||
listen = /run/php-fpm/www.sock
|
||||
listen.owner = apache
|
||||
listen.group = apache
|
||||
listen = ${listen_directive}
|
||||
${listen_owner_block}
|
||||
|
||||
pm = static
|
||||
pm.max_children = 10
|
||||
pm.max_requests = 150
|
||||
pm = ${PHP_FPM_PM}
|
||||
pm.max_children = ${PHP_FPM_MAX_CHILDREN}
|
||||
pm.max_requests = ${PHP_FPM_MAX_REQUESTS}
|
||||
pm.process_idle_timeout = ${PHP_FPM_PROCESS_IDLE_TIMEOUT}
|
||||
|
||||
slowlog = /etc/httpd/logs/error_log
|
||||
; Settings used when pm = dynamic (fallback if user overrides FPM_PM)
|
||||
pm.start_servers = ${PHP_FPM_START_SERVERS}
|
||||
pm.min_spare_servers = ${PHP_FPM_MIN_SPARE}
|
||||
pm.max_spare_servers = ${PHP_FPM_MAX_SPARE}
|
||||
|
||||
; Health check endpoints
|
||||
ping.path = /fpm-ping
|
||||
ping.response = pong
|
||||
pm.status_path = /fpm-status
|
||||
|
||||
slowlog = /home/$user/logs/php-fpm/slowlog
|
||||
request_slowlog_timeout = 3s
|
||||
|
||||
php_admin_value[error_log] = /etc/httpd/logs/error_log
|
||||
php_admin_value[error_log] = /home/$user/logs/php-fpm/error.log
|
||||
php_admin_flag[log_errors] = on
|
||||
php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache
|
||||
${env_block}
|
||||
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
exit 0
|
||||
|
||||
92
scripts/create-vhost-litespeed.sh
Normal file
92
scripts/create-vhost-litespeed.sh
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/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 `}`.
|
||||
## extProcessor lsphp is stripped because the stock one hard-codes
|
||||
## PHP_LSAPI_CHILDREN=10 regardless of container size — our appended
|
||||
## extProcessor scales it from detect-memory-litespeed.sh.
|
||||
awk '
|
||||
BEGIN { skip = 0 }
|
||||
/^listener HTTP \{/ || /^listener HTTPS \{/ || /^vhTemplate docker \{/ || /^extProcessor lsphp\{/ || /^extProcessor lsphp \{/ { 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"
|
||||
|
||||
## Server-level user/group → customer. Without this, OLS runs as nobody and
|
||||
## either can't read customer files (no setUIDMode) or has to lscgid-spawn a
|
||||
## per-uid lsphp for every httpd worker (the setUIDMode 2 pathway). With OLS
|
||||
## itself running as ${user}, a single shared lsphp parent serves all httpd
|
||||
## workers, LSAPI children-mode actually engages, and shmem stops fanning out.
|
||||
## OLS still starts as root (PID 1 binds 80/443) then drops privs after bind.
|
||||
sed -i \
|
||||
-e "s|^user[[:space:]].*|user ${user}|" \
|
||||
-e "s|^group[[:space:]].*|group ${user}|" \
|
||||
"$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} ${PHPVER} ${LSAPI_CHILDREN}' < "$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
|
||||
@@ -34,6 +34,12 @@ cat <<EOF > /etc/httpd/conf.d/$domain.conf
|
||||
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
|
||||
</VirtualHost>
|
||||
|
||||
Listen 443 https
|
||||
SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog
|
||||
SSLSessionCache shmcb:/run/httpd/sslcache(512000)
|
||||
SSLSessionCacheTimeout 300
|
||||
SSLCryptoDevice builtin
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost _default_:443>
|
||||
ServerName "$domain"
|
||||
|
||||
89
scripts/detect-memory-litespeed.sh
Normal file
89
scripts/detect-memory-litespeed.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/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) ----
|
||||
## Per the 2026-06-02 cac-litespeed memory-sizing finding (vantagehealth
|
||||
## OOM-spawn loop at 512 MB cap): each lsphp worker carries ~115 MB
|
||||
## shmem-rss + ~25 MB anon + ~10 MB file ≈ 130-150 MB real cgroup cost
|
||||
## per worker on heavy WP workloads. shmem is RSS-accounted per worker
|
||||
## (vs cac-fpm's COW-shared fork model) so the cost is real per cgroup,
|
||||
## not just per process.
|
||||
##
|
||||
## 115 (the previous default) was set from idle-state measurements and
|
||||
## ran brain-jar.com into 142 OOM-kills at 1 GiB on 2026-06-02 night —
|
||||
## the formula computed CHILDREN=8, which left zero headroom once Divi
|
||||
## page renders started growing worker anon. Bumped to 130 to track the
|
||||
## active per-worker cost; gives slightly fewer workers but real headroom.
|
||||
##
|
||||
## Sub-512 MB containers remain unsafe for dynamic WP on OLS — the floor
|
||||
## of 2 workers still applies but it'll be cap-marginal. Per-site override
|
||||
## via FPM_MAX_CHILDREN env var (panel edit-site UI) overrides this for
|
||||
## sites where the default isn't right for their workload.
|
||||
LSPHP_WORKER_ESTIMATE_MB=${LSPHP_WORKER_ESTIMATE_MB:-130}
|
||||
|
||||
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}}
|
||||
|
||||
## Per-worker mem limits (RLIMIT_AS) live in httpd_config.tpl now as
|
||||
## hard-coded 1024M soft / 1500M hard — those values comfortably fit
|
||||
## typical Divi/WooCommerce VSZ (~280-365 MB) while still catching a
|
||||
## true runaway script. Cgroup remains the real backstop. The earlier
|
||||
## AVAILABLE/CHILDREN formula was killing legitimate workers because
|
||||
## it conflated VSZ (RLIMIT_AS) with RSS-budget arithmetic.
|
||||
|
||||
export CONTAINER_MEMORY_MB LSAPI_CHILDREN
|
||||
75
scripts/detect-memory-lsphp.sh
Normal file
75
scripts/detect-memory-lsphp.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
## detect-memory-lsphp.sh — sibling of detect-memory-litespeed.sh for the
|
||||
## cac-lsphp DETACHED sidecar (lsphp -b, no local webserver).
|
||||
##
|
||||
## Computes PHP_LSAPI_CHILDREN from the container memory cap. Identical worker
|
||||
## arithmetic to detect-memory-litespeed.sh, with ONE difference: there is no
|
||||
## OpenLiteSpeed daemon in this container (OLS runs in the shared-ols tier), so
|
||||
## the ~40 MB OLS_RESERVE is dropped — every MB above the OS reserve goes to
|
||||
## lsphp workers. Sourced by entrypoint-lsphp.sh.
|
||||
|
||||
## ---- container memory detection (mirrors detect-memory-litespeed.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 (all non-OS memory is the lsphp workers' to use) ----
|
||||
OS_RESERVE_MB=50
|
||||
DEV_OVERHEAD_MB=0
|
||||
if [ "${environment:-PROD}" = "DEV" ]; then
|
||||
DEV_OVERHEAD_MB=125
|
||||
fi
|
||||
|
||||
AVAILABLE_MB=$((CONTAINER_MEMORY_MB - OS_RESERVE_MB - DEV_OVERHEAD_MB))
|
||||
if [ "$AVAILABLE_MB" -lt 60 ]; then
|
||||
AVAILABLE_MB=60
|
||||
fi
|
||||
|
||||
## ---- LSAPI children ----
|
||||
## Same ~130 MB/worker estimate as cac-litespeed (see detect-memory-litespeed.sh
|
||||
## for the vantagehealth/brain-jar OOM history that set this). Detached lsphp
|
||||
## has the SAME per-worker shmem-RSS profile as in-container lsphp — splitting
|
||||
## the webserver out doesn't change lsphp's memory model, only removes the OLS
|
||||
## daemon footprint from the budget.
|
||||
LSPHP_WORKER_ESTIMATE_MB=${LSPHP_WORKER_ESTIMATE_MB:-130}
|
||||
|
||||
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 precedence — the WHP panel (site-pool-env.php) passes the
|
||||
## customer's override as LSAPI_CHILDREN and/or FPM_MAX_CHILDREN; either wins
|
||||
## over the calculated default. entrypoint-lsphp.sh exports the result as
|
||||
## PHP_LSAPI_CHILDREN (the name lsphp -b reads).
|
||||
LSAPI_CHILDREN=${LSAPI_CHILDREN:-${FPM_MAX_CHILDREN:-$calc_lsapi_children}}
|
||||
|
||||
export CONTAINER_MEMORY_MB LSAPI_CHILDREN
|
||||
142
scripts/detect-memory.sh
Executable file
142
scripts/detect-memory.sh
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
# detect-memory.sh — Detect container memory and calculate tuning parameters.
|
||||
# Must be sourced (not executed) so variables are available to the caller.
|
||||
|
||||
# --- Memory detection (cgroups v2 → v1 → /proc/meminfo → fallback) ---
|
||||
CONTAINER_MEMORY_BYTES=""
|
||||
|
||||
# cgroups v2
|
||||
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
|
||||
|
||||
# cgroups v1
|
||||
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)
|
||||
# Values near page-aligned max (like 9223372036854771712) mean "no limit"
|
||||
if [ -n "$val" ] && [ "$val" -lt 8589934592000 ] 2>/dev/null; then
|
||||
CONTAINER_MEMORY_BYTES=$val
|
||||
fi
|
||||
fi
|
||||
|
||||
# /proc/meminfo (host memory — used when no cgroup limit is set)
|
||||
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
|
||||
|
||||
# Fallback
|
||||
if [ -z "$CONTAINER_MEMORY_BYTES" ]; then
|
||||
CONTAINER_MEMORY_BYTES=$((512 * 1024 * 1024))
|
||||
fi
|
||||
|
||||
CONTAINER_MEMORY_MB=$((CONTAINER_MEMORY_BYTES / 1024 / 1024))
|
||||
|
||||
# --- Budget calculation ---
|
||||
CONTAINER_ROLE=${CONTAINER_ROLE:-combined} # combined | fpm_only | httpd_only
|
||||
|
||||
OS_RESERVE_MB=50
|
||||
FIXED_PROCESS_MB=50
|
||||
DEV_OVERHEAD_MB=0
|
||||
if [ "$environment" = "DEV" ]; then
|
||||
DEV_OVERHEAD_MB=125
|
||||
fi
|
||||
|
||||
AVAILABLE_MB=$((CONTAINER_MEMORY_MB - OS_RESERVE_MB - FIXED_PROCESS_MB - DEV_OVERHEAD_MB))
|
||||
if [ "$AVAILABLE_MB" -lt 60 ]; then
|
||||
AVAILABLE_MB=60
|
||||
fi
|
||||
|
||||
case "$CONTAINER_ROLE" in
|
||||
fpm_only)
|
||||
PHP_BUDGET_MB=$AVAILABLE_MB
|
||||
APACHE_BUDGET_MB=0
|
||||
;;
|
||||
httpd_only)
|
||||
PHP_BUDGET_MB=0
|
||||
APACHE_BUDGET_MB=$AVAILABLE_MB
|
||||
;;
|
||||
*)
|
||||
PHP_BUDGET_MB=$((AVAILABLE_MB * 80 / 100))
|
||||
APACHE_BUDGET_MB=$((AVAILABLE_MB * 20 / 100))
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- PHP-FPM parameters (skipped for httpd_only) ---
|
||||
if [ "$CONTAINER_ROLE" != "httpd_only" ]; then
|
||||
# PHP_WORKER_ESTIMATE_MB sizes the divisor for pm.max_children. The
|
||||
# previous default of 60 was optimistic for modern Woo/Elementor stacks:
|
||||
# the alphaone 2026-06-01 incident measured ~193 MB resident per worker
|
||||
# against the 60 MB assumption, and 15 calculated children put peak
|
||||
# demand (15 * 193 = 2.9 GB) over the 1-2 GiB container cap. 128 lands
|
||||
# closer to plugin-heavy WP reality while remaining conservative for
|
||||
# leaner sites. Customers can still override via the FPM_MAX_CHILDREN
|
||||
# env var on the container if a different shape is justified.
|
||||
PHP_WORKER_ESTIMATE_MB=${PHP_WORKER_ESTIMATE_MB:-128}
|
||||
|
||||
calc_max_children=$((PHP_BUDGET_MB / PHP_WORKER_ESTIMATE_MB))
|
||||
# Floor at 2, cap at 50
|
||||
if [ "$calc_max_children" -lt 2 ]; then
|
||||
calc_max_children=2
|
||||
fi
|
||||
if [ "$calc_max_children" -gt 50 ]; then
|
||||
calc_max_children=50
|
||||
fi
|
||||
|
||||
PHP_FPM_PM=${FPM_PM:-ondemand}
|
||||
PHP_FPM_MAX_CHILDREN=${FPM_MAX_CHILDREN:-$calc_max_children}
|
||||
PHP_FPM_PROCESS_IDLE_TIMEOUT=${FPM_PROCESS_IDLE_TIMEOUT:-5s}
|
||||
PHP_FPM_MAX_REQUESTS=${FPM_MAX_REQUESTS:-200}
|
||||
|
||||
# Dynamic mode fallbacks (used if user overrides FPM_PM=dynamic)
|
||||
PHP_FPM_START_SERVERS=${FPM_START_SERVERS:-2}
|
||||
PHP_FPM_MIN_SPARE=${FPM_MIN_SPARE_SERVERS:-1}
|
||||
PHP_FPM_MAX_SPARE=${FPM_MAX_SPARE_SERVERS:-3}
|
||||
fi
|
||||
|
||||
# --- Apache MPM parameters (skipped for fpm_only) ---
|
||||
if [ "$CONTAINER_ROLE" != "fpm_only" ]; then
|
||||
# ServerLimit: roughly 1 process per ~25 workers, floor 2, cap 16
|
||||
calc_server_limit=$((APACHE_BUDGET_MB / 30))
|
||||
if [ "$calc_server_limit" -lt 2 ]; then
|
||||
calc_server_limit=2
|
||||
fi
|
||||
if [ "$calc_server_limit" -gt 16 ]; then
|
||||
calc_server_limit=16
|
||||
fi
|
||||
|
||||
# MaxRequestWorkers: ServerLimit * ThreadsPerChild (25)
|
||||
calc_max_request_workers=$((calc_server_limit * 25))
|
||||
if [ "$calc_max_request_workers" -gt 400 ]; then
|
||||
calc_max_request_workers=400
|
||||
fi
|
||||
|
||||
# StartServers: 1 for ≤1GB, 2 for larger
|
||||
calc_start_servers=1
|
||||
if [ "$CONTAINER_MEMORY_MB" -gt 1024 ]; then
|
||||
calc_start_servers=2
|
||||
fi
|
||||
|
||||
APACHE_START_SERVERS=${APACHE_START_SERVERS:-$calc_start_servers}
|
||||
APACHE_SERVER_LIMIT=${APACHE_SERVER_LIMIT:-$calc_server_limit}
|
||||
APACHE_MAX_REQUEST_WORKERS=${APACHE_MAX_REQUEST_WORKERS:-$calc_max_request_workers}
|
||||
APACHE_MIN_SPARE_THREADS=${APACHE_MIN_SPARE_THREADS:-5}
|
||||
APACHE_MAX_SPARE_THREADS=${APACHE_MAX_SPARE_THREADS:-15}
|
||||
APACHE_MAX_CONNECTIONS_PER_CHILD=${APACHE_MAX_CONNECTIONS_PER_CHILD:-3000}
|
||||
fi
|
||||
|
||||
# --- Export all variables ---
|
||||
export CONTAINER_ROLE CONTAINER_MEMORY_MB
|
||||
if [ "$CONTAINER_ROLE" != "httpd_only" ]; then
|
||||
export PHP_FPM_PM PHP_FPM_MAX_CHILDREN PHP_FPM_PROCESS_IDLE_TIMEOUT PHP_FPM_MAX_REQUESTS
|
||||
export PHP_FPM_START_SERVERS PHP_FPM_MIN_SPARE PHP_FPM_MAX_SPARE
|
||||
fi
|
||||
if [ "$CONTAINER_ROLE" != "fpm_only" ]; then
|
||||
export APACHE_START_SERVERS APACHE_SERVER_LIMIT APACHE_MAX_REQUEST_WORKERS
|
||||
export APACHE_MIN_SPARE_THREADS APACHE_MAX_SPARE_THREADS APACHE_MAX_CONNECTIONS_PER_CHILD
|
||||
fi
|
||||
86
scripts/entrypoint-fpm.sh
Executable file
86
scripts/entrypoint-fpm.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$PHPVER" ]; then
|
||||
PHPVER="83";
|
||||
fi
|
||||
|
||||
if [ -z "$environment" ]; then
|
||||
environment="PROD"
|
||||
fi
|
||||
|
||||
# Default to FPM-only role
|
||||
export CONTAINER_ROLE="fpm_only"
|
||||
export FPM_LISTEN=${FPM_LISTEN:-9000}
|
||||
|
||||
adduser -u $uid $user
|
||||
|
||||
mkdir -p /home/$user/public_html
|
||||
mkdir -p /home/$user/logs/php-fpm
|
||||
|
||||
ln -sf /home/$user/logs/php-fpm /var/log/php-fpm
|
||||
|
||||
source /scripts/detect-memory.sh
|
||||
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP-FPM pm=${PHP_FPM_PM} max_children=${PHP_FPM_MAX_CHILDREN} | Listen=${FPM_LISTEN}"
|
||||
|
||||
/scripts/create-php-config.sh
|
||||
|
||||
mkdir -p /run/php-fpm/
|
||||
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
||||
chown -R $user:$user /home/$user
|
||||
chmod -R 755 /home/$user
|
||||
|
||||
if [[ $environment == 'DEV' ]]; then
|
||||
echo "Starting Dev Deployment (FPM-only mode)"
|
||||
mkdir -p /home/$user/_db_backups
|
||||
if ! command -v microdnf &> /dev/null; then
|
||||
echo "microdnf not found, installing with dnf..."
|
||||
dnf install -y microdnf && dnf clean all
|
||||
fi
|
||||
microdnf install -y MariaDB-server MariaDB-client memcached
|
||||
sed -r -i 's/session.save_path="memcache:11211/session.save_path="localhost:11211/' /etc/php.ini
|
||||
nohup mysqld -umysql &
|
||||
if [ ! -f /home/$user/mysql_creds ]; then
|
||||
echo "Give MySQL a chance to finish starting..."
|
||||
sleep 10
|
||||
mysql_user=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13 ; echo '')
|
||||
mysql_password=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 18 ; echo '')
|
||||
mysql_db=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 6 ; echo '')
|
||||
mysql -e "CREATE DATABASE devdb_"$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 "# User crontab for $user" > /home/$user/crontab
|
||||
echo "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" >> /home/$user/crontab
|
||||
chown $user:$user /home/$user/crontab
|
||||
echo "MySQL User: "$mysql_user > /home/$user/mysql_creds
|
||||
echo "MySQL Password: "$mysql_password >> /home/$user/mysql_creds
|
||||
echo "MySQL Database: devdb_"$mysql_db >> /home/$user/mysql_creds
|
||||
cat /home/$user/mysql_creds
|
||||
fi
|
||||
/usr/bin/memcached -d -u $user
|
||||
fi
|
||||
|
||||
if [[ $environment == 'PROD' ]]; then
|
||||
if [ -f /etc/php.d/50-memcached.ini ]; then
|
||||
sed -r -i 's/;session.save_path="localhost:11211/session.save_path="memcache:11211/' /etc/php.d/50-memcached.ini
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set up user crontab
|
||||
if [ ! -f /home/$user/crontab ]; then
|
||||
echo "# User crontab for $user" > /home/$user/crontab
|
||||
echo "# Add your cron jobs here" >> /home/$user/crontab
|
||||
echo "# Example: */5 * * * * /home/$user/scripts/my-script.sh" >> /home/$user/crontab
|
||||
chown $user:$user /home/$user/crontab
|
||||
fi
|
||||
|
||||
# Load user crontab
|
||||
crontab -u $user /home/$user/crontab
|
||||
|
||||
/usr/sbin/crond
|
||||
|
||||
# Tail PHP-FPM logs (becomes PID 1 process)
|
||||
touch /home/$user/logs/php-fpm/error.log
|
||||
tail -f /home/$user/logs/php-fpm/*
|
||||
|
||||
exit 0
|
||||
255
scripts/entrypoint-litespeed.sh
Normal file
255
scripts/entrypoint-litespeed.sh
Normal file
@@ -0,0 +1,255 @@
|
||||
#!/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"
|
||||
## Log dirs mirror cac:phpNN exactly — apache/ for web server access+error,
|
||||
## php-fpm/ for PHP errors. OLS isn't Apache and lsphp isn't php-fpm, but
|
||||
## the customer-facing paths stay identical so log-gathering, analytics,
|
||||
## and the customer's "where do I find my access log?" mental model all
|
||||
## just work without per-image-family special cases.
|
||||
mkdir -p "/home/$user/logs/apache" "/home/$user/logs/php-fpm"
|
||||
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} | 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
|
||||
|
||||
## ---- point PHP error_log at the same customer-visible path that
|
||||
## cac:phpNN uses for php-fpm errors. Drop-in compat: customer code that
|
||||
## was tailing /home/$user/logs/php-fpm/error.log on the old image will
|
||||
## see lsphp's PHP errors in the exact same file on the new image.
|
||||
## Rendered as a tiny ini in lsphp's scan dir; PHP merges it after the
|
||||
## production-tuning overrides at startup.
|
||||
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
|
||||
if [ -n "$SCAN_DIR" ]; then
|
||||
cat > "$SCAN_DIR/99-user-error-log.ini" <<EOF
|
||||
; rendered at container start by entrypoint-litespeed.sh
|
||||
error_log = /home/${user}/logs/php-fpm/error.log
|
||||
log_errors = On
|
||||
EOF
|
||||
|
||||
## Per-site opcache override (panel: Advanced Tuning → OpCache size).
|
||||
## Falls back to the global lsphp-overrides.ini values (64 MB / 8000 files)
|
||||
## when the env vars aren't set. Numeric range/sanity is enforced in the
|
||||
## WHP panel before the env var lands here.
|
||||
if [ -n "${OPCACHE_MEMORY_MB:-}" ] || [ -n "${OPCACHE_MAX_FILES:-}" ]; then
|
||||
{
|
||||
echo "; rendered at container start by entrypoint-litespeed.sh"
|
||||
echo "; per-site override from WHP whp.sites.opcache_*_override"
|
||||
[ -n "${OPCACHE_MEMORY_MB:-}" ] && echo "opcache.memory_consumption = ${OPCACHE_MEMORY_MB}"
|
||||
[ -n "${OPCACHE_MAX_FILES:-}" ] && echo "opcache.max_accelerated_files = ${OPCACHE_MAX_FILES}"
|
||||
} > "$SCAN_DIR/99-user-opcache.ini"
|
||||
fi
|
||||
fi
|
||||
|
||||
## ---- ownership: OLS runs as $user end-to-end (server-level user set by
|
||||
## create-vhost-litespeed.sh, no setUIDMode). So OLS runtime dirs need to
|
||||
## be customer-owned for log writes, swap files, lsphp socket creation.
|
||||
## Master still starts as root for port binding, then drops privs to $user.
|
||||
chown -R "$user:$user" /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 NOT baked into the image (saves ~500MB
|
||||
## on PROD pulls). Install them at runtime, but only once per container —
|
||||
## the command -v guard means a restart of an already-bootstrapped
|
||||
## container skips the apt step and DEV boot stays ~1.5s like PROD.
|
||||
## First-boot in DEV adds ~30-60s for the apt install; acceptable
|
||||
## tradeoff per the design spec.
|
||||
if ! command -v mysqld >/dev/null 2>&1; then
|
||||
echo "DEV first boot: installing mariadb-server + memcached..."
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
mariadb-server memcached
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
||||
fi
|
||||
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 ) &
|
||||
|
||||
## Stream OLS + customer logs to PID-1 stdout so `docker logs` works. Started
|
||||
## once, before the supervisor loop — it follows the files across OLS restarts.
|
||||
touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log
|
||||
touch "/home/$user/logs/apache/error_log" "/home/$user/logs/apache/access_log"
|
||||
touch "/home/$user/logs/php-fpm/error.log"
|
||||
chown "$user:$user" "/home/$user/logs/apache/error_log" \
|
||||
"/home/$user/logs/apache/access_log" \
|
||||
"/home/$user/logs/php-fpm/error.log"
|
||||
tail -F /usr/local/lsws/logs/error.log \
|
||||
/usr/local/lsws/logs/access.log \
|
||||
"/home/$user/logs/apache/error_log" \
|
||||
"/home/$user/logs/apache/access_log" \
|
||||
"/home/$user/logs/php-fpm/error.log" 2>/dev/null &
|
||||
|
||||
## ---- supervise OLS in DAEMON mode (NOT `openlitespeed -n` + wait) ----
|
||||
## OLS performs INTERNAL graceful self-restarts: the LiteSpeed Cache /
|
||||
## QUIC.cloud integration refreshes the QUIC.cloud IP allowlist on a schedule
|
||||
## and, when it changes, sends SIGUSR1 → "request a graceful server restart".
|
||||
## In `-n` foreground mode the OLD main PID exits after the zero-downtime
|
||||
## handoff; a bare `wait` on that PID lets bash (PID 1) exit and tears the whole
|
||||
## container down. Worse, that exit is *clean*, so `RestartPolicy` doesn't
|
||||
## reliably catch it — the container just stops and HAProxy serves 503 until
|
||||
## someone manually starts it. (Root-caused on whp02 alsacorp, 2026-06-06.)
|
||||
##
|
||||
## Daemon mode is OLS's native model: it owns the SIGUSR1 handoff, keeps the
|
||||
## listeners bound across generations, and rewrites lshttpd.pid to the new main.
|
||||
## PID 1 just FOLLOWS the pidfile — a graceful self-restart is invisible here
|
||||
## (zero downtime), and we only ever relaunch on a genuine crash (no live main).
|
||||
STOP_REQUESTED=0
|
||||
term_handler() {
|
||||
STOP_REQUESTED=1
|
||||
/usr/local/lsws/bin/lswsctrl stop >/dev/null 2>&1 || true
|
||||
}
|
||||
trap term_handler TERM INT
|
||||
|
||||
## Authoritative, path-independent liveness check: `lswsctrl status` prints
|
||||
## "litespeed is running with PID N." when up (and "...is not running" when
|
||||
## down). We match the running message specifically — a bare grep for "running"
|
||||
## would also match "not running". (This image keeps the pidfile under
|
||||
## /tmp/lshttpd, not logs/, so we never hard-code a pidfile path.)
|
||||
ols_running() { /usr/local/lsws/bin/lswsctrl status 2>/dev/null | grep -qi 'running with pid'; }
|
||||
|
||||
## Crash-loop cap: if OLS can't stay up, bail out so Docker's restart policy and
|
||||
## the site-health monitor escalate instead of us hot-looping forever.
|
||||
MAX_STARTS=5
|
||||
WINDOW=60
|
||||
starts=""
|
||||
|
||||
start_ols() {
|
||||
/usr/local/lsws/bin/lswsctrl start >/dev/null 2>&1 || true
|
||||
## wait up to 10s for the daemon to report running
|
||||
for _ in $(seq 1 20); do
|
||||
ols_running && return 0
|
||||
sleep 0.5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! start_ols; then
|
||||
echo "entrypoint: OLS failed to start (not running after 10s)." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "entrypoint: OLS started in daemon mode — $(/usr/local/lsws/bin/lswsctrl status 2>/dev/null || true)"
|
||||
|
||||
while true; do
|
||||
if ols_running; then
|
||||
sleep 3
|
||||
continue
|
||||
fi
|
||||
|
||||
## Not running this instant. This is EITHER a clean shutdown OR the brief
|
||||
## handoff window of a graceful self-restart (status momentarily reports down
|
||||
## while the new main takes over). Grace, then re-check before judging.
|
||||
sleep 2
|
||||
if [ "$STOP_REQUESTED" -eq 0 ] && ols_running; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$STOP_REQUESTED" -eq 1 ]; then
|
||||
echo "entrypoint: SIGTERM received, OLS stopped — exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
## Genuine crash: not running and no shutdown requested. Relaunch, capped.
|
||||
now=$(date +%s)
|
||||
starts="$starts $now"
|
||||
pruned=""
|
||||
for t in $starts; do
|
||||
[ $((now - t)) -lt "$WINDOW" ] && pruned="$pruned $t"
|
||||
done
|
||||
starts="$pruned"
|
||||
n=$(echo $starts | wc -w)
|
||||
echo "entrypoint: OLS not running — relaunching (attempt $n/$MAX_STARTS within ${WINDOW}s)." >&2
|
||||
if [ "$n" -ge "$MAX_STARTS" ]; then
|
||||
echo "entrypoint: OLS crash-looping ($n starts in ${WINDOW}s) — bailing out for Docker restart policy / monitor." >&2
|
||||
exit 1
|
||||
fi
|
||||
start_ols || true
|
||||
done
|
||||
135
scripts/entrypoint-lsphp.sh
Normal file
135
scripts/entrypoint-lsphp.sh
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/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.
|
||||
##
|
||||
## 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.
|
||||
##
|
||||
## 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.
|
||||
|
||||
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
|
||||
: "${domain:=localhost}"
|
||||
export user domain
|
||||
|
||||
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
|
||||
|
||||
## ---- user + directories (identical to entrypoint-litespeed.sh: docroot at
|
||||
## /home/$user, the customer's bind-mounted domain dir) ----
|
||||
if ! id -u "$user" >/dev/null 2>&1; then
|
||||
useradd -u "$uid" -m -s /bin/bash "$user"
|
||||
fi
|
||||
mkdir -p "/home/$user/public_html" "/home/$user/logs/php-fpm"
|
||||
|
||||
## ---- 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
|
||||
mkdir -p "/mnt/users/$user"
|
||||
ln -sfn "/home/$user" "/mnt/users/$user/$SAFE_DOMAIN"
|
||||
|
||||
## ---- 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}"
|
||||
|
||||
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP_LSAPI_CHILDREN=${PHP_LSAPI_CHILDREN} | LSAPI_MAX_IDLE=${LSAPI_MAX_IDLE} | PHPVER=${PHPVER} | bind=${LSPHP_BIND}"
|
||||
|
||||
## ---- 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.
|
||||
SCAN_DIR=$("$LSPHP_BIN" -i 2>/dev/null | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
|
||||
if [ -n "$SCAN_DIR" ]; then
|
||||
mkdir -p "$SCAN_DIR"
|
||||
cat > "$SCAN_DIR/99-user-error-log.ini" <<EOF
|
||||
; rendered at container start by entrypoint-lsphp.sh
|
||||
error_log = /home/${user}/logs/php-fpm/error.log
|
||||
log_errors = On
|
||||
EOF
|
||||
## Normalise \$_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] from the OLS-sent
|
||||
## /mnt/users path back to /home/<user> so cac-lsphp is byte-for-byte 1:1 with
|
||||
## cac-fpm. Customer sites have no auto_prepend by default, so this is safe; a
|
||||
## site that sets its own .user.ini auto_prepend overrides it (paths still
|
||||
## resolve via the symlink either way).
|
||||
cat > "$SCAN_DIR/99-cac-lsphp-normalize.ini" <<'EOF'
|
||||
; rendered at container start by entrypoint-lsphp.sh
|
||||
auto_prepend_file = /scripts/cac-lsphp-normalize.php
|
||||
EOF
|
||||
## Per-site opcache override (panel: Advanced Tuning → OpCache size); falls
|
||||
## back to the baked lsphp-overrides.ini defaults when unset.
|
||||
if [ -n "${OPCACHE_MEMORY_MB:-}" ] || [ -n "${OPCACHE_MAX_FILES:-}" ]; then
|
||||
{
|
||||
echo "; rendered at container start by entrypoint-lsphp.sh"
|
||||
echo "; per-site override from WHP whp.sites.opcache_*_override"
|
||||
[ -n "${OPCACHE_MEMORY_MB:-}" ] && echo "opcache.memory_consumption = ${OPCACHE_MEMORY_MB}"
|
||||
[ -n "${OPCACHE_MAX_FILES:-}" ] && echo "opcache.max_accelerated_files = ${OPCACHE_MAX_FILES}"
|
||||
} > "$SCAN_DIR/99-user-opcache.ini"
|
||||
fi
|
||||
fi
|
||||
|
||||
## ---- ownership ----
|
||||
## 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
|
||||
|
||||
## ---- 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
|
||||
77
scripts/entrypoint-shared-httpd.sh
Executable file
77
scripts/entrypoint-shared-httpd.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export CONTAINER_ROLE="httpd_only"
|
||||
|
||||
if [ -z "$environment" ]; then
|
||||
environment="PROD"
|
||||
fi
|
||||
|
||||
# Generate self-signed SSL cert if not already present
|
||||
if [ ! -f /etc/pki/tls/certs/localhost.crt ]; then
|
||||
openssl req -newkey rsa:2048 -nodes \
|
||||
-keyout /etc/pki/tls/private/localhost.key \
|
||||
-x509 -days 3650 -subj "/CN=localhost" \
|
||||
-out /etc/pki/tls/certs/localhost.crt
|
||||
fi
|
||||
|
||||
# Create log directory
|
||||
mkdir -p /var/log/httpd
|
||||
|
||||
# Remove default configs that conflict
|
||||
rm -f /etc/httpd/conf.d/userdir.conf
|
||||
|
||||
# Configure RemoteIP for Docker network
|
||||
docker_network=$(ip addr show | grep eth0 | grep inet | awk -F " " '{print $2}')
|
||||
if [ -n "$docker_network" ]; then
|
||||
echo "RemoteIPInternalProxy $docker_network" >> /etc/httpd/conf.d/remoteip.conf
|
||||
fi
|
||||
|
||||
# Detect memory and calculate Apache MPM tuning
|
||||
source /scripts/detect-memory.sh
|
||||
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | Apache workers=${APACHE_MAX_REQUEST_WORKERS} | Role=${CONTAINER_ROLE}"
|
||||
|
||||
# Generate MPM tuning config
|
||||
/scripts/create-apache-mpm-config.sh
|
||||
|
||||
# Write SSL global config (matches standalone CAC behavior)
|
||||
cat <<'EOF' > /etc/httpd/conf.d/ssl-global.conf
|
||||
Listen 443 https
|
||||
SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog
|
||||
SSLSessionCache shmcb:/run/httpd/sslcache(512000)
|
||||
SSLSessionCacheTimeout 300
|
||||
SSLCryptoDevice builtin
|
||||
EOF
|
||||
|
||||
# Disable the default ssl.conf if present (we use per-vhost SSL)
|
||||
if [ -f /etc/httpd/conf.d/ssl.conf ]; then
|
||||
mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.bak
|
||||
fi
|
||||
|
||||
# Ensure vhosts directory exists and is included
|
||||
mkdir -p /etc/httpd/conf.d/vhosts
|
||||
if ! grep -q 'IncludeOptional conf.d/vhosts/' /etc/httpd/conf/httpd.conf; then
|
||||
echo 'IncludeOptional conf.d/vhosts/*.conf' >> /etc/httpd/conf/httpd.conf
|
||||
fi
|
||||
|
||||
# Start Apache
|
||||
/usr/sbin/httpd -k start
|
||||
|
||||
# Start cron for log rotation
|
||||
/usr/sbin/crond
|
||||
|
||||
# Tail Apache logs (becomes PID 1 process)
|
||||
# Use a loop to pick up new log files as vhosts are added.
|
||||
# tail -f only watches files that exist at start time.
|
||||
touch /var/log/httpd/error_log
|
||||
TAIL_PID=""
|
||||
while true; do
|
||||
LOG_FILES=$(find /var/log/httpd/ -name '*.log' -o -name '*_log' 2>/dev/null | sort)
|
||||
if [ -n "$TAIL_PID" ]; then
|
||||
kill "$TAIL_PID" 2>/dev/null
|
||||
wait "$TAIL_PID" 2>/dev/null
|
||||
fi
|
||||
tail -f $LOG_FILES &
|
||||
TAIL_PID=$!
|
||||
# Re-check for new log files every 60 seconds
|
||||
sleep 60
|
||||
done
|
||||
126
scripts/entrypoint-shared-ols.sh
Normal file
126
scripts/entrypoint-shared-ols.sh
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
## entrypoint-shared-ols.sh — PID 1 for the shared-ols tier.
|
||||
##
|
||||
## One OpenLiteSpeed container fronting MANY tenants' detached cac-lsphp
|
||||
## sidecars (the OLS analogue of the shared-httpd container). Webserver ONLY —
|
||||
## it runs NO PHP locally (render-shared-ols-config.sh strips the stock local
|
||||
## lsphp; every site's PHP goes to its own sidecar over LSAPI). HAProxy stays
|
||||
## the TLS/WAF/SNI edge and routes OLS-type hostnames here on :443.
|
||||
##
|
||||
## Reuses cac-litespeed's hard-won DAEMON-MODE supervision (NOT `openlitespeed
|
||||
## -n` + wait): OLS self-restarts on QUIC.cloud IP refresh would otherwise exit
|
||||
## PID 1 cleanly and tear the container down. See entrypoint-litespeed.sh and
|
||||
## feedback_ols_quiccloud_restart_kills_container.
|
||||
set -euo pipefail
|
||||
|
||||
: "${environment:=PROD}"
|
||||
export CONTAINER_ROLE="shared_ols"
|
||||
|
||||
LSWS_CONF=/usr/local/lsws/conf
|
||||
CERT_DIR="$LSWS_CONF/cert"
|
||||
HEALTH_DIR=/usr/local/lsws/shared-ols-health
|
||||
export SITES_ROOT="${SITES_ROOT:-$LSWS_CONF/shared-sites}"
|
||||
export LSCACHE_ROOT="${LSCACHE_ROOT:-/var/lscache}"
|
||||
export CERT_FILE="$CERT_DIR/shared-ols.crt"
|
||||
export KEY_FILE="$CERT_DIR/shared-ols.key"
|
||||
|
||||
mkdir -p "$SITES_ROOT" "$LSCACHE_ROOT" "$CERT_DIR" "$HEALTH_DIR/html"
|
||||
|
||||
## ---- self-signed cert for the :443 listener (HAProxy verifies none) ----
|
||||
if [ ! -f "$CERT_FILE" ]; then
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
||||
-keyout "$KEY_FILE" -out "$CERT_FILE" -subj "/CN=shared-ols" 2>/dev/null
|
||||
fi
|
||||
|
||||
## ---- health vhost (catch-all): valid server with zero customer sites +
|
||||
## answers HAProxy health checks that hit by IP / unknown Host with a 200 ----
|
||||
cat > "$HEALTH_DIR/vhconf.conf" <<'EOF'
|
||||
docRoot $VH_ROOT/html
|
||||
enableScript 0
|
||||
context / {
|
||||
allowBrowse 1
|
||||
location $DOC_ROOT/
|
||||
}
|
||||
EOF
|
||||
printf 'ok\n' > "$HEALTH_DIR/html/healthz"
|
||||
printf 'shared-ols\n' > "$HEALTH_DIR/html/index.html"
|
||||
|
||||
## ---- ownership: OLS reads conf/ as lsadm. chown the base conf dir + health dir
|
||||
## NON-recursively (the per-site files under conf/shared-sites are written by the
|
||||
## panel and are world-readable; a recursive chown here would be O(N-sites) on
|
||||
## every container (re)start, delaying first-listen after a crash). The render
|
||||
## script chowns the httpd_config.conf it produces. ----
|
||||
chown lsadm:nogroup "$LSWS_CONF" "$HEALTH_DIR" "$HEALTH_DIR/html" 2>/dev/null || true
|
||||
chown lsadm:nogroup "$HEALTH_DIR/vhconf.conf" "$HEALTH_DIR/html/healthz" "$HEALTH_DIR/html/index.html" 2>/dev/null || true
|
||||
|
||||
## ---- assemble httpd_config.conf from the panel's per-site files ----
|
||||
/scripts/render-shared-ols-config.sh
|
||||
|
||||
## ---- stream OLS logs to PID-1 stdout (follows across restarts) ----
|
||||
mkdir -p /usr/local/lsws/logs
|
||||
touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log
|
||||
tail -F /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log 2>/dev/null &
|
||||
|
||||
## ---- .htaccess watcher (required; spec 5.3). Background; the panel monitors
|
||||
## that it stays alive (its death silently stops rewrite changes applying). ----
|
||||
/scripts/ols-htaccess-watcher.sh &
|
||||
WATCHER_PID=$!
|
||||
|
||||
## ---- supervise OLS in DAEMON mode (verbatim model from entrypoint-litespeed.sh) ----
|
||||
STOP_REQUESTED=0
|
||||
term_handler() {
|
||||
STOP_REQUESTED=1
|
||||
kill "$WATCHER_PID" 2>/dev/null || true
|
||||
/usr/local/lsws/bin/lswsctrl stop >/dev/null 2>&1 || true
|
||||
}
|
||||
trap term_handler TERM INT
|
||||
|
||||
ols_running() { /usr/local/lsws/bin/lswsctrl status 2>/dev/null | grep -qi 'running with pid'; }
|
||||
|
||||
MAX_STARTS=5
|
||||
WINDOW=60
|
||||
starts=""
|
||||
|
||||
start_ols() {
|
||||
/usr/local/lsws/bin/lswsctrl start >/dev/null 2>&1 || true
|
||||
for _ in $(seq 1 20); do
|
||||
ols_running && return 0
|
||||
sleep 0.5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! start_ols; then
|
||||
echo "entrypoint-shared-ols: OLS failed to start (not running after 10s)." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "entrypoint-shared-ols: OLS started in daemon mode — $(/usr/local/lsws/bin/lswsctrl status 2>/dev/null || true)"
|
||||
|
||||
while true; do
|
||||
if ols_running; then
|
||||
sleep 3
|
||||
continue
|
||||
fi
|
||||
sleep 2
|
||||
if [ "$STOP_REQUESTED" -eq 0 ] && ols_running; then
|
||||
continue
|
||||
fi
|
||||
if [ "$STOP_REQUESTED" -eq 1 ]; then
|
||||
echo "entrypoint-shared-ols: SIGTERM received, OLS stopped — exiting."
|
||||
exit 0
|
||||
fi
|
||||
now=$(date +%s)
|
||||
starts="$starts $now"
|
||||
pruned=""
|
||||
for t in $starts; do
|
||||
[ $((now - t)) -lt "$WINDOW" ] && pruned="$pruned $t"
|
||||
done
|
||||
starts="$pruned"
|
||||
n=$(echo $starts | wc -w)
|
||||
echo "entrypoint-shared-ols: OLS not running — relaunching (attempt $n/$MAX_STARTS within ${WINDOW}s)." >&2
|
||||
if [ "$n" -ge "$MAX_STARTS" ]; then
|
||||
echo "entrypoint-shared-ols: OLS crash-looping — bailing for Docker restart policy / monitor." >&2
|
||||
exit 1
|
||||
fi
|
||||
start_ols || true
|
||||
done
|
||||
@@ -1,29 +1,56 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$PHPVER" ]; then
|
||||
PHPVER="81";
|
||||
PHPVER="83";
|
||||
fi
|
||||
|
||||
if [ -z "$environment" ]; then
|
||||
environment="PROD"
|
||||
fi
|
||||
|
||||
adduser -u $uid $user
|
||||
|
||||
mkdir -p /home/$user/public_html
|
||||
mkdir -p /home/$user/logs/{apache,php-fpm}
|
||||
|
||||
chown -R $user:$user /home/$user
|
||||
chmod -R 755 /home/$user
|
||||
mv /var/log/httpd /var/log/httpd.bak
|
||||
|
||||
/scripts/install-php$PHPVER.sh
|
||||
ln -s /home/$user/logs/apache /var/log/httpd
|
||||
ln -s /home/$user/logs/php-fpm /var/log/php-fpm
|
||||
|
||||
rm -f /etc/httpd/conf.d/userdir.conf
|
||||
docker_network=$(ip addr show |grep eth0 |grep inet |awk -F " " {'print $2'})
|
||||
echo "RemoteIPInternalProxy $docker_network" >> /etc/httpd/conf.d/remoteip.conf
|
||||
# /scripts/install-php$PHPVER.sh
|
||||
|
||||
source /scripts/detect-memory.sh
|
||||
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP-FPM pm=${PHP_FPM_PM} max_children=${PHP_FPM_MAX_CHILDREN} | Apache workers=${APACHE_MAX_REQUEST_WORKERS}"
|
||||
|
||||
/scripts/create-vhost.sh
|
||||
/scripts/create-php-config.sh
|
||||
/scripts/create-apache-mpm-config.sh
|
||||
|
||||
if [ -f /etc/httpd/conf.d/ssl.conf ]; then
|
||||
mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.bak
|
||||
fi
|
||||
|
||||
/usr/sbin/httpd -k start
|
||||
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
||||
chown -R $user:$user /home/$user
|
||||
chmod -R 755 /home/$user
|
||||
|
||||
if [[ $environment == 'DEV' ]]; then
|
||||
echo "Starting Dev Deployment"
|
||||
dnf install -y MariaDB-server MariaDB-client memcached
|
||||
mkdir -p /home/$user/_db_backups
|
||||
# Ensure microdnf is available for installing MariaDB and memcached in DEV mode
|
||||
if ! command -v microdnf &> /dev/null; then
|
||||
echo "microdnf not found, installing with dnf..."
|
||||
dnf install -y microdnf && dnf clean all
|
||||
fi
|
||||
microdnf install -y MariaDB-server MariaDB-client memcached
|
||||
sed -r -i 's/session.save_path="memcache:11211/session.save_path="localhost:11211/' /etc/php.ini
|
||||
nohup mysqld -umysql &
|
||||
if [ ! -f /var/lib/mysql/creds ]; then
|
||||
if [ ! -f /home/$user/mysql_creds ]; then
|
||||
echo "Give MySQL a chance to finish starting..."
|
||||
sleep 10
|
||||
mysql_user=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13 ; echo '')
|
||||
@@ -33,14 +60,37 @@ if [[ $environment == 'DEV' ]]; then
|
||||
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 > /var/lib/mysql/creds
|
||||
echo "MySQL Password: "$mysql_password >> /var/lib/mysql/creds
|
||||
echo "MySQL Database: devdb_"$mysql_db >> /var/lib/mysql/creds
|
||||
cat /var/lib/mysql/creds
|
||||
# Create user crontab with MySQL backup job
|
||||
echo "# User crontab for $user" > /home/$user/crontab
|
||||
echo "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" >> /home/$user/crontab
|
||||
chown $user:$user /home/$user/crontab
|
||||
echo "MySQL User: "$mysql_user > /home/$user/mysql_creds
|
||||
echo "MySQL Password: "$mysql_password >> /home/$user/mysql_creds
|
||||
echo "MySQL Database: devdb_"$mysql_db >> /home/$user/mysql_creds
|
||||
cat /home/$user/mysql_creds
|
||||
|
||||
fi
|
||||
/usr/bin/memcached -d -u $user
|
||||
|
||||
fi
|
||||
tail -f /etc/httpd/logs/*
|
||||
|
||||
if [[ $environment == 'PROD' ]]; then
|
||||
sed -r -i 's/;session.save_path="localhost:11211/session.save_path="memcache:11211/' /etc/php.d/50-memcached.ini
|
||||
fi
|
||||
|
||||
# Set up user crontab
|
||||
if [ ! -f /home/$user/crontab ]; then
|
||||
echo "# User crontab for $user" > /home/$user/crontab
|
||||
echo "# Add your cron jobs here" >> /home/$user/crontab
|
||||
echo "# Example: */5 * * * * /home/$user/scripts/my-script.sh" >> /home/$user/crontab
|
||||
chown $user:$user /home/$user/crontab
|
||||
fi
|
||||
|
||||
# Load user crontab
|
||||
crontab -u $user /home/$user/crontab
|
||||
|
||||
/usr/sbin/crond
|
||||
tail -f /var/log/httpd/*
|
||||
|
||||
exit 0
|
||||
|
||||
|
||||
|
||||
17
scripts/healthcheck-lsphp.sh
Normal file
17
scripts/healthcheck-lsphp.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
## healthcheck-lsphp.sh — liveness for the detached-lsphp sidecar.
|
||||
##
|
||||
## LSAPI is not FastCGI, so the cac-fpm `cgi-fcgi ... | grep pong` ping doesn't
|
||||
## apply here. Minimum viable check (spec §5.1 fallback): the LSAPI listener is
|
||||
## accepting TCP connections on :9000 AND at least one lsphp process is alive.
|
||||
## A bound-but-wedged listener with no lsphp would fail the pgrep; a crashed
|
||||
## listener fails the connect.
|
||||
|
||||
PORT="${LSPHP_HEALTH_PORT:-9000}"
|
||||
|
||||
# bash /dev/tcp connect test (bash is present on the litespeedtech base).
|
||||
exec 3<>"/dev/tcp/127.0.0.1/${PORT}" 2>/dev/null || exit 1
|
||||
exec 3>&- 3<&-
|
||||
|
||||
pgrep -x lsphp >/dev/null 2>&1 || exit 1
|
||||
exit 0
|
||||
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."
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
dnf module enable php:remi-7.4 -y
|
||||
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-xmlrpc \
|
||||
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
|
||||
php-mysqlnd php-mbstring php-ioncube-loader php-intl php-gd libzip php-cli
|
||||
php-mysqlnd php-mbstring php-ioncube-loader php-intl php-gd php-pgsql libzip php-cli
|
||||
exit 0
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
dnf module enable php:remi-8.0 -y
|
||||
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
|
||||
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
|
||||
php-mysqlnd php-mbstring php-ioncube-loader php-intl php-gd libzip php-cli
|
||||
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
|
||||
exit 0
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
dnf module enable php:remi-8.1 -y
|
||||
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
|
||||
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
|
||||
php-mysqlnd php-mbstring php-ioncube-loader php-intl php-gd libzip php-cli
|
||||
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
|
||||
exit 0
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
dnf module enable php:remi-8.2 -y
|
||||
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
|
||||
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
|
||||
php-mysqlnd php-mbstring php-intl php-gd libzip php-cli
|
||||
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
|
||||
exit 0
|
||||
6
scripts/install-php83.sh
Normal file
6
scripts/install-php83.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
dnf module enable php:remi-8.3 -y
|
||||
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
|
||||
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
|
||||
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
|
||||
exit 0
|
||||
6
scripts/install-php84.sh
Normal file
6
scripts/install-php84.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
dnf module enable php:remi-8.4 -y
|
||||
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
|
||||
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
|
||||
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
|
||||
exit 0
|
||||
6
scripts/install-php85.sh
Normal file
6
scripts/install-php85.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
dnf module enable php:remi-8.5 -y
|
||||
dnf install -y php php-fpm php-mysqlnd php-xml php-pecl-zip php-sodium php-soap php-pecl-xmlrpc \
|
||||
php-pecl-redis5 php-pecl-memcached php-pecl-memcache php-pecl-ip2location php-pecl-imagick php-pecl-geoip \
|
||||
php-mysqlnd php-mbstring php-intl php-gd php-pgsql libzip php-cli
|
||||
exit 0
|
||||
26
scripts/log-rotate.sh
Normal file
26
scripts/log-rotate.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Set the log directory
|
||||
LOG_DIR="/var/log/httpd"
|
||||
|
||||
# Get current date
|
||||
DATE=$(date +%Y%m%d)
|
||||
|
||||
# Rotate access log
|
||||
if [ -f "$LOG_DIR/access_log" ]; then
|
||||
cp "$LOG_DIR/access_log" "$LOG_DIR/access_log.$DATE"
|
||||
cat /dev/null > "$LOG_DIR/access_log"
|
||||
fi
|
||||
|
||||
# Rotate error log
|
||||
if [ -f "$LOG_DIR/error_log" ]; then
|
||||
cp "$LOG_DIR/error_log" "$LOG_DIR/error_log.$DATE"
|
||||
cat /dev/null > "$LOG_DIR/error_log"
|
||||
fi
|
||||
|
||||
# Compress logs older than 3 days
|
||||
find "$LOG_DIR" -name "*.log.*" -type f -mtime +3 -exec gzip {} \;
|
||||
|
||||
# Delete logs older than 7 days
|
||||
find "$LOG_DIR" -name "*.log.*" -type f -mtime +7 -delete
|
||||
|
||||
14
scripts/mysql-backup.sh
Normal file
14
scripts/mysql-backup.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
user=$1
|
||||
mysql_db=$2
|
||||
dt=$(date +%y%m%d-%T)
|
||||
if [ ! -d /home/$user/_db_backups ]; then
|
||||
mkdir -p /home/$user/_db_backups
|
||||
fi
|
||||
|
||||
/usr/bin/mysqldump $mysql_db > /home/$user/_db_backups/$mysql_db.$dt.sql
|
||||
chown -R $user:$user /home/$user/_db_backups
|
||||
/usr/bin/find /home/$user/_db_backups/ -type f -mmin +360 -delete
|
||||
|
||||
exit 0
|
||||
73
scripts/ols-htaccess-watcher.sh
Normal file
73
scripts/ols-htaccess-watcher.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
## ols-htaccess-watcher.sh — graceful-restart the shared OLS when any tenant's
|
||||
## .htaccess changes. OLS reads .htaccess (RewriteFile) at (re)start, NOT per
|
||||
## request, so without this a WordPress permalink/LiteSpeed-Cache change would
|
||||
## silently not take effect. Required by spec 5.3.
|
||||
##
|
||||
## Watches all docroots for .htaccess writes, COALESCES bursts (a WP plugin save
|
||||
## touches the file several times) within a debounce window, and RATE-LIMITS to
|
||||
## a floor (one restart per FLOOR seconds) so many tenants saving at once can't
|
||||
## trigger a restart storm. Debounce/floor are env-tunable (panel discloses the
|
||||
## resulting "~60s" window to customers).
|
||||
##
|
||||
## Failure of THIS process is the silent-ticket failure mode (spec 7): if it
|
||||
## dies, tenants' rewrite changes stop applying with no error. The entrypoint
|
||||
## runs it and the panel monitors it (check-ols-htaccess-watcher.php).
|
||||
set -uo pipefail
|
||||
|
||||
WATCH_ROOT="${OLS_WATCH_ROOT:-/mnt/users}"
|
||||
DEBOUNCE="${OLS_HTACCESS_DEBOUNCE:-15}" # coalesce window (s)
|
||||
FLOOR="${OLS_HTACCESS_FLOOR:-60}" # min seconds between restarts
|
||||
LSWSCTRL=/usr/local/lsws/bin/lswsctrl
|
||||
last_restart=0
|
||||
|
||||
log() { echo "ols-htaccess-watcher: $*" >&2; }
|
||||
|
||||
do_restart() {
|
||||
now=$(date +%s)
|
||||
if [ $((now - last_restart)) -lt "$FLOOR" ]; then
|
||||
log "within ${FLOOR}s floor — coalescing, skipping restart"
|
||||
return
|
||||
fi
|
||||
if "$LSWSCTRL" restart >/dev/null 2>&1; then
|
||||
last_restart=$now
|
||||
log "graceful restart issued (.htaccess change)"
|
||||
else
|
||||
log "WARNING: lswsctrl restart failed"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! command -v inotifywait >/dev/null 2>&1; then
|
||||
log "FATAL: inotifywait not installed (inotify-tools)"; exit 1
|
||||
fi
|
||||
mkdir -p "$WATCH_ROOT"
|
||||
log "watching $WATCH_ROOT for .htaccess changes (debounce=${DEBOUNCE}s floor=${FLOOR}s)"
|
||||
|
||||
## -m monitor, -r recursive. We filter to .htaccess in the read loop rather than
|
||||
## --include so this works on older inotify-tools too. modify/create/delete/move
|
||||
## all matter (delete of .htaccess also changes rewrite behavior).
|
||||
inotifywait -m -r -e modify,create,delete,move "$WATCH_ROOT" --format '%f' 2>/dev/null |
|
||||
while read -r fname; do
|
||||
case "$fname" in
|
||||
.htaccess) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
## A tenant .htaccess changed. Coalesce the save-burst, then restart ONCE.
|
||||
##
|
||||
## The coalesce is HARD-BOUNDED to DEBOUNCE seconds: a previous version blocked
|
||||
## on `read -t DEBOUNCE` which, on a busy multi-tenant server, never timed out
|
||||
## (unrelated file writes under $WATCH_ROOT kept resetting it) — so the restart
|
||||
## was starved and rewrite changes silently never applied. Here we read further
|
||||
## events only until the deadline OR ~2s of total quiet, whichever comes first,
|
||||
## so continuous activity can delay us by at most DEBOUNCE. do_restart's FLOOR
|
||||
## then rate-limits across consecutive bursts.
|
||||
deadline=$(( $(date +%s) + DEBOUNCE ))
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
if read -r -t 2 _; then
|
||||
continue # more activity — keep coalescing toward the deadline
|
||||
else
|
||||
break # ~2s of total quiet — the burst has settled
|
||||
fi
|
||||
done
|
||||
do_restart
|
||||
done
|
||||
160
scripts/render-shared-ols-config.sh
Normal file
160
scripts/render-shared-ols-config.sh
Normal file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
## render-shared-ols-config.sh — assemble httpd_config.conf for the shared-ols
|
||||
## tier from the per-site files the WHP panel drops into $SITES_ROOT.
|
||||
##
|
||||
## WHY THIS EXISTS: OpenLiteSpeed has NO top-level `include` directive (unlike
|
||||
## Apache's IncludeOptional that shared-httpd relies on). So we cannot just drop
|
||||
## per-vhost files in a dir and have OLS pick them up — the listener `map` lines
|
||||
## and the vhost stanzas must live IN httpd_config.conf. This script is the
|
||||
## "include" OLS lacks: it concatenates the panel's per-site pieces into one
|
||||
## valid httpd_config.conf, then the caller issues `lswsctrl restart`.
|
||||
## (Empirically established 2026-06-10 — see the OLS-tier PoC.)
|
||||
##
|
||||
## Per-site contract — the panel writes, for each site, a directory:
|
||||
## $SITES_ROOT/<vhname>/vhconf.conf (rendered by the WHP panel from its own
|
||||
## web-files/configs/shared-ols-vhconf-template.tpl
|
||||
## — the single source of truth for vhost detail)
|
||||
## $SITES_ROOT/<vhname>/site.meta (VHNAME=, VHROOT=, DOMAINS=a.com,www.a.com)
|
||||
## This script turns each into a `virtualhost {configFile}` stanza + a listener
|
||||
## `map` line. A site dir missing either file is skipped (logged).
|
||||
##
|
||||
## Idempotent: always rebuilds from the stock config, so re-runs never compound.
|
||||
set -euo pipefail
|
||||
|
||||
LSWS_CONF=/usr/local/lsws/conf
|
||||
TPL_DIR=${TPL_DIR:-/etc/shared-ols-templates}
|
||||
SITES_ROOT=${SITES_ROOT:-$LSWS_CONF/shared-sites}
|
||||
LSCACHE_ROOT=${LSCACHE_ROOT:-/var/lscache}
|
||||
CERT_FILE=${CERT_FILE:-$LSWS_CONF/cert/shared-ols.crt}
|
||||
KEY_FILE=${KEY_FILE:-$LSWS_CONF/cert/shared-ols.key}
|
||||
export LSCACHE_ROOT
|
||||
|
||||
OUT="$LSWS_CONF/httpd_config.conf"
|
||||
TMP="$LSWS_CONF/.httpd_config.conf.tmp.$$"
|
||||
STOCK="/usr/local/lsws/.conf/httpd_config.conf"
|
||||
|
||||
mkdir -p "$SITES_ROOT" "$LSCACHE_ROOT"
|
||||
|
||||
## --- SERIALIZE concurrent renders + write ATOMICALLY ---
|
||||
## The panel can fire two renders at once (parallel provisioning), and the
|
||||
## in-container .htaccess watcher issues `lswsctrl restart` independently. If OLS
|
||||
## (re)reads httpd_config.conf while it's half-written, it fails to parse and the
|
||||
## whole tier 503s. So: (1) flock so only one render runs at a time; (2) build
|
||||
## into $TMP and atomically `mv` into place at the end, so any concurrent OLS
|
||||
## restart always sees a COMPLETE config (the old one until the instant of mv).
|
||||
exec 9>"$LSWS_CONF/.render.lock"
|
||||
## Bounded wait (-w): if a previous render is hung, fail after 30s rather than
|
||||
## blocking the panel's `docker exec` call (and thus the site-save request)
|
||||
## indefinitely. The caller re-tries on the next change.
|
||||
flock -w 30 9 || { echo "render-shared-ols: could not acquire render lock within 30s" >&2; exit 1; }
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
## Sweep any stale temp configs left by a prior SIGKILL (trap EXIT doesn't run on
|
||||
## SIGKILL); each render uses a unique $$ suffix so this never races a live render.
|
||||
rm -f "$LSWS_CONF"/.httpd_config.conf.tmp.* 2>/dev/null || true
|
||||
## From here on, build into $TMP (not $OUT).
|
||||
|
||||
## --- 1. start from a pristine stock config (idempotent) ---
|
||||
if [ ! -f "$STOCK" ]; then
|
||||
## Some image builds keep the only copy at conf/; snapshot it once so future
|
||||
## renders have a clean base to strip.
|
||||
mkdir -p "$(dirname "$STOCK")"
|
||||
cp "$OUT" "$STOCK"
|
||||
fi
|
||||
|
||||
## --- 2. strip stock blocks that conflict or would run PHP LOCALLY ---
|
||||
## extProcessor lsphp (autoStart 1, uds) + the server scriptHandler are removed
|
||||
## so this server NEVER executes PHP itself — all PHP goes to remote sidecars.
|
||||
## listener HTTP/HTTPS + vhTemplate docker are removed (we add our own).
|
||||
awk '
|
||||
/^listener HTTP \{/ { skip=1; next }
|
||||
/^listener HTTPS \{/ { skip=1; next }
|
||||
/^vhTemplate docker ?\{/ { skip=1; next }
|
||||
/^extProcessor lsphp ?\{/{ skip=1; next }
|
||||
/^scriptHandler ?\{/ { skip=1; next }
|
||||
skip && /^\}/ { skip=0; next }
|
||||
!skip { print }
|
||||
' "$STOCK" > "$TMP"
|
||||
|
||||
## --- 3. append our server-level base (real-IP, cache module, no local PHP) ---
|
||||
{
|
||||
echo ""
|
||||
envsubst '${LSCACHE_ROOT}' < "$TPL_DIR/httpd_config_base.tpl"
|
||||
} >> "$TMP"
|
||||
|
||||
## --- 4. emit per-site vhost stanzas + collect listener map lines ---
|
||||
maps=""
|
||||
site_count=0
|
||||
for meta in "$SITES_ROOT"/*/site.meta; do
|
||||
[ -e "$meta" ] || continue
|
||||
sdir=$(dirname "$meta")
|
||||
## PARSE site.meta with sed — do NOT `source` it. The panel writes these values
|
||||
## (derived from DB domains), so they should be safe, but sourcing paneldata as
|
||||
## shell would execute any metacharacters as root in this container if a value
|
||||
## ever slipped validation. sed extraction treats them as plain data.
|
||||
VHNAME=$(sed -n 's/^VHNAME=//p' "$meta" | head -1)
|
||||
VHROOT=$(sed -n 's/^VHROOT=//p' "$meta" | head -1)
|
||||
DOMAINS=$(sed -n 's/^DOMAINS=//p' "$meta" | head -1)
|
||||
if [ -z "$VHNAME" ] || [ -z "$VHROOT" ] || [ -z "$DOMAINS" ] || [ ! -f "$sdir/vhconf.conf" ]; then
|
||||
echo "render-shared-ols: skipping $sdir (incomplete: VHNAME/VHROOT/DOMAINS/vhconf.conf)" >&2
|
||||
continue
|
||||
fi
|
||||
{
|
||||
echo ""
|
||||
echo "virtualhost ${VHNAME} {"
|
||||
echo " vhRoot ${VHROOT}"
|
||||
echo " configFile ${sdir}/vhconf.conf"
|
||||
echo " allowSymbolLink 1"
|
||||
echo " enableScript 1"
|
||||
echo " restrained 1"
|
||||
echo "}"
|
||||
} >> "$TMP"
|
||||
maps="${maps} map ${VHNAME} ${DOMAINS}"$'\n'
|
||||
site_count=$((site_count + 1))
|
||||
done
|
||||
|
||||
## --- 5. ALWAYS add a health vhost mapped to the catch-all so the server is
|
||||
## valid with zero customer sites and HAProxy health checks (which hit by IP /
|
||||
## unknown Host) get a 200. Exact-domain maps above win over this '*'. ---
|
||||
{
|
||||
echo ""
|
||||
echo "virtualhost _health {"
|
||||
echo " vhRoot /usr/local/lsws/shared-ols-health"
|
||||
echo " configFile /usr/local/lsws/shared-ols-health/vhconf.conf"
|
||||
echo " allowSymbolLink 1"
|
||||
echo " enableScript 0"
|
||||
echo "}"
|
||||
} >> "$TMP"
|
||||
maps="${maps} map _health *"$'\n'
|
||||
|
||||
## --- 6. listeners (HTTP :80 + HTTPS :443 self-signed) carrying ALL maps.
|
||||
## HAProxy terminates real TLS and connects to this tier on :443 ssl verify
|
||||
## none (same as shared-httpd), so :443 needs a cert — self-signed is fine. ---
|
||||
{
|
||||
echo ""
|
||||
echo "listener shared_http {"
|
||||
echo " address *:80"
|
||||
echo " secure 0"
|
||||
printf '%s' "$maps"
|
||||
echo "}"
|
||||
echo ""
|
||||
echo "listener shared_https {"
|
||||
echo " address *:443"
|
||||
echo " secure 1"
|
||||
echo " keyFile ${KEY_FILE}"
|
||||
echo " certFile ${CERT_FILE}"
|
||||
printf '%s' "$maps"
|
||||
echo "}"
|
||||
} >> "$TMP"
|
||||
|
||||
## --- 7. publish atomically. Validate the temp parses as non-empty, then mv into
|
||||
## place (rename is atomic on the same filesystem) so a concurrent OLS restart
|
||||
## never sees a half-written config. chown only the file we wrote — NOT a
|
||||
## recursive chown of the whole conf tree (that was O(N-sites) on every single
|
||||
## change; the per-site files are world-readable and owned correctly already). ---
|
||||
if [ ! -s "$TMP" ]; then
|
||||
echo "render-shared-ols: refusing to publish empty config" >&2
|
||||
exit 1
|
||||
fi
|
||||
chown lsadm:nogroup "$TMP" 2>/dev/null || true
|
||||
mv -f "$TMP" "$OUT"
|
||||
echo "render-shared-ols: wrote $OUT ($site_count customer vhost(s) + health)"
|
||||
79
scripts/tune-mpm.sh
Executable file
79
scripts/tune-mpm.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hot-adjust Apache MPM Event settings and graceful reload.
|
||||
# Usage: tune-mpm.sh [--max-workers N] [--server-limit N] [--start-servers N]
|
||||
# [--min-spare-threads N] [--max-spare-threads N]
|
||||
# [--max-connections-per-child N]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Read current values from the config as defaults
|
||||
CONFIG_FILE="/etc/httpd/conf.d/mpm-tuning.conf"
|
||||
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "Error: $CONFIG_FILE not found. Run detect-memory.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse current values from config
|
||||
current_start=$(grep -oP 'StartServers\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "1")
|
||||
current_min_spare=$(grep -oP 'MinSpareThreads\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "5")
|
||||
current_max_spare=$(grep -oP 'MaxSpareThreads\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "15")
|
||||
current_max_workers=$(grep -oP 'MaxRequestWorkers\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "50")
|
||||
current_server_limit=$(grep -oP 'ServerLimit\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "2")
|
||||
current_max_conn=$(grep -oP 'MaxConnectionsPerChild\s+\K\d+' "$CONFIG_FILE" 2>/dev/null || echo "3000")
|
||||
|
||||
# Parse arguments
|
||||
START_SERVERS=$current_start
|
||||
MIN_SPARE_THREADS=$current_min_spare
|
||||
MAX_SPARE_THREADS=$current_max_spare
|
||||
MAX_REQUEST_WORKERS=$current_max_workers
|
||||
SERVER_LIMIT=$current_server_limit
|
||||
MAX_CONNECTIONS_PER_CHILD=$current_max_conn
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--max-workers) MAX_REQUEST_WORKERS="$2"; shift 2 ;;
|
||||
--server-limit) SERVER_LIMIT="$2"; shift 2 ;;
|
||||
--start-servers) START_SERVERS="$2"; shift 2 ;;
|
||||
--min-spare-threads) MIN_SPARE_THREADS="$2"; shift 2 ;;
|
||||
--max-spare-threads) MAX_SPARE_THREADS="$2"; shift 2 ;;
|
||||
--max-connections-per-child) MAX_CONNECTIONS_PER_CHILD="$2"; shift 2 ;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--max-workers N] [--server-limit N] [--start-servers N]"
|
||||
echo " [--min-spare-threads N] [--max-spare-threads N]"
|
||||
echo " [--max-connections-per-child N]"
|
||||
echo ""
|
||||
echo "Current values:"
|
||||
echo " StartServers: $current_start"
|
||||
echo " MinSpareThreads: $current_min_spare"
|
||||
echo " MaxSpareThreads: $current_max_spare"
|
||||
echo " MaxRequestWorkers: $current_max_workers"
|
||||
echo " ServerLimit: $current_server_limit"
|
||||
echo " MaxConnectionsPerChild: $current_max_conn"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Write updated config
|
||||
cat <<EOF > "$CONFIG_FILE"
|
||||
<IfModule mpm_event_module>
|
||||
StartServers ${START_SERVERS}
|
||||
MinSpareThreads ${MIN_SPARE_THREADS}
|
||||
MaxSpareThreads ${MAX_SPARE_THREADS}
|
||||
ThreadLimit 64
|
||||
ThreadsPerChild 25
|
||||
MaxRequestWorkers ${MAX_REQUEST_WORKERS}
|
||||
ServerLimit ${SERVER_LIMIT}
|
||||
MaxConnectionsPerChild ${MAX_CONNECTIONS_PER_CHILD}
|
||||
</IfModule>
|
||||
EOF
|
||||
|
||||
echo "MPM config updated:"
|
||||
echo " StartServers=$START_SERVERS ServerLimit=$SERVER_LIMIT MaxRequestWorkers=$MAX_REQUEST_WORKERS"
|
||||
echo " MinSpareThreads=$MIN_SPARE_THREADS MaxSpareThreads=$MAX_SPARE_THREADS MaxConnectionsPerChild=$MAX_CONNECTIONS_PER_CHILD"
|
||||
|
||||
# Graceful reload
|
||||
/usr/sbin/httpd -k graceful
|
||||
echo "Apache graceful reload triggered."
|
||||
Reference in New Issue
Block a user