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>