From 2c0a238f1cb17dade689e1de66270bdbdbee2c37 Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 06:51:59 +0200 Subject: [PATCH 1/8] Fix switching the Apache MPM with APACHE_EXTENSION_* Modules were enabled before the others were disabled: a2enmod refuses to enable an MPM while another one is still enabled. Disabling first also no longer skips the second command when the first one fails. --- CHANGELOG.md | 1 + tests-suite/variant-apache.sh | 10 ++++++++++ utils/enable_apache_mods.php | 16 ++++++++++------ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35bda06..ac6e12f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Minor changes * **2026-10-09** + * Fix switching the Apache MPM with `APACHE_EXTENSION_*` (modules are now disabled before being enabled, the MPM last) * Upgrade Supercronic from 0.1.9 to 0.2.49 (built with an up-to-date Go version) * Fix Composer and NodeJS binaries (`vendor/bin`, `node_modules/.bin` and global Composer binaries) not found by `docker exec` / `docker compose exec` when run without their path * Fix extensions that could not be loaded without another extension: the required extensions are now enabled with them (`memcached` requires `igbinary` and `msgpack`, `redis` requires `igbinary`, `mailparse` requires `mbstring` and `swoole` requires `curl`) diff --git a/tests-suite/variant-apache.sh b/tests-suite/variant-apache.sh index 3cd6175..d569f83 100755 --- a/tests-suite/variant-apache.sh +++ b/tests-suite/variant-apache.sh @@ -52,6 +52,16 @@ test_enableApacheModule() { assert_not_matches "does not exist" "$RESULT" "a2enmod/a2dismod received an unknown module" } ############################################################ +## The MPM can be switched with APACHE_EXTENSION_* +############################################################ +test_switchMpm() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e APACHE_EXTENSION_MPM_EVENT=1 -e APACHE_EXTENSION_MPM_PREFORK=0 -e "APACHE_EXTENSION_PHP${PHP_VERSION}=0" \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" ls /etc/apache2/mods-enabled 2>&1)" + assert_matches "mpm_event.load" "$RESULT" "mpm_event is not enabled" + assert_not_matches "mpm_prefork.load" "$RESULT" "mpm_prefork should be disabled" + assert_not_matches "ERROR" "$RESULT" "a2enmod/a2dismod failed" +} +############################################################ ## A stop requested during the initialization is not lost ############################################################ test_stopDuringStartup() { diff --git a/utils/enable_apache_mods.php b/utils/enable_apache_mods.php index f921b3a..0a1af2b 100644 --- a/utils/enable_apache_mods.php +++ b/utils/enable_apache_mods.php @@ -55,16 +55,20 @@ function enableExtension(string $extensionName): bool { } } -$toEnableExtensions = ''; -$toDisableExtensions = ''; +$toEnableExtensions = []; +$toDisableExtensions = []; foreach ($availableExtensions as $extension) { if (enableExtension($extension)) { - $toEnableExtensions .= $extension.' '; + $toEnableExtensions[] = $extension; } else { - $toDisableExtensions .= $extension.' '; + $toDisableExtensions[] = $extension; } } -echo 'a2enmod '.$toEnableExtensions.' > /dev/null && '; -echo 'a2dismod '.$toDisableExtensions." > /dev/null\n"; +// Modules are disabled before the others are enabled, and the MPM last (mod_php depends on mpm_prefork) +usort($toDisableExtensions, function (string $a, string $b): int { + return (strpos($a, 'mpm_') === 0) <=> (strpos($b, 'mpm_') === 0) ?: strcmp($a, $b); +}); +echo 'a2dismod -q '.implode(' ', $toDisableExtensions).' > /dev/null; '; +echo 'a2enmod -q '.implode(' ', $toEnableExtensions)." > /dev/null\n"; From 61ab28c9260d854ee79b85bfd212591b69e5e15d Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 17:03:07 +0000 Subject: [PATCH 2/8] Accept the http2 and proxy_http2 Apache modules in APACHE_EXTENSION_* They were listed as available in the README but silently ignored (APACHE_EXTENSION_*) or rejected (APACHE_EXTENSIONS). HTTP/2 is not served with mpm_prefork (required by mod_php): they are useful with the built-in Apache of the fpm variant (mpm_event). --- CHANGELOG.md | 1 + utils/enable_apache_mods.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac6e12f..a990133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Minor changes * **2026-10-09** + * Accept the `http2` and `proxy_http2` Apache modules in `APACHE_EXTENSION_*` (HTTP/2 requires a threaded MPM such as `mpm_event`: it is not served with the `mpm_prefork` MPM required by mod_php) * Fix switching the Apache MPM with `APACHE_EXTENSION_*` (modules are now disabled before being enabled, the MPM last) * Upgrade Supercronic from 0.1.9 to 0.2.49 (built with an up-to-date Go version) * Fix Composer and NodeJS binaries (`vendor/bin`, `node_modules/.bin` and global Composer binaries) not found by `docker exec` / `docker compose exec` when run without their path diff --git a/utils/enable_apache_mods.php b/utils/enable_apache_mods.php index 0a1af2b..adfd835 100644 --- a/utils/enable_apache_mods.php +++ b/utils/enable_apache_mods.php @@ -5,7 +5,7 @@ $defaultExtensions = ['access_compat', 'alias', 'auth_basic', 'authn_core', 'authn_file', 'authz_core', 'authz_host', 'authz_user', 'autoindex', 'deflate', 'dir', 'env', 'expires', 'filter', 'mime', 'mpm_prefork', 'negotiation', 'php'.getenv('PHP_VERSION'), 'reqtimeout', 'rewrite', 'setenvif', 'status']; -$availableExtensions = ['access_compat', 'actions', 'alias', 'allowmethods', 'asis', 'auth_basic', 'auth_digest', 'auth_form', 'authn_anon', 'authn_core', 'authn_dbd', 'authn_dbm', 'authn_file', 'authn_socache', 'authnz_fcgi', 'authnz_ldap', 'authz_core', 'authz_dbd', 'authz_dbm', 'authz_groupfile', 'authz_host', 'authz_owner', 'authz_user', 'autoindex', 'brotli', 'buffer', 'cache', 'cache_disk', 'cache_socache', 'cern_meta', 'cgi', 'cgid', 'charset_lite', 'data', 'dav', 'dav_fs', 'dav_lock', 'dbd', 'deflate', 'dialup', 'dir', 'dump_io', 'echo', 'env', 'expires', 'ext_filter', 'file_cache', 'filter', 'headers', 'heartbeat', 'heartmonitor', 'ident', 'imagemap', 'include', 'info', 'lbmethod_bybusyness', 'lbmethod_byrequests', 'lbmethod_bytraffic', 'lbmethod_heartbeat', 'ldap', 'log_debug', 'log_forensic', 'lua', 'macro', 'md', 'mime', 'mime_magic', 'mpm_event', 'mpm_prefork', 'mpm_worker', 'negotiation', 'php'.getenv('PHP_VERSION'), 'proxy', 'proxy_ajp', 'proxy_balancer', 'proxy_connect', 'proxy_express', 'proxy_fcgi', 'proxy_fdpass', 'proxy_ftp', 'proxy_hcheck', 'proxy_html', 'proxy_http', 'proxy_scgi', 'proxy_uwsgi', 'proxy_wstunnel', 'ratelimit', 'reflector', 'remoteip', 'reqtimeout', 'request', 'rewrite', 'sed', 'session', 'session_cookie', 'session_crypto', 'session_dbd', 'setenvif', 'slotmem_plain', 'slotmem_shm', 'socache_dbm', 'socache_memcache', 'socache_redis', 'socache_shmcb', 'speling', 'ssl', 'status', 'substitute', 'suexec', 'unique_id', 'userdir', 'usertrack', 'vhost_alias', 'xml2enc']; +$availableExtensions = ['access_compat', 'actions', 'alias', 'allowmethods', 'asis', 'auth_basic', 'auth_digest', 'auth_form', 'authn_anon', 'authn_core', 'authn_dbd', 'authn_dbm', 'authn_file', 'authn_socache', 'authnz_fcgi', 'authnz_ldap', 'authz_core', 'authz_dbd', 'authz_dbm', 'authz_groupfile', 'authz_host', 'authz_owner', 'authz_user', 'autoindex', 'brotli', 'buffer', 'cache', 'cache_disk', 'cache_socache', 'cern_meta', 'cgi', 'cgid', 'charset_lite', 'data', 'dav', 'dav_fs', 'dav_lock', 'dbd', 'deflate', 'dialup', 'dir', 'dump_io', 'echo', 'env', 'expires', 'ext_filter', 'file_cache', 'filter', 'headers', 'heartbeat', 'heartmonitor', 'http2', 'ident', 'imagemap', 'include', 'info', 'lbmethod_bybusyness', 'lbmethod_byrequests', 'lbmethod_bytraffic', 'lbmethod_heartbeat', 'ldap', 'log_debug', 'log_forensic', 'lua', 'macro', 'md', 'mime', 'mime_magic', 'mpm_event', 'mpm_prefork', 'mpm_worker', 'negotiation', 'php'.getenv('PHP_VERSION'), 'proxy', 'proxy_ajp', 'proxy_balancer', 'proxy_connect', 'proxy_express', 'proxy_fcgi', 'proxy_fdpass', 'proxy_ftp', 'proxy_hcheck', 'proxy_html', 'proxy_http', 'proxy_http2', 'proxy_scgi', 'proxy_uwsgi', 'proxy_wstunnel', 'ratelimit', 'reflector', 'remoteip', 'reqtimeout', 'request', 'rewrite', 'sed', 'session', 'session_cookie', 'session_crypto', 'session_dbd', 'setenvif', 'slotmem_plain', 'slotmem_shm', 'socache_dbm', 'socache_memcache', 'socache_redis', 'socache_shmcb', 'speling', 'ssl', 'status', 'substitute', 'suexec', 'unique_id', 'userdir', 'usertrack', 'vhost_alias', 'xml2enc']; $delimiter = [',', '|', ';', ':']; $replace = str_replace($delimiter, ' ', getenv('APACHE_EXTENSIONS')); From 4ce08e96093f54a6394840dafa6283a0b3352414 Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 07:34:29 +0200 Subject: [PATCH 3/8] Add an optional built-in Apache to the fpm variant (PHP_FPM_WEB_SERVER=apache) The fpm variant can run Apache (mpm_event + proxy_fcgi) in front of PHP-FPM in the same container: same Apache features as the apache variant (.htaccess, APACHE_DOCUMENT_ROOT, APACHE_EXTENSION_*) without mod_php, which forces mpm_prefork (one process embedding PHP per connection). - Disabled by default: the fpm variant keeps its behavior (PHP-FPM on port 9000) - apache2-fpm-foreground runs both processes; if one of them stops, the other one is stopped too and the container exits with an error - PHP-FPM runs with the Apache user, the Authorization header is forwarded, missing PHP files are answered by Apache (404) - PHP-FPM process manager and access log configurable with PHP_FPM_PM* and PHP_FPM_ACCESS_LOG (closes #410) - php-fpm-healthcheck command (ping endpoint) for Docker healthchecks and Kubernetes probes --- CHANGELOG.md | 3 + Dockerfile.slim.apache | 18 +- Dockerfile.slim.cli | 1 + Dockerfile.slim.fpm | 110 ++++++++++- tests-suite/assets/apache/authorization.php | 2 + tests-suite/variant-fpm.sh | 208 +++++++++++++++++--- utils/Dockerfile.slim.blueprint | 51 +++-- utils/apache-docker-php-fpm.conf | 16 ++ utils/apache2-fpm-foreground | 69 +++++++ utils/docker-entrypoint-as-root.sh | 15 +- utils/enable_apache_mods.php | 9 +- utils/fpm-docker.conf | 9 +- utils/fpm-zz-docker-pm.conf | 13 ++ utils/fpm-zz-docker.conf | 3 + utils/php-fpm-healthcheck | 6 + 15 files changed, 483 insertions(+), 50 deletions(-) create mode 100644 tests-suite/assets/apache/authorization.php create mode 100644 utils/apache-docker-php-fpm.conf create mode 100755 utils/apache2-fpm-foreground create mode 100644 utils/fpm-zz-docker-pm.conf create mode 100755 utils/php-fpm-healthcheck diff --git a/CHANGELOG.md b/CHANGELOG.md index a990133..b740814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Minor changes * **2026-10-09** + * fpm variant: optional built-in Apache (`PHP_FPM_WEB_SERVER=apache`): Apache (`mpm_event`) in front of PHP-FPM in the same container. Same features as the `apache` variant (`.htaccess`, `APACHE_DOCUMENT_ROOT`, `APACHE_EXTENSION_*`), lower memory usage and much better handling of concurrent connections (see `benchmarks/fpm-apache`) + * PHP-FPM: the process manager and the access log can be configured with `PHP_FPM_PM*` and `PHP_FPM_ACCESS_LOG` environment variables + * PHP-FPM: new `php-fpm-healthcheck` command (ping endpoint), usable as a Docker healthcheck or a Kubernetes probe * Accept the `http2` and `proxy_http2` Apache modules in `APACHE_EXTENSION_*` (HTTP/2 requires a threaded MPM such as `mpm_event`: it is not served with the `mpm_prefork` MPM required by mod_php) * Fix switching the Apache MPM with `APACHE_EXTENSION_*` (modules are now disabled before being enabled, the MPM last) * Upgrade Supercronic from 0.1.9 to 0.2.49 (built with an up-to-date Go version) diff --git a/Dockerfile.slim.apache b/Dockerfile.slim.apache index 24b60e1..341b82b 100644 --- a/Dockerfile.slim.apache +++ b/Dockerfile.slim.apache @@ -161,6 +161,7 @@ RUN composer global require bamarni/symfony-console-autocomplete && \ USER root +# Apache: built-in web server of the fpm variant (disabled by default, see PHP_FPM_WEB_SERVER) ENV APACHE_CONFDIR=/etc/apache2 ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars @@ -200,20 +201,19 @@ RUN set -eux; \ ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \ chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR" -# Apache + PHP requires preforking Apache for best results + +# Apache + mod_php requires preforking Apache (mod_php is not thread-safe) RUN a2dismod mpm_event && a2enmod mpm_prefork # PHP files should be handled by PHP, and should be preferred over any other file type COPY utils/apache-docker-php.conf /etc/apache2/conf-available/docker-php.conf -RUN a2enconf docker-php - -ENV PHP_EXTRA_BUILD_DEPS=apache2-dev -ENV PHP_EXTRA_CONFIGURE_ARGS="--with-apxs2 --disable-cgi" - # https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop STOPSIGNAL SIGWINCH + +RUN a2enconf docker-php + COPY utils/apache2-foreground /usr/local/bin/ EXPOSE 80 @@ -223,9 +223,11 @@ ENV APACHE_DOCUMENT_ROOT= RUN sed -ri -e 's!/var/www/html!${ABSOLUTE_APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf && \ sed -ri -e 's!/var/www/!${ABSOLUTE_APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf + # Let's remove the default Apache php.ini file (it will be copied from TEMPLATE_PHP_INI) RUN rm /etc/php/${PHP_VERSION}/apache2/php.ini + # |-------------------------------------------------------------------------- # | Apache mod_rewrite # |-------------------------------------------------------------------------- @@ -318,6 +320,8 @@ COPY utils/generate_cron.php /usr/local/bin/generate_cron.php COPY utils/startup_commands.php /usr/local/bin/startup_commands.php COPY utils/enable_apache_mods.php /usr/local/bin/enable_apache_mods.php + + COPY utils/apache-expose-envvars.sh /usr/local/bin/apache-expose-envvars.sh COPY utils/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh @@ -333,8 +337,10 @@ ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] RUN echo "ServerName localhost" > /etc/apache2/conf-available/servername.conf && \ a2enconf servername + CMD ["apache2-foreground"] + # |-------------------------------------------------------------------------- # | Entrypoint # |-------------------------------------------------------------------------- diff --git a/Dockerfile.slim.cli b/Dockerfile.slim.cli index 5a242b8..7a32e37 100644 --- a/Dockerfile.slim.cli +++ b/Dockerfile.slim.cli @@ -243,6 +243,7 @@ ENV IMAGE_VARIANT=cli COPY utils/generate_cron.php /usr/local/bin/generate_cron.php COPY utils/startup_commands.php /usr/local/bin/startup_commands.php + COPY utils/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh COPY utils/docker-entrypoint-as-root.sh /usr/local/bin/docker-entrypoint-as-root.sh diff --git a/Dockerfile.slim.fpm b/Dockerfile.slim.fpm index 3633cc2..576588b 100644 --- a/Dockerfile.slim.fpm +++ b/Dockerfile.slim.fpm @@ -161,10 +161,81 @@ RUN composer global require bamarni/symfony-console-autocomplete && \ USER root +# Apache: built-in web server of the fpm variant (disabled by default, see PHP_FPM_WEB_SERVER) +ENV APACHE_CONFDIR=/etc/apache2 +ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars + +RUN set -eux; \ + apt update; \ + apt install -y --no-install-recommends apache2; \ + rm -rf /var/lib/apt/lists/*; \ + \ +# generically convert lines like +# export APACHE_RUN_USER=www-data +# into +# : ${APACHE_RUN_USER:=www-data} +# export APACHE_RUN_USER +# so that they can be overridden at runtime ("-e APACHE_RUN_USER=...") + sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \ + \ +# setup directories and permissions + . "$APACHE_ENVVARS"; \ + for dir in \ + "$APACHE_LOCK_DIR" \ + "$APACHE_RUN_DIR" \ + "$APACHE_LOG_DIR" \ + ; do \ + rm -rvf "$dir"; \ + mkdir -p "$dir"; \ + chown "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$dir"; \ +# allow running as an arbitrary user (https://github.com/docker-library/php/issues/743) + chmod 777 "$dir"; \ + done; \ + \ +# delete the "index.html" that installing Apache drops in here + rm -rvf /var/www/html/*; \ + \ +# logs should go to stdout / stderr + ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \ + ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log"; \ + ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \ + chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR" + + +# Apache keeps its default threaded MPM (mpm_event) and forwards PHP files to PHP-FPM +RUN a2enmod proxy_fcgi setenvif + +# PHP files should be handled by PHP-FPM, and should be preferred over any other file type +COPY utils/apache-docker-php-fpm.conf /etc/apache2/conf-available/docker-php.conf + + +RUN a2enconf docker-php + +COPY utils/apache2-foreground /usr/local/bin/ + +EXPOSE 80 + +ENV APACHE_DOCUMENT_ROOT= + +RUN sed -ri -e 's!/var/www/html!${ABSOLUTE_APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf && \ + sed -ri -e 's!/var/www/!${ABSOLUTE_APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf + +# |-------------------------------------------------------------------------- +# | Apache mod_rewrite +# |-------------------------------------------------------------------------- +# | +# | Enables Apache mod_rewrite. +# | + +RUN a2enmod rewrite + + + +# libfcgi-bin provides cgi-fcgi, used by php-fpm-healthcheck RUN apt update \ - && apt install -y --no-install-recommends php${PHP_VERSION}-fpm \ + && apt install -y --no-install-recommends php${PHP_VERSION}-fpm libfcgi-bin \ && apt clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* @@ -172,8 +243,24 @@ RUN apt update \ RUN rm /etc/php/${PHP_VERSION}/fpm/php.ini && \ ln -s /usr/sbin/php-fpm${PHP_VERSION} /usr/sbin/php-fpm COPY utils/fpm-docker.conf /etc/php/${PHP_VERSION}/fpm/pool.d/docker.conf +COPY utils/fpm-zz-docker-pm.conf /etc/php/${PHP_VERSION}/fpm/pool.d/zz-docker-pm.conf +COPY utils/php-fpm-healthcheck /usr/local/bin/php-fpm-healthcheck + +# PHP-FPM process manager (defaults are the ones of the Ubuntu package) +ENV PHP_FPM_PM=dynamic \ + PHP_FPM_PM_MAX_CHILDREN=5 \ + PHP_FPM_PM_START_SERVERS=2 \ + PHP_FPM_PM_MIN_SPARE_SERVERS=1 \ + PHP_FPM_PM_MAX_SPARE_SERVERS=3 \ + PHP_FPM_PM_MAX_REQUESTS=0 \ + PHP_FPM_ACCESS_LOG=/proc/self/fd/2 + COPY utils/fpm-zz-docker.conf /etc/php/${PHP_VERSION}/fpm/pool.d/zz-docker.conf +# Built-in web server: "apache" runs Apache in front of PHP-FPM (empty: PHP-FPM only) +ENV PHP_FPM_WEB_SERVER= +COPY utils/apache2-fpm-foreground /usr/local/bin/ + EXPOSE 9000 STOPSIGNAL SIGQUIT @@ -260,6 +347,9 @@ ENV IMAGE_VARIANT=fpm COPY utils/generate_cron.php /usr/local/bin/generate_cron.php COPY utils/startup_commands.php /usr/local/bin/startup_commands.php +COPY utils/enable_apache_mods.php /usr/local/bin/enable_apache_mods.php + + COPY utils/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh COPY utils/docker-entrypoint-as-root.sh /usr/local/bin/docker-entrypoint-as-root.sh @@ -269,6 +359,24 @@ RUN ln -s ${PHP_VERSION} /usr/local/lib/thecodingmachine-php/extensions/current ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +# Let's register a servername to remove the message "apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message" +RUN echo "ServerName localhost" > /etc/apache2/conf-available/servername.conf && \ + a2enconf servername + + + +# |-------------------------------------------------------------------------- +# | Entrypoint +# |-------------------------------------------------------------------------- +# | +# | Defines Apache user. By default, we switch this to "docker" user. +# | This way, no problem to write from Apache in the current working directory. +# | Important! This should be changed back to www-data in production. +# | + +ENV APACHE_RUN_USER=docker \ + APACHE_RUN_GROUP=docker + CMD ["php-fpm"] diff --git a/tests-suite/assets/apache/authorization.php b/tests-suite/assets/apache/authorization.php new file mode 100644 index 0000000..311f1a1 --- /dev/null +++ b/tests-suite/assets/apache/authorization.php @@ -0,0 +1,2 @@ + /dev/null 2>&1 && \ + { [[ "$2" != "apache" ]] || docker exec "$1" curl -s -o /dev/null http://localhost/ > /dev/null 2>&1; }; then + return 0 + fi + sleep 0.5 + done + return 1 +} ############################################################ -## Test if environment starts without errors +## PHP-FPM starts and answers to FastCGI requests (ping endpoint) ############################################################ test_start() { - docker run --name "${FPM_CONTAINER_NAME}" ${RUN_OPTIONS} --rm -e MYVAR=foo -e PHP_INI_MEMORY_LIMIT=2G -p "$(unused_port):9000" -d -v "${SCRIPT_DIR}/assets/":/var/www/html \ - "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null - assert_equals "0" "$?" "Docker run failed" - # Let's wait for FPM to start - sleep 3 - # If the container is still up, it will not fail when stopping. - docker stop "${FPM_CONTAINER_NAME}" > /dev/null 2>&1 - assert_equals "0" "$?" "Docker stop failed" + wait_ready "${FPM_CONTAINER_NAME}" + docker exec "${FPM_CONTAINER_NAME}" php-fpm-healthcheck + assert_equals "0" "$?" "PHP-FPM healthcheck failed" +} +############################################################ +## Process manager is configurable with environment variables +############################################################ +test_processManager() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e PHP_FPM_PM=static -e PHP_FPM_PM_MAX_CHILDREN=7 \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" php-fpm -tt 2>&1)" + assert_matches "pm = static" "$RESULT" "PHP_FPM_PM was not applied" + assert_matches "pm.max_children = 7" "$RESULT" "PHP_FPM_PM_MAX_CHILDREN was not applied" } - ############################################################ ## Graceful stop (SIGQUIT): fast and successful exit ############################################################ test_gracefulStop() { docker run --name "${FPM_STOP_CONTAINER_NAME}" ${RUN_OPTIONS} -d \ "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null - # Let's wait for FPM to start - for _ in $(seq 1 60); do - docker logs "${FPM_STOP_CONTAINER_NAME}" 2>&1 | grep -q "ready to handle connections" && break - sleep 0.5 - done + wait_ready "${FPM_STOP_CONTAINER_NAME}" START=$(date +%s) docker stop "${FPM_STOP_CONTAINER_NAME}" > /dev/null 2>&1 DURATION=$(( $(date +%s) - START )) @@ -38,26 +49,173 @@ test_gracefulStop() { assert_equals "0" "$EXIT_CODE" "PHP-FPM did not stop gracefully" assert "test ${DURATION} -lt 5" "PHP-FPM took ${DURATION}s to stop (killed by timeout?)" } + ############################################################ -## A stop requested during the initialization is not lost +## Apache is not started by default ############################################################ -test_stopDuringStartup() { - docker run --name "${FPM_STOP_CONTAINER_NAME}" ${RUN_OPTIONS} -d \ +test_noApacheByDefault() { + RESULT="$(docker exec "${FPM_CONTAINER_NAME}" ps -eo args 2>&1)" + assert_not_matches "apache2" "$RESULT" "Apache should not be started without PHP_FPM_WEB_SERVER" +} +############################################################ +## Apache modules of the built-in Apache can be enabled with APACHE_EXTENSION_* +############################################################ +test_apacheEnableModule() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e PHP_FPM_WEB_SERVER=apache -e APACHE_EXTENSION_HTTP2=1 \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" ls /etc/apache2/mods-enabled 2>&1)" + assert_matches "http2.load" "$RESULT" "APACHE_EXTENSION_HTTP2 was not applied" + assert_matches "proxy_fcgi.load" "$RESULT" "proxy_fcgi should be enabled" + assert_not_matches "does not exist" "$RESULT" "a2enmod/a2dismod received an unknown module" +} +############################################################ +## An invalid PHP_FPM_WEB_SERVER value is rejected +############################################################ +test_invalidWebServer() { + docker run ${RUN_OPTIONS} --rm -e PHP_FPM_WEB_SERVER=nginx "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null 2>&1 + assert_not_equals "0" "$?" "An invalid PHP_FPM_WEB_SERVER value should fail" +} +############################################################ +## Run apache and try to retrieve var content +############################################################ +test_apacheDisplayVarInPhp() { + RESULT="$(curl -sq http://localhost:${DOCKER1_PORT}/apache/ 2>&1)" + assert_equals "foo" "$RESULT" "MYVAR was not populate onto php" +} +############################################################ +## Run apache with relative document root +############################################################ +test_apacheDocumentRootRelative() { + RESULT="$(curl -sq http://localhost:${DOCKER2_PORT}/ 2>&1)" + assert_equals "foo" "$RESULT" "Apache document root (relative) does not work properly" +} +############################################################ +## Run apache with absolute document root +############################################################ +test_apacheDocumentRootAbsolute() { + RESULT="$(curl -sq http://localhost:${DOCKER3_PORT}/ 2>&1)" + assert_equals "foo" "$RESULT" "Apache document root (absolute) does not work properly" +} +############################################################ +## Run apache HtAccess +############################################################ +test_apacheHtaccessRewrite() { + RESULT="$(curl -sq http://localhost:${DOCKER1_PORT}/apache/htaccess/ 2>&1)" + assert_equals "foo" "$RESULT" "Apache HtAccess RewriteRule was not applied" +} +############################################################ +## Test PHP_INI_... variables are correctly handled by PHP-FPM +############################################################ +test_apacheChangeMemoryLimit() { + RESULT="$(curl -sq http://localhost:${DOCKER1_PORT}/apache/echo_memory_limit.php 2>&1 )" + assert_equals "2G" "$RESULT" "PHP-FPM PHP_INI_MEMORY_LIMIT was not applied" +} +############################################################ +## The Authorization header is forwarded to PHP-FPM +############################################################ +test_apacheAuthorizationHeader() { + RESULT="$(curl -sq -H "Authorization: Bearer foo" http://localhost:${DOCKER1_PORT}/apache/authorization.php 2>&1 )" + assert_equals "Bearer foo" "$RESULT" "Authorization header was not forwarded to PHP-FPM" +} +############################################################ +## A missing PHP file is a 404 answered by Apache +############################################################ +test_apacheMissingPhpFile() { + RESULT="$(curl -sq -o /dev/null -w '%{http_code}' http://localhost:${DOCKER1_PORT}/apache/missing.php 2>&1 )" + assert_equals "404" "$RESULT" "A missing PHP file should return a 404" +} +############################################################ +## Apache uses the threaded MPM (mod_php is not loaded) +############################################################ +test_apacheMpmEvent() { + RESULT="$(docker exec "${DOCKER1_NAME}" ls /etc/apache2/mods-enabled/ 2>&1)" + assert_matches "mpm_event.load" "$RESULT" "mpm_event is not enabled" + assert_not_matches "mpm_prefork.load" "$RESULT" "mpm_prefork should not be enabled" +} +############################################################ +## PHP-FPM healthcheck +############################################################ +test_apacheHealthcheck() { + docker exec "${DOCKER1_NAME}" php-fpm-healthcheck + assert_equals "0" "$?" "PHP-FPM healthcheck failed" +} +############################################################ +## Graceful stop: fast and successful exit +############################################################ +test_apacheGracefulStop() { + docker run --name "${STOP_NAME}" ${RUN_OPTIONS} -d -e PHP_FPM_WEB_SERVER=apache \ "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null - sleep 0.3 + wait_ready "${STOP_NAME}" apache START=$(date +%s) - docker stop "${FPM_STOP_CONTAINER_NAME}" > /dev/null 2>&1 + docker stop "${STOP_NAME}" > /dev/null 2>&1 DURATION=$(( $(date +%s) - START )) - docker rm -f "${FPM_STOP_CONTAINER_NAME}" > /dev/null 2>&1 - assert "test ${DURATION} -lt 5" "Stopping during startup took ${DURATION}s (killed by timeout?)" + EXIT_CODE="$(docker inspect -f '{{.State.ExitCode}}' "${STOP_NAME}")" + docker rm "${STOP_NAME}" > /dev/null 2>&1 + assert_equals "0" "$EXIT_CODE" "The container did not stop gracefully" + assert "test ${DURATION} -lt 5" "The container took ${DURATION}s to stop (killed by timeout?)" +} +############################################################ +## A stop requested during the initialization is not lost +############################################################ +test_stopDuringStartup() { + for web_server in "" apache; do + docker run --name "${STARTUP_STOP_NAME}" ${RUN_OPTIONS} -d -e PHP_FPM_WEB_SERVER="${web_server}" \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null + sleep 0.3 + START=$(date +%s) + docker stop "${STARTUP_STOP_NAME}" > /dev/null 2>&1 + DURATION=$(( $(date +%s) - START )) + docker rm -f "${STARTUP_STOP_NAME}" > /dev/null 2>&1 + assert "test ${DURATION} -lt 5" "Stopping during startup took ${DURATION}s (web server: '${web_server}')" + done +} +############################################################ +## When PHP-FPM dies, the container exits with an error +############################################################ +test_apacheFpmCrashStopsContainer() { + docker run --name "${CRASH_NAME}" ${RUN_OPTIONS} -d -e PHP_FPM_WEB_SERVER=apache \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null + wait_ready "${CRASH_NAME}" apache + docker exec -u root "${CRASH_NAME}" pkill -KILL -f "php-fpm: master" > /dev/null 2>&1 + EXIT_CODE="$(timeout 10 docker wait "${CRASH_NAME}")" + docker rm -f "${CRASH_NAME}" > /dev/null 2>&1 + assert_not_equals "0" "${EXIT_CODE:-0}" "The container should exit with an error when PHP-FPM dies" } setup_suite() { export FPM_CONTAINER_NAME="test-fpm-$(unused_port)" export FPM_STOP_CONTAINER_NAME="test-fpm-stop-$(unused_port)" + export STARTUP_STOP_NAME="test-fpm-startup-stop-$(unused_port)" + docker run --name "${FPM_CONTAINER_NAME}" ${RUN_OPTIONS} --rm -e MYVAR=foo -e PHP_INI_MEMORY_LIMIT=2G -p "$(unused_port):9000" -d -v "${SCRIPT_DIR}/assets/":/var/www/html \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null + assert_equals "0" "$?" "Docker run failed" + # Built-in Apache (PHP_FPM_WEB_SERVER=apache) + export STOP_NAME="test-fpm-builtin-apache-stop-$(unused_port)" + export CRASH_NAME="test-fpm-builtin-apache-crash-$(unused_port)" + # SETUP apache1 + export DOCKER1_PORT="$(unused_port)" + export DOCKER1_NAME="test-fpm-builtin-apache1-${DOCKER1_PORT}" + docker run --name "${DOCKER1_NAME}" ${RUN_OPTIONS} --rm -e MYVAR=foo -e PHP_INI_MEMORY_LIMIT=2G -e PHP_FPM_WEB_SERVER=apache -p "${DOCKER1_PORT}:80" -d -v "${SCRIPT_DIR}/assets/":/var/www/html \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null + assert_equals "0" "$?" "Docker run failed" + # SETUP apache2 + export DOCKER2_PORT="$(unused_port)" + export DOCKER2_NAME="test-fpm-builtin-apache2-${DOCKER2_PORT}" + docker run --name "${DOCKER2_NAME}" ${RUN_OPTIONS} --rm -e MYVAR=foo -e APACHE_DOCUMENT_ROOT=apache -e PHP_FPM_WEB_SERVER=apache -p "${DOCKER2_PORT}:80" -d -v "${SCRIPT_DIR}/assets/":/var/www/html \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null + assert_equals "0" "$?" "Docker run failed" + # SETUP apache3 + export DOCKER3_PORT="$(unused_port)" + export DOCKER3_NAME="test-fpm-builtin-apache3-${DOCKER3_PORT}" + docker run --name "${DOCKER3_NAME}" ${RUN_OPTIONS} --rm -e MYVAR=foo -e APACHE_DOCUMENT_ROOT=/var/www/foo/apache -e PHP_FPM_WEB_SERVER=apache -p "${DOCKER3_PORT}:80" -d -v "${SCRIPT_DIR}/assets/":/var/www/foo \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" > /dev/null + assert_equals "0" "$?" "Docker run failed" + # Let's wait for Apache to start + waitfor http://localhost:${DOCKER1_PORT} + waitfor http://localhost:${DOCKER2_PORT} + waitfor http://localhost:${DOCKER3_PORT} } teardown_suite() { - docker stop "${FPM_CONTAINER_NAME}" > /dev/null 2>&1 - docker rm -f "${FPM_STOP_CONTAINER_NAME}" > /dev/null 2>&1 + docker stop "${FPM_CONTAINER_NAME}" "${DOCKER1_NAME}" "${DOCKER2_NAME}" "${DOCKER3_NAME}" > /dev/null 2>&1 + docker rm -f "${FPM_STOP_CONTAINER_NAME}" "${STOP_NAME}" "${CRASH_NAME}" "${STARTUP_STOP_NAME}" > /dev/null 2>&1 } diff --git a/utils/Dockerfile.slim.blueprint b/utils/Dockerfile.slim.blueprint index 198ddd9..076cfe7 100644 --- a/utils/Dockerfile.slim.blueprint +++ b/utils/Dockerfile.slim.blueprint @@ -159,13 +159,14 @@ RUN composer global require bamarni/symfony-console-autocomplete && \ USER root -{{if eq .Orbit.variant "apache" }} +{{if or (eq .Orbit.variant "apache") (eq .Orbit.variant "fpm") }} +# Apache: built-in web server of the fpm variant (disabled by default, see PHP_FPM_WEB_SERVER) ENV APACHE_CONFDIR=/etc/apache2 ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars RUN set -eux; \ apt update; \ - apt install -y --no-install-recommends apache2 libapache2-mod-php${PHP_VERSION}; \ + apt install -y --no-install-recommends apache2{{if eq .Orbit.variant "apache" }} libapache2-mod-php${PHP_VERSION}{{end}}; \ rm -rf /var/lib/apt/lists/*; \ \ # generically convert lines like @@ -199,19 +200,24 @@ RUN set -eux; \ ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \ chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR" -# Apache + PHP requires preforking Apache for best results +{{if eq .Orbit.variant "apache" }} +# Apache + mod_php requires preforking Apache (mod_php is not thread-safe) RUN a2dismod mpm_event && a2enmod mpm_prefork # PHP files should be handled by PHP, and should be preferred over any other file type COPY utils/apache-docker-php.conf /etc/apache2/conf-available/docker-php.conf -RUN a2enconf docker-php - -ENV PHP_EXTRA_BUILD_DEPS=apache2-dev -ENV PHP_EXTRA_CONFIGURE_ARGS="--with-apxs2 --disable-cgi" - # https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop STOPSIGNAL SIGWINCH +{{else}} +# Apache keeps its default threaded MPM (mpm_event) and forwards PHP files to PHP-FPM +RUN a2enmod proxy_fcgi setenvif + +# PHP files should be handled by PHP-FPM, and should be preferred over any other file type +COPY utils/apache-docker-php-fpm.conf /etc/apache2/conf-available/docker-php.conf +{{end}} + +RUN a2enconf docker-php COPY utils/apache2-foreground /usr/local/bin/ @@ -222,8 +228,10 @@ ENV APACHE_DOCUMENT_ROOT= RUN sed -ri -e 's!/var/www/html!${ABSOLUTE_APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf && \ sed -ri -e 's!/var/www/!${ABSOLUTE_APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf +{{if eq .Orbit.variant "apache" }} # Let's remove the default Apache php.ini file (it will be copied from TEMPLATE_PHP_INI) RUN rm /etc/php/${PHP_VERSION}/apache2/php.ini +{{end}} # |-------------------------------------------------------------------------- # | Apache mod_rewrite @@ -236,8 +244,9 @@ RUN a2enmod rewrite {{end}} {{if eq .Orbit.variant "fpm" }} +# libfcgi-bin provides cgi-fcgi, used by php-fpm-healthcheck RUN apt update \ - && apt install -y --no-install-recommends php${PHP_VERSION}-fpm \ + && apt install -y --no-install-recommends php${PHP_VERSION}-fpm libfcgi-bin \ && apt clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* @@ -245,8 +254,24 @@ RUN apt update \ RUN rm /etc/php/${PHP_VERSION}/fpm/php.ini && \ ln -s /usr/sbin/php-fpm${PHP_VERSION} /usr/sbin/php-fpm COPY utils/fpm-docker.conf /etc/php/${PHP_VERSION}/fpm/pool.d/docker.conf +COPY utils/fpm-zz-docker-pm.conf /etc/php/${PHP_VERSION}/fpm/pool.d/zz-docker-pm.conf +COPY utils/php-fpm-healthcheck /usr/local/bin/php-fpm-healthcheck + +# PHP-FPM process manager (defaults are the ones of the Ubuntu package) +ENV PHP_FPM_PM=dynamic \ + PHP_FPM_PM_MAX_CHILDREN=5 \ + PHP_FPM_PM_START_SERVERS=2 \ + PHP_FPM_PM_MIN_SPARE_SERVERS=1 \ + PHP_FPM_PM_MAX_SPARE_SERVERS=3 \ + PHP_FPM_PM_MAX_REQUESTS=0 \ + PHP_FPM_ACCESS_LOG=/proc/self/fd/2 + COPY utils/fpm-zz-docker.conf /etc/php/${PHP_VERSION}/fpm/pool.d/zz-docker.conf +# Built-in web server: "apache" runs Apache in front of PHP-FPM (empty: PHP-FPM only) +ENV PHP_FPM_WEB_SERVER= +COPY utils/apache2-fpm-foreground /usr/local/bin/ + EXPOSE 9000 STOPSIGNAL SIGQUIT @@ -339,8 +364,10 @@ ENV IMAGE_VARIANT={{ .Orbit.variant }} COPY utils/generate_cron.php /usr/local/bin/generate_cron.php COPY utils/startup_commands.php /usr/local/bin/startup_commands.php -{{if eq .Orbit.variant "apache" }} +{{if or (eq .Orbit.variant "apache") (eq .Orbit.variant "fpm") }} COPY utils/enable_apache_mods.php /usr/local/bin/enable_apache_mods.php +{{end}} +{{if eq .Orbit.variant "apache" }} COPY utils/apache-expose-envvars.sh /usr/local/bin/apache-expose-envvars.sh {{end}} COPY utils/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh @@ -351,12 +378,14 @@ RUN ln -s ${PHP_VERSION} /usr/local/lib/thecodingmachine-php/extensions/current ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] -{{if eq .Orbit.variant "apache" }} +{{if or (eq .Orbit.variant "apache") (eq .Orbit.variant "fpm") }} # Let's register a servername to remove the message "apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message" RUN echo "ServerName localhost" > /etc/apache2/conf-available/servername.conf && \ a2enconf servername +{{if eq .Orbit.variant "apache" }} CMD ["apache2-foreground"] +{{end}} # |-------------------------------------------------------------------------- # | Entrypoint diff --git a/utils/apache-docker-php-fpm.conf b/utils/apache-docker-php-fpm.conf new file mode 100644 index 0000000..774cfce --- /dev/null +++ b/utils/apache-docker-php-fpm.conf @@ -0,0 +1,16 @@ +# Only existing PHP files are sent to PHP-FPM (Apache answers 404 for the others) + + + SetHandler "proxy:fcgi://127.0.0.1:9000" + + + +DirectoryIndex disabled +DirectoryIndex index.php index.html + + + Options -Indexes + AllowOverride All + # Forward the Authorization header to PHP (HTTP_AUTHORIZATION, PHP_AUTH_USER...) + CGIPassAuth On + diff --git a/utils/apache2-fpm-foreground b/utils/apache2-fpm-foreground new file mode 100755 index 0000000..b140cdb --- /dev/null +++ b/utils/apache2-fpm-foreground @@ -0,0 +1,69 @@ +#!/bin/bash +# Runs PHP-FPM and Apache side by side (fpm variant with PHP_FPM_WEB_SERVER=apache). +# If one of them stops, the other one is stopped too: the container exits and can be restarted by its supervisor. + +# The access log is written by Apache +if [[ "$PHP_FPM_ACCESS_LOG" == "/proc/self/fd/2" ]]; then + export PHP_FPM_ACCESS_LOG=/dev/null +fi + +stopping=0 +stop_all() { + stopping=1 + # Graceful stops: running requests are completed + if [[ -n "$apache_pid" ]]; then + if [[ "$(cat "/proc/${apache_pid}/comm" 2>/dev/null)" == "apache2" ]]; then + kill -WINCH "$apache_pid" 2>/dev/null + else + # apache2-foreground has not exec'd Apache yet (and ignores SIGWINCH): nothing to drain + kill -TERM "$apache_pid" 2>/dev/null + fi + fi + [[ -n "$fpm_pid" ]] && kill -QUIT "$fpm_pid" 2>/dev/null +} +trap stop_all TERM INT QUIT + +# PHP-FPM runs with the Apache user (like mod_php) +sudo -E -H -u "$APACHE_RUN_USER" php-fpm & +fpm_pid=$! + +# Wait for PHP-FPM so that the first requests do not fail +for _ in $(seq 1 50); do + if php-fpm-healthcheck || ! kill -0 "$fpm_pid" 2>/dev/null; then + break + fi + sleep 0.1 +done + +apache2-foreground & +apache_pid=$! + +# A stop signal may have been received before Apache was started +if [[ "$stopping" == "1" ]]; then + stop_all +fi + +# Wait for the first process to exit (or for a stop signal) +wait -n "$fpm_pid" "$apache_pid" +status=$? + +if [[ "$stopping" == "0" ]]; then + if kill -0 "$fpm_pid" 2>/dev/null; then + >&2 echo "Apache exited (code ${status}), stopping PHP-FPM" + else + >&2 echo "PHP-FPM exited (code ${status}), stopping Apache" + fi + stop_all + stopping=2 +fi + +# Wait for both processes (a trapped signal interrupts "wait") +while kill -0 "$fpm_pid" 2>/dev/null || kill -0 "$apache_pid" 2>/dev/null; do + wait +done + +if [[ "$stopping" == "2" ]]; then + # Unexpected stop: always report a failure + exit $(( status == 0 ? 1 : status )) +fi +exit 0 diff --git a/utils/docker-entrypoint-as-root.sh b/utils/docker-entrypoint-as-root.sh index 539ea9f..6092012 100755 --- a/utils/docker-entrypoint-as-root.sh +++ b/utils/docker-entrypoint-as-root.sh @@ -25,6 +25,15 @@ if [[ "$IMAGE_VARIANT" == "fpm" ]]; then ln -sf /usr/lib/php/${PHP_VERSION}/php.ini-${TEMPLATE_PHP_INI} /etc/php/${PHP_VERSION}/fpm/php.ini fi +# Built-in web server of the fpm variant +if [[ -n "$PHP_FPM_WEB_SERVER" ]] && [[ "$PHP_FPM_WEB_SERVER" != "apache" ]]; then + >&2 echo "Invalid PHP_FPM_WEB_SERVER value: '$PHP_FPM_WEB_SERVER' (supported: 'apache', or empty to disable it)" + exit 1 +fi +if [[ "$IMAGE_VARIANT" == "apache" ]] || [[ "$PHP_FPM_WEB_SERVER" == "apache" ]]; then + WITH_APACHE=1 +fi + # Let's find the user to use for commands. # If $DOCKER_USER, let's use this. Otherwise, let's find it. if [[ "$DOCKER_USER" == "" ]]; then @@ -156,7 +165,7 @@ if [[ -s /tmp/generated_crontab ]]; then supercronic ${SUPERCRONIC_OPTIONS} /tmp/generated_crontab & fi -if [[ "$IMAGE_VARIANT" == "apache" ]]; then +if [[ "$WITH_APACHE" == "1" ]]; then /usr/bin/real_php -d display_errors=stderr /usr/local/bin/enable_apache_mods.php | bash fi @@ -175,6 +184,10 @@ fi if [[ "$@" == "apache2-foreground" ]]; then /usr/local/bin/apache-expose-envvars.sh; exec "$@"; +elif [[ "$@" == "php-fpm" ]] && [[ "$WITH_APACHE" == "1" ]]; then + # Apache must be started as root. PHP-FPM reads the environment variables itself (clear_env = no): + # no need to expose them through Apache + exec apache2-fpm-foreground; else exec "sudo" "-E" "-H" "-u" "#$DOCKER_USER_ID" "$@"; fi diff --git a/utils/enable_apache_mods.php b/utils/enable_apache_mods.php index adfd835..1a75ecc 100644 --- a/utils/enable_apache_mods.php +++ b/utils/enable_apache_mods.php @@ -3,10 +3,17 @@ * Enables or disables Apache extensions based on environment variables set. */ -$defaultExtensions = ['access_compat', 'alias', 'auth_basic', 'authn_core', 'authn_file', 'authz_core', 'authz_host', 'authz_user', 'autoindex', 'deflate', 'dir', 'env', 'expires', 'filter', 'mime', 'mpm_prefork', 'negotiation', 'php'.getenv('PHP_VERSION'), 'reqtimeout', 'rewrite', 'setenvif', 'status']; +$defaultExtensions = ['access_compat', 'alias', 'auth_basic', 'authn_core', 'authn_file', 'authz_core', 'authz_host', 'authz_user', 'autoindex', 'deflate', 'dir', 'env', 'expires', 'filter', 'mime', 'negotiation', 'reqtimeout', 'rewrite', 'setenvif', 'status']; $availableExtensions = ['access_compat', 'actions', 'alias', 'allowmethods', 'asis', 'auth_basic', 'auth_digest', 'auth_form', 'authn_anon', 'authn_core', 'authn_dbd', 'authn_dbm', 'authn_file', 'authn_socache', 'authnz_fcgi', 'authnz_ldap', 'authz_core', 'authz_dbd', 'authz_dbm', 'authz_groupfile', 'authz_host', 'authz_owner', 'authz_user', 'autoindex', 'brotli', 'buffer', 'cache', 'cache_disk', 'cache_socache', 'cern_meta', 'cgi', 'cgid', 'charset_lite', 'data', 'dav', 'dav_fs', 'dav_lock', 'dbd', 'deflate', 'dialup', 'dir', 'dump_io', 'echo', 'env', 'expires', 'ext_filter', 'file_cache', 'filter', 'headers', 'heartbeat', 'heartmonitor', 'http2', 'ident', 'imagemap', 'include', 'info', 'lbmethod_bybusyness', 'lbmethod_byrequests', 'lbmethod_bytraffic', 'lbmethod_heartbeat', 'ldap', 'log_debug', 'log_forensic', 'lua', 'macro', 'md', 'mime', 'mime_magic', 'mpm_event', 'mpm_prefork', 'mpm_worker', 'negotiation', 'php'.getenv('PHP_VERSION'), 'proxy', 'proxy_ajp', 'proxy_balancer', 'proxy_connect', 'proxy_express', 'proxy_fcgi', 'proxy_fdpass', 'proxy_ftp', 'proxy_hcheck', 'proxy_html', 'proxy_http', 'proxy_http2', 'proxy_scgi', 'proxy_uwsgi', 'proxy_wstunnel', 'ratelimit', 'reflector', 'remoteip', 'reqtimeout', 'request', 'rewrite', 'sed', 'session', 'session_cookie', 'session_crypto', 'session_dbd', 'setenvif', 'slotmem_plain', 'slotmem_shm', 'socache_dbm', 'socache_memcache', 'socache_redis', 'socache_shmcb', 'speling', 'ssl', 'status', 'substitute', 'suexec', 'unique_id', 'userdir', 'usertrack', 'vhost_alias', 'xml2enc']; +if (getenv('IMAGE_VARIANT') === 'fpm') { + $defaultExtensions = array_merge($defaultExtensions, ['mpm_event', 'proxy', 'proxy_fcgi']); + $availableExtensions = array_values(array_diff($availableExtensions, ['php'.getenv('PHP_VERSION')])); +} else { + $defaultExtensions = array_merge($defaultExtensions, ['mpm_prefork', 'php'.getenv('PHP_VERSION')]); +} + $delimiter = [',', '|', ';', ':']; $replace = str_replace($delimiter, ' ', getenv('APACHE_EXTENSIONS')); $apacheExtensions = explode(' ', $replace); diff --git a/utils/fpm-docker.conf b/utils/fpm-docker.conf index 7508c50..db75bef 100644 --- a/utils/fpm-docker.conf +++ b/utils/fpm-docker.conf @@ -1,14 +1,13 @@ [global] error_log = /proc/self/fd/2 + ; https://github.com/docker-library/php/pull/725#issuecomment-443540114 log_limit = 8192 [www] -; if we send this to /proc/self/fd/1, it never appears -access.log = /proc/self/fd/2 - -clear_env = no - ; Ensure worker stdout and stderr are sent to the main error log. catch_workers_output = yes decorate_workers_output = no + +; Environment variables of the container are available in PHP +clear_env = no diff --git a/utils/fpm-zz-docker-pm.conf b/utils/fpm-zz-docker-pm.conf new file mode 100644 index 0000000..e96a591 --- /dev/null +++ b/utils/fpm-zz-docker-pm.conf @@ -0,0 +1,13 @@ +; Loaded after the www.conf file of the Ubuntu package, to override its process manager settings. +; Values are taken from the PHP_FPM_* environment variables (defaults are set in the Dockerfile). + +[www] +pm = ${PHP_FPM_PM} +pm.max_children = ${PHP_FPM_PM_MAX_CHILDREN} +pm.start_servers = ${PHP_FPM_PM_START_SERVERS} +pm.min_spare_servers = ${PHP_FPM_PM_MIN_SPARE_SERVERS} +pm.max_spare_servers = ${PHP_FPM_PM_MAX_SPARE_SERVERS} +pm.max_requests = ${PHP_FPM_PM_MAX_REQUESTS} + +; Used by php-fpm-healthcheck +ping.path = /ping diff --git a/utils/fpm-zz-docker.conf b/utils/fpm-zz-docker.conf index e0590d8..4c8f897 100644 --- a/utils/fpm-zz-docker.conf +++ b/utils/fpm-zz-docker.conf @@ -2,4 +2,7 @@ daemonize = no [www] +; if we send this to /proc/self/fd/1, it never appears +access.log = ${PHP_FPM_ACCESS_LOG} + listen = 9000 diff --git a/utils/php-fpm-healthcheck b/utils/php-fpm-healthcheck new file mode 100755 index 0000000..430c947 --- /dev/null +++ b/utils/php-fpm-healthcheck @@ -0,0 +1,6 @@ +#!/bin/bash +# Checks that PHP-FPM answers on its ping endpoint. +# Usable as a Docker HEALTHCHECK or a Kubernetes exec probe. + +SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET \ + cgi-fcgi -bind -connect "${PHP_FPM_HEALTHCHECK_ADDRESS:-127.0.0.1:9000}" 2>/dev/null | grep -q '^pong' From 63ca74319b4519e4ccf41d871d701ef2dd3e514b Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 07:34:29 +0200 Subject: [PATCH 4/8] Document the built-in Apache of the fpm variant and add its benchmark - README: announcement, migration notes from the apache variant, PHP-FPM settings - benchmarks/fpm-apache: k6 benchmark comparing the apache variant (mod_php) and the fpm variant with its built-in Apache under the same load (constant arrival rate), with the results --- README.md | 78 +++++++++++++- benchmarks/fpm-apache/.gitignore | 2 + benchmarks/fpm-apache/README.md | 31 ++++++ benchmarks/fpm-apache/app/index.php | 14 +++ benchmarks/fpm-apache/run.sh | 156 ++++++++++++++++++++++++++++ benchmarks/fpm-apache/scenario.js | 44 ++++++++ utils/README.blueprint.md | 78 +++++++++++++- 7 files changed, 399 insertions(+), 4 deletions(-) create mode 100644 benchmarks/fpm-apache/.gitignore create mode 100644 benchmarks/fpm-apache/README.md create mode 100644 benchmarks/fpm-apache/app/index.php create mode 100755 benchmarks/fpm-apache/run.sh create mode 100644 benchmarks/fpm-apache/scenario.js diff --git a/README.md b/README.md index 8328d55..f40aef7 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,31 @@ # General purpose PHP images for Docker +> **New: built-in Apache in the fpm variant (`PHP_FPM_WEB_SERVER=apache`)** +> +> The *fpm* variant can now run Apache in front of PHP-FPM, in the same container. It is a drop-in alternative to the +> *apache* variant: same Apache features (`.htaccess`, `APACHE_DOCUMENT_ROOT`, `APACHE_EXTENSION_*`), but PHP runs in +> PHP-FPM instead of `mod_php`. +> +> **Why?** `mod_php` forces Apache to dedicate a whole process (embedding PHP) to each connection, including idle +> keep-alive connections and static files. With PHP-FPM, Apache uses its threaded MPM (`mpm_event`) and only PHP +> requests reach the PHP workers. Under the same load ([benchmark](https://github.com/thecodingmachine/docker-images-php/tree/v5/benchmarks/fpm-apache)), +> PHP is not faster, but the container handles **3x more pages per second** with a p95 under 100 ms and a p99 under 1 s +> (30 vs 10 pages/s on 2 CPUs) and uses **4x less memory** (~90 vs ~380 MiB). +> +> ```bash +> $ docker run -p 80:80 -e PHP_FPM_WEB_SERVER=apache -v "$PWD":/var/www/html thecodingmachine/php:8.4-v5-fpm +> ``` +> +> Migrating from the *apache* variant: see [Built-in Apache of the fpm variant](#built-in-apache-of-the-fpm-variant) +> (for instance, `php_value` directives are not supported in `.htaccess` files). The *apache* variant is still available. + This repository contains a set of developer-friendly, general purpose PHP images for Docker. - You can enable or disable the extensions using environment variables. - You can also modify the `php.ini` settings using environment variables. - 2 types available: `slim` (no extensions preloaded) or `fat` (most common PHP extensions are built-in) - - 3 variants available: `CLI`, `apache` and `fpm` + - 3 variants available: `CLI`, `apache` and `fpm` (with an optional built-in Apache) - Fat images are bundled with [Supercronic](https://github.com/aptible/supercronic) which is a Cron compatible task runner. Cron jobs can be configured using environment variables - Fat images come with [Composer](https://getcomposer.org/) and [Prestissimo](https://github.com/hirak/prestissimo) installed - All variants can be installed with or without NodeJS (if you need to build your static assets). @@ -158,6 +177,12 @@ Example with PHP-FPM: $ docker run -p 9000:9000 --rm --name my-php-fpm -v "$PWD":/var/www/html thecodingmachine/php:8.4-v5-fpm ``` +Example with PHP-FPM and its built-in Apache (in the same container): + +```bash +$ docker run -p 80:80 --rm --name my-apache-fpm-app -e PHP_FPM_WEB_SERVER=apache -v "$PWD":/var/www/html thecodingmachine/php:8.4-v5-fpm +``` + Example with Apache + Node 24.x in a Dockerfile: **Dockerfile** @@ -312,10 +337,57 @@ you are using: | apache | `/var/www/html` | | fpm | `/var/www/html` | +## Built-in Apache of the fpm variant + +With `PHP_FPM_WEB_SERVER=apache`, the *fpm* variant runs Apache in front of PHP-FPM, in the same container: + +- Apache uses the threaded `mpm_event` MPM instead of `mpm_prefork` (required by `mod_php`): static files and + keep-alive connections no longer hold a process embedding PHP, so the memory usage is lower and the container + handles more concurrent connections (see `benchmarks/fpm-apache`). HTTP/2 can also be enabled with `APACHE_EXTENSION_HTTP2=1`. +- The Apache features of the *apache* variant are available (`.htaccess`, `APACHE_DOCUMENT_ROOT`, `APACHE_EXTENSION_*`). +- The number of PHP workers is configured independently (see [PHP-FPM settings](#php-fpm-settings)). +- If Apache or PHP-FPM stops, the other one is stopped too and the container exits (so that your orchestrator can restart it). + +It is meant to replace the *apache* variant (`mod_php`). When migrating, be aware that: + +- `php_value` and `php_flag` directives are not supported in `.htaccess` files (Apache answers with a 500 error): + use the `PHP_INI_*` environment variables or a [`.user.ini` file](https://www.php.net/manual/en/configuration.file.per-user.php) instead. +- The `Authorization` header is forwarded to PHP (`CGIPassAuth On`). +- Environment variables are read by PHP-FPM itself: they are no longer exposed through Apache (`PassEnv`). +- The PHP-FPM access log is disabled (Apache already writes one). + +## PHP-FPM settings + +For the *fpm* variant, the PHP-FPM process manager can be configured with environment variables +(the defaults are the ones of the Ubuntu package): + +| Environment variable | `php-fpm.conf` setting | Default | +|--------------------------------|-------------------------|-----------| +| `PHP_FPM_PM` | `pm` | `dynamic` | +| `PHP_FPM_PM_MAX_CHILDREN` | `pm.max_children` | `5` | +| `PHP_FPM_PM_START_SERVERS` | `pm.start_servers` | `2` | +| `PHP_FPM_PM_MIN_SPARE_SERVERS` | `pm.min_spare_servers` | `1` | +| `PHP_FPM_PM_MAX_SPARE_SERVERS` | `pm.max_spare_servers` | `3` | +| `PHP_FPM_PM_MAX_REQUESTS` | `pm.max_requests` | `0` | +| `PHP_FPM_ACCESS_LOG` | `access.log` | `/proc/self/fd/2` | + +The `php-fpm-healthcheck` command checks that PHP-FPM answers (on its `/ping` endpoint). You can use it as a Docker +healthcheck or as a Kubernetes probe: + +```yml +services: + my_app: + image: thecodingmachine/php:8.4-v5-fpm + healthcheck: + test: ["CMD", "php-fpm-healthcheck"] +``` + +The *fpm* variant (with or without Apache) is stopped gracefully (`SIGQUIT`): requests being processed are completed before the container stops. + ## Changing Apache document root -For the *apache* variant, you can change the document root of Apache (i.e. your "public" directory) by using the +For the *apache* variant and the built-in Apache of the *fpm* variant, you can change the document root of Apache (i.e. your "public" directory) by using the `APACHE_DOCUMENT_ROOT` variable: ```bash @@ -358,6 +430,8 @@ APACHE_EXTENSIONS="dav ssl" **Apache modules enabled by default:** `access_compat` `alias` `auth_basic` `authn_core` `authn_file` `authz_core` `authz_host` `authz_user` `autoindex` `deflate` `dir` `env` `expires` `filter` `mime` `mpm_prefork` `negotiation` `php8.4 (depend of your active version)` `reqtimeout` `rewrite` `setenvif` `status` +For the built-in Apache of the *fpm* variant, `mpm_event` `proxy` `proxy_fcgi` are enabled instead of `mpm_prefork` and `php8.4` (`mod_php` is not available). + **Apache modules available:** `access_compat` `actions` `alias` `allowmethods` `asis` `auth_basic` `auth_digest` `auth_form` `authn_anon` `authn_core` `authn_dbd` `authn_dbm` `authn_file` `authn_socache` `authnz_fcgi` `authnz_ldap` `authz_core` `authz_dbd` `authz_dbm` `authz_groupfile` `authz_host` `authz_owner` `authz_user` `autoindex` `brotli` `buffer` `cache` `cache_disk` `cache_socache` `cern_meta` `cgi` `cgid` `charset_lite` `data` `dav` `dav_fs` `dav_lock` `dbd` `deflate` `dialup` `dir` `dump_io` `echo` `env` `ext_filter` `expires` `file_cache` `filter` `headers` `heartbeat` `heartmonitor` `http2` `ident` `imagemap` `include` `info` `lbmethod_bybusyness` `lbmethod_byrequests` `lbmethod_bytraffic` `lbmethod_heartbeat` `ldap` `log_debug` `log_forensic` `lua` `macro` `md` `mime` `mime_magic` `mpm_event` `mpm_prefork` `mpm_worker` `negotiation` `php8.4 (depend of your active version)` `proxy` `proxy_ajp` `proxy_balancer` `proxy_connect` `proxy_express` `proxy_fcgi` `proxy_fdpass` `proxy_ftp` `proxy_hcheck` `proxy_html` `proxy_http` `proxy_http2` `proxy_scgi` `proxy_wstunnel` `ratelimit` `reflector` `remoteip` `reqtimeout` `request` `rewrite` `sed` `session` `session_cookie` `session_crypto` `session_dbd` `setenvif` `slotmem_plain` `slotmem_shm` `socache_dbm` `socache_memcache` `socache_redis` `socache_shmcb` `speling` `ssl` `status` `substitute` `suexec` `unique_id` `userdir` `usertrack` `vhost_alias` `xml2enc` This list can be outdated, you can verify by executing : `docker run --rm -it thecodingmachine/php:8.4-v5-slim-apache a2enmod` diff --git a/benchmarks/fpm-apache/.gitignore b/benchmarks/fpm-apache/.gitignore new file mode 100644 index 0000000..835c020 --- /dev/null +++ b/benchmarks/fpm-apache/.gitignore @@ -0,0 +1,2 @@ +app/assets/ +results/ diff --git a/benchmarks/fpm-apache/README.md b/benchmarks/fpm-apache/README.md new file mode 100644 index 0000000..c9d82de --- /dev/null +++ b/benchmarks/fpm-apache/README.md @@ -0,0 +1,31 @@ +# Benchmark: `apache` (mod_php) vs `fpm` with its built-in Apache + +Both variants run the same application with their default Apache settings and the same resources (2 CPUs, 2 GB), +and receive exactly the same load: a constant number of pages per second. A page is a PHP request (~3 ms of CPU and +20 ms of I/O) and 10 static assets, like a browser. A variant handles a rate when its p95 stays under 100 ms and its +p99 under 1 s, without error. + +## Results (PHP 8.4, amd64) + +| | `apache` (mod_php) | `fpm` + built-in Apache | +|---|---|---| +| Pages per second handled | 10 | **30** | +| PHP requests per second handled (no assets) | < 100 | **200** | +| Memory | 280-385 MiB | **80-100 MiB** | + +The `fpm` variant serves **2 to 3 times more traffic with 4 times less memory**. + +With `mod_php`, every connection holds an Apache process embedding PHP, including idle keep-alive connections (a +browser opens up to 6): from 15 pages/s the 150 processes are exhausted and requests wait for up to 30 s, while the +CPU is almost idle. With PHP-FPM, Apache (`mpm_event`) keeps the connections with a few threads and only PHP +requests use the 20 PHP workers. + +Single run on a developer machine: the orders of magnitude are reliable, not the exact values. + +## Running it + +Requires Docker only ([k6](https://k6.io/) runs in a container): + +```bash +./run.sh 8.4 +``` diff --git a/benchmarks/fpm-apache/app/index.php b/benchmarks/fpm-apache/app/index.php new file mode 100644 index 0000000..f2ed4fa --- /dev/null +++ b/benchmarks/fpm-apache/app/index.php @@ -0,0 +1,14 @@ +'; +for ($i = 1; $i <= 10; $i++) { + $html .= ''; +} +$html .= '' . str_repeat('

