Add shared httpd + PHP-FPM-only container architecture
Some checks failed
Cloud Apache Container / Build-and-Push (74) (push) Successful in 2m22s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 3m14s
Cloud Apache Container / Build-and-Push (82) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (83) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (84) (push) Has been cancelled
Cloud Apache Container / Build-and-Push (85) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (74) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (80) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (81) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (82) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (83) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (84) (push) Has been cancelled
Cloud Apache Container / Build-FPM-Images (85) (push) Has been cancelled
Cloud Apache Container / Build-Shared-httpd (push) Has been cancelled
Cloud Apache Container / Build-and-Push (81) (push) Has been cancelled

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>
This commit is contained in:
2026-04-01 10:08:00 -07:00
parent 87c4f2befc
commit c78167871c
9 changed files with 510 additions and 55 deletions

View File

@@ -38,3 +38,65 @@ jobs:
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-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

43
Dockerfile.fpm Normal file
View File

@@ -0,0 +1,43 @@
FROM almalinux/9-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-9.noarch.rpm \
https://rpms.remirepo.net/enterprise/remi-release-9.rpm && \
dnf update -y && \
dnf install -y 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/*
# 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" ]

40
Dockerfile.shared-httpd Normal file
View File

@@ -0,0 +1,40 @@
FROM almalinux/9-base
# Install Apache and minimal dependencies (no PHP at all)
RUN dnf install -y \
https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm && \
dnf update -y && \
dnf install -y httpd mod_ssl 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" ]

View File

@@ -0,0 +1,39 @@
<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
<FilesMatch \.php$>
SetHandler "proxy:fcgi://~~fpm_host~~:~~fpm_port~~"
</FilesMatch>
DirectoryIndex index.php index.html index.htm
ErrorLog "/var/log/httpd/~~domain~~-error.log"
CustomLog "/var/log/httpd/~~domain~~-access.log" combined
</VirtualHost>
</IfModule>

View File

@@ -2,15 +2,28 @@
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
listen_directive="$FPM_LISTEN"
listen_owner_block="listen.owner = apache
listen.group = apache"
else
# TCP port mode
listen_directive="0.0.0.0:${FPM_LISTEN}"
listen_owner_block=""
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 = ${PHP_FPM_PM}
pm.max_children = ${PHP_FPM_MAX_CHILDREN}
@@ -22,7 +35,12 @@ pm.start_servers = ${PHP_FPM_START_SERVERS}
pm.min_spare_servers = ${PHP_FPM_MIN_SPARE}
pm.max_spare_servers = ${PHP_FPM_MAX_SPARE}
slowlog = /etc/httpd/logs/error_log
; 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] = /home/$user/logs/php-fpm/error.log

View File

@@ -38,6 +38,8 @@ 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
@@ -50,10 +52,23 @@ 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 ---
# --- PHP-FPM parameters (skipped for httpd_only) ---
if [ "$CONTAINER_ROLE" != "httpd_only" ]; then
PHP_WORKER_ESTIMATE_MB=${PHP_WORKER_ESTIMATE_MB:-60}
calc_max_children=$((PHP_BUDGET_MB / PHP_WORKER_ESTIMATE_MB))
@@ -74,8 +89,10 @@ PHP_FPM_MAX_REQUESTS=${FPM_MAX_REQUESTS:-500}
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 ---
# --- 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
@@ -103,10 +120,15 @@ APACHE_MAX_REQUEST_WORKERS=${APACHE_MAX_REQUEST_WORKERS:-$calc_max_request_worke
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_MEMORY_MB
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
View 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

View File

@@ -0,0 +1,66 @@
#!/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)
touch /var/log/httpd/error_log
tail -f /var/log/httpd/*
exit 0

79
scripts/tune-mpm.sh Executable file
View 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."