Opcache:
- memory_consumption: 128MB → 64MB (most WordPress sites use <40MB)
- max_accelerated_files: 10000 → 4000 (sufficient for WordPress)
- revalidate_freq: 2s → 60s (reduce stat() calls in production)
- enable_cli: Off (don't cache scripts run from command line)
FPM workers:
- process_idle_timeout: 10s → 5s (faster worker teardown when idle)
- max_requests: 500 → 200 (recycle workers sooner to release leaked memory)
These changes primarily reduce the baseline memory of idle containers
where opcache was reserving 128MB even for small sites.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added ImageMagick-heic package to both Dockerfile and Dockerfile.fpm.
This is a separate EPEL subpackage that provides HEIC, HEIF, and AVIF
format support via libheif. Without it, ImageMagick is installed but
cannot process iPhone photos and modern image formats.
Also fixed MariaDB repo URL: AlmaLinux 10 uses $releasever=10 but
MariaDB mirrors don't have an 'almalinux10' directory. Changed to
'rhel10' which is the supported path for EL10 derivatives.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts from ProxyPassMatch back to SetHandler + ProxyFCGISetEnvIf.
ProxyPassMatch couldn't override DOCUMENT_ROOT (Apache sets it as a
CGI param after all directives run). SetHandler with unconditional
ProxyFCGISetEnvIf correctly overrides both:
- DOCUMENT_ROOT: set to /home/{user}/public_html (FPM path)
- SCRIPT_FILENAME: constructed from DOCUMENT_ROOT + SCRIPT_NAME
This fixes WordFence WAF and other plugins that use DOCUMENT_ROOT to
locate config/log files. Tested on live sites with WordPress pretty
URLs, wp-admin, static assets, and WordFence WAF optimization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WordPress plugins like WordFence use $_SERVER['DOCUMENT_ROOT'] to locate
config/log files. With ProxyPassMatch, Apache sends its own mount path
(/mnt/users/...) as DOCUMENT_ROOT, which doesn't exist in the FPM
container.
ProxyFCGISetEnvIf can't override DOCUMENT_ROOT when using ProxyPassMatch
(Apache sets it after the directive evaluates). Instead, set it via the
FPM pool config's env[] directive which takes precedence.
create-php-config.sh now adds env[DOCUMENT_ROOT] = /home/$user/public_html
when in TCP listen mode (shared httpd), giving PHP the correct path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SetHandler + ProxyFCGISetEnvIf doesn't work for path remapping because
reqenv('SCRIPT_FILENAME') is empty when the directive evaluates with
the SetHandler approach.
ProxyPassMatch directly maps .php URLs to the FPM container's filesystem
path, bypassing the SCRIPT_FILENAME rewrite issue entirely:
^/(.*\.php(/.*)?)$ -> fcgi://fpm:9000/home/{user}/public_html/$1
Static assets (CSS, JS, images) bypass the proxy since they don't match
\.php and are served directly by Apache from the read-only mount.
Tested and confirmed working on live site with WordPress (including
pretty URLs via .htaccess mod_rewrite).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous expr= with s|...|...| substitution syntax doesn't exist
in Apache expressions — it silently failed, leaving SCRIPT_FILENAME
pointing to /mnt/users/ which PHP-FPM can't find.
Fixed to use regex match in the conditional with backreferences:
reqenv('SCRIPT_FILENAME') =~ m#^/mnt/users/([^/]+)/([^/]+)/public_html(.*)#
-> /home/$1/public_html$3
This is also generic (captures user from the path) so the template
no longer needs per-user placeholder substitution for this directive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The shared httpd serves files from /mnt/users/{user}/{domain}/public_html
but PHP-FPM containers have them at /home/{user}/public_html. When Apache
proxied PHP requests via fcgi, SCRIPT_FILENAME pointed to the Apache path
which doesn't exist inside the FPM container, causing "File not found".
Added ProxyFCGISetEnvIf to rewrite SCRIPT_FILENAME from the shared httpd
path to the FPM container path before proxying the request.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AlmaLinux 10 base image does not include openssl by default (AL9 did).
Add it explicitly to all three Dockerfiles since it's needed for
self-signed cert generation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bump all three Dockerfiles to almalinux/10-base with matching EPEL 10
and Remi 10 repository URLs. AlmaLinux 10.1 has been stable since Nov
2025. All PHP versions (7.4-8.5) confirmed available via Remi for EL10.
Also removes --allowerasing from shared-httpd Dockerfile since AL10
base does not ship curl-minimal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The almalinux/9-base image ships curl-minimal which conflicts with the
full curl package. Add --allowerasing to allow dnf to replace it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The official ImageMagick 7.1.2-18 RPMs require GLIBC 2.38 which is not
available on AlmaLinux 9 (ships GLIBC 2.34). Switch to EPEL-provided
ImageMagick packages which are built for EL9 and guaranteed compatible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds ImageMagick and ImageMagick-libs from the official CentOS x86_64
RPMs before PHP installation so php-pecl-imagick links against the
latest version. Applied to both Dockerfile (standalone) and
Dockerfile.fpm (shared httpd mode).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Separate Apache and PHP-FPM into distinct container roles to reduce
per-customer memory overhead on shared servers. Adds three new images:
- Dockerfile.fpm: PHP-FPM only (no Apache), listens on TCP port 9000
- Dockerfile.shared-httpd: Apache only (no PHP), with SSL and proxy_fcgi
- Existing Dockerfile unchanged for standalone mode
Key changes:
- detect-memory.sh: CONTAINER_ROLE env var (combined/fpm_only/httpd_only)
controls the memory budget split
- create-php-config.sh: FPM_LISTEN env var for TCP port vs Unix socket,
added /fpm-ping and /fpm-status health endpoints
- New entrypoints for each container role
- tune-mpm.sh for hot-adjusting Apache MPM settings
- shared-vhost-template.tpl with proxy_fcgi and SSL on port 443
- CI/CD builds all three image types in parallel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch PHP-FPM from pm=dynamic to pm=ondemand (zero idle workers),
auto-detect container memory via cgroups to calculate appropriate
limits, and generate Apache MPM config at runtime. All tuning values
are now overridable via environment variables.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Created user-specific crontab file at /home/$user/crontab
- Crontab now persists through container restarts/refreshes
- Users can manage their own cron jobs by editing their crontab file
- Automatically loads user crontab on container start
- Updated DEV environment to use user crontab for MySQL backups
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added git, nano, rsync, unzip, zip, mariadb client, bind-utils, jq, patch, nc, tree, and dos2unix to provide developers with commonly needed tools for PHP development and debugging.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The php-ioncube-loader package is incompatible with PHP 8.1 and was causing
a segmentation fault (exit code 139) when the Composer installer tried to
run PHP. This aligns PHP 8.1 with other PHP versions that already had
ioncube-loader removed.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Install Composer globally at /usr/local/bin/composer
- Available for all PHP versions and users
- Also includes previously added microdnf and less utilities
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
PHP error logs were incorrectly being written to /etc/httpd/logs/error_log
instead of the expected /home/$user/logs/php-fpm/ directory. Updated the
php_admin_value[error_log] setting to point to the proper location.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added postgresql-devel package to Dockerfile for client libraries
- Added php-pgsql extension to all PHP versions (7.4, 8.0, 8.1, 8.2, 8.3, 8.4)
- Enables PHP applications to connect to PostgreSQL databases
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Apache mpm_event: Reduced StartServers from 10 to 2, adjusted spare threads
and worker limits for container environments
- PHP-FPM: Switched from static to dynamic process management with lower
process counts (5 max children instead of 10)
- Removed php-ioncube-loader from PHP 8.0 installation
- Expected memory reduction: 60-70% in idle state while maintaining responsiveness
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>