' . $data[array_rand($data)] . '

', 200) . ''; +echo $html; diff --git a/benchmarks/fpm-apache/run.sh b/benchmarks/fpm-apache/run.sh new file mode 100755 index 0000000..eccad43 --- /dev/null +++ b/benchmarks/fpm-apache/run.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# Compares the "apache" variant (mod_php, mpm_prefork) and the "fpm" variant with its built-in Apache +# (PHP_FPM_WEB_SERVER=apache: mpm_event + PHP-FPM), labelled "fpm-apache". +# +# Both variants receive exactly the same load: a constant number of pages per second (open model), +# for several rates. The capacity of a variant is the highest rate served with a page p95 under +# MAX_P95 ms and a p99 under MAX_P99 ms (no request stuck), without error nor dropped page. +# +# Usage: ./run.sh [php version] +# Env: RATES, PHP_RATES, DURATION, MAX_P95, MAX_P99, CPUS, MEMORY, FPM_MAX_CHILDREN, REPO, TAG_PREFIX, +# SUMMARY_ONLY=1 (only print the summary of the previous run) +set -e +cd "$(dirname "$0")" + +PHP_VERSION="${1:-8.4}" +REPO="${REPO:-thecodingmachine/php}" +TAG_PREFIX="${TAG_PREFIX:-}" +RATES="${RATES:-10 15 20 30 40 60 80}" +PHP_RATES="${PHP_RATES:-100 200 300}" +DURATION="${DURATION:-30s}" +MAX_P95="${MAX_P95:-100}" +MAX_P99="${MAX_P99:-1000}" +CPUS="${CPUS:-2}" +MEMORY="${MEMORY:-2g}" +FPM_MAX_CHILDREN="${FPM_MAX_CHILDREN:-20}" +NETWORK="bench-fpm-apache" +RESULTS="results" +VARIANTS="apache fpm-apache" + +if [[ "$SUMMARY_ONLY" != "1" ]]; then +mkdir -p "$RESULTS" app/assets +for i in $(seq 1 10); do + [[ -f "app/assets/asset$i.css" ]] || head -c 30000 /dev/urandom | base64 > "app/assets/asset$i.css" +done +docker network create "$NETWORK" > /dev/null 2>&1 || true + +k6() { + docker run --rm -u "$(id -u)" --network "$NETWORK" -v "$PWD":/bench grafana/k6 run -q "$@" /bench/scenario.js +} + +# run_level +run_level() { + local variant="$1" container="$2" result="$3"; shift 3 + # Sample memory and CPU of the server during the run + (while true; do + docker stats --no-stream --format '{{.MemUsage}};{{.CPUPerc}}' "$container" 2>/dev/null || break + done) > "$RESULTS/${variant}-${result}-stats.log" & + local stats_pid=$! + k6 -e DURATION="$DURATION" "$@" \ + --summary-export "/bench/$RESULTS/${variant}-${result}-summary.json" > "$RESULTS/${variant}-${result}-k6.log" 2>&1 || true + kill "$stats_pid" 2>/dev/null || true + wait "$stats_pid" 2>/dev/null || true + # Let idle connections and processes settle between levels + sleep 10 +} + +bench() { + local variant="$1"; local image_variant="$2"; shift 2 + local name="bench-${variant}" + docker rm -f "$name" > /dev/null 2>&1 || true + docker run -d --name "$name" --network "$NETWORK" --network-alias app \ + --cpus "$CPUS" --memory "$MEMORY" \ + -e TEMPLATE_PHP_INI=production "$@" \ + -v "$PWD/app":/var/www/html:ro \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-v5-slim-${image_variant}" > /dev/null + sleep 5 + # Warm up (opcache, process spawning) + k6 -e RATE=5 -e DURATION=10s > /dev/null 2>&1 + for rate in $RATES; do + run_level "$variant" "$name" "page-${rate}" -e RATE="$rate" + done + for rate in $PHP_RATES; do + run_level "$variant" "$name" "php-${rate}" -e RATE="$rate" -e PHP_ONLY=1 + done + docker logs "$name" > "$RESULTS/${variant}-server.log" 2>&1 + docker rm -f "$name" > /dev/null +} + +bench apache apache +bench fpm-apache fpm -e PHP_FPM_WEB_SERVER=apache -e PHP_FPM_PM=static -e PHP_FPM_PM_MAX_CHILDREN="$FPM_MAX_CHILDREN" +docker network rm "$NETWORK" > /dev/null +fi + +# |-- Summary ---------------------------------------------------------------- +# metrics : p50, p95, p99, max, errors (%), dropped +metrics() { + jq -r '[ + (.metrics["group_duration{group:::page}"]["p(50)"] | floor), + (.metrics["group_duration{group:::page}"]["p(95)"] | floor), + (.metrics["group_duration{group:::page}"]["p(99)"] | floor), + (.metrics["group_duration{group:::page}"].max | floor), + (.metrics.http_req_failed.value * 1000 | floor / 10), + (.metrics.dropped_iterations.count // 0) + ] | map(tostring) | join(" ")' "$1" +} +max_mib() { + cut -d';' -f1 "$1" | cut -d'/' -f1 | sed 's/ //g' | awk ' + /GiB$/ {v=$0; sub(/GiB/,"",v); v*=1024} + /MiB$/ {v=$0; sub(/MiB/,"",v)} + /KiB$/ {v=$0; sub(/KiB/,"",v); v/=1024} + {if (v>m) m=v} END {printf "%d", m}' +} +avg_cpu() { + cut -d';' -f2 "$1" | tr -d '%' | awk '$1>1 {s+=$1; n++} END {printf "%d", (n ? s/n : 0)}' +} +# table