From f3e3e648f05261b24d75a9ba5ad4e7dff04dc9a5 Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Mon, 12 Oct 2026 09:01:35 +0200 Subject: [PATCH 1/5] Add APACHE_HTTP_PORT and APACHE_HTTPS_PORT Apache can listen on unprivileged ports, e.g. in a Kubernetes cluster that forbids binding the ports below 1024. Closes #360 --- CHANGELOG.md | 3 +++ Dockerfile.slim.apache | 7 +++++++ README.md | 11 +++++++++++ tests-suite/variant-apache.sh | 16 +++++++++++++++- utils/Dockerfile.slim.blueprint | 7 +++++++ utils/README.blueprint.md | 11 +++++++++++ 6 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca53797..f63e0aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ### Minor changes +* **2026-10-12** + * Add `APACHE_HTTP_PORT` and `APACHE_HTTPS_PORT` to change the ports Apache listens on (e.g. unprivileged ports) + * **2026-10-09** * Imagick can read SVG images (also WMF, DjVu and OpenEXR): the fat images are ~17MB larger * Add the `opentelemetry` extension on PHP 8.2+ diff --git a/Dockerfile.slim.apache b/Dockerfile.slim.apache index 24b60e1..bad874f 100644 --- a/Dockerfile.slim.apache +++ b/Dockerfile.slim.apache @@ -223,6 +223,13 @@ 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 +ENV APACHE_HTTP_PORT=80 \ + APACHE_HTTPS_PORT=443 + +RUN sed -ri -e 's/^Listen 80$/Listen ${APACHE_HTTP_PORT}/' -e 's/^(\s*)Listen 443$/\1Listen ${APACHE_HTTPS_PORT}/' /etc/apache2/ports.conf && \ + sed -i 's///' /etc/apache2/sites-available/000-default.conf && \ + sed -i 's///' /etc/apache2/sites-available/default-ssl.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 diff --git a/README.md b/README.md index a736dfd..6505c19 100644 --- a/README.md +++ b/README.md @@ -334,6 +334,17 @@ APACHE_DOCUMENT_ROOT=public/ APACHE_DOCUMENT_ROOT=/var/www/html/public ``` +## Changing Apache ports + +For the *apache* variant, Apache listens on port 80 (and 443 when the `ssl` extension is enabled). +You can change these ports with the `APACHE_HTTP_PORT` and `APACHE_HTTPS_PORT` variables, for instance to use +unprivileged ports: + +```bash +APACHE_HTTP_PORT=8080 +APACHE_HTTPS_PORT=8443 +``` + ## Enabling/disabling Apache extensions You can enable/disable Apache extensions using the `APACHE_EXTENSION_[extension_name]` environment variable. diff --git a/tests-suite/variant-apache.sh b/tests-suite/variant-apache.sh index 3cd6175..5cd80f8 100755 --- a/tests-suite/variant-apache.sh +++ b/tests-suite/variant-apache.sh @@ -52,6 +52,13 @@ test_enableApacheModule() { assert_not_matches "does not exist" "$RESULT" "a2enmod/a2dismod received an unknown module" } ############################################################ +## Apache listens on APACHE_HTTP_PORT +############################################################ +test_httpPort() { + RESULT="$(curl -sq http://localhost:${DOCKER4_PORT}/apache/ 2>&1)" + assert_equals "foo" "$RESULT" "Apache does not listen on APACHE_HTTP_PORT" +} +############################################################ ## A stop requested during the initialization is not lost ############################################################ test_stopDuringStartup() { @@ -85,13 +92,20 @@ setup_suite() { docker run --name "${DOCKER3_NAME}" ${RUN_OPTIONS} --rm -e MYVAR=foo -e APACHE_DOCUMENT_ROOT=/var/www/foo/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" + # SETUP apache4 + export DOCKER4_PORT="$(unused_port)" + export DOCKER4_NAME="test-apache4-${DOCKER4_PORT}" + docker run --name "${DOCKER4_NAME}" ${RUN_OPTIONS} --rm -e MYVAR=foo -e APACHE_HTTP_PORT=8080 -p "${DOCKER4_PORT}:8080" -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 Apache to start waitfor http://localhost:${DOCKER1_PORT} waitfor http://localhost:${DOCKER2_PORT} waitfor http://localhost:${DOCKER3_PORT} + waitfor http://localhost:${DOCKER4_PORT} } teardown_suite() { - docker stop "${DOCKER1_NAME}" "${DOCKER2_NAME}" "${DOCKER3_NAME}" > /dev/null 2>&1 + docker stop "${DOCKER1_NAME}" "${DOCKER2_NAME}" "${DOCKER3_NAME}" "${DOCKER4_NAME}" > /dev/null 2>&1 docker rm -f "${STOP_NAME}" > /dev/null 2>&1 } diff --git a/utils/Dockerfile.slim.blueprint b/utils/Dockerfile.slim.blueprint index 198ddd9..c57fe7b 100644 --- a/utils/Dockerfile.slim.blueprint +++ b/utils/Dockerfile.slim.blueprint @@ -222,6 +222,13 @@ 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 +ENV APACHE_HTTP_PORT=80 \ + APACHE_HTTPS_PORT=443 + +RUN sed -ri -e 's/^Listen 80$/Listen ${APACHE_HTTP_PORT}/' -e 's/^(\s*)Listen 443$/\1Listen ${APACHE_HTTPS_PORT}/' /etc/apache2/ports.conf && \ + sed -i 's///' /etc/apache2/sites-available/000-default.conf && \ + sed -i 's///' /etc/apache2/sites-available/default-ssl.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 diff --git a/utils/README.blueprint.md b/utils/README.blueprint.md index 67c628d..7dbfdad 100644 --- a/utils/README.blueprint.md +++ b/utils/README.blueprint.md @@ -264,6 +264,17 @@ APACHE_DOCUMENT_ROOT=public/ APACHE_DOCUMENT_ROOT=/var/www/html/public ``` +## Changing Apache ports + +For the *apache* variant, Apache listens on port 80 (and 443 when the `ssl` extension is enabled). +You can change these ports with the `APACHE_HTTP_PORT` and `APACHE_HTTPS_PORT` variables, for instance to use +unprivileged ports: + +```bash +APACHE_HTTP_PORT=8080 +APACHE_HTTPS_PORT=8443 +``` + ## Enabling/disabling Apache extensions You can enable/disable Apache extensions using the `APACHE_EXTENSION_[extension_name]` environment variable. From 640f0adee0b54703247956026cf0bece0554fcc5 Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Mon, 12 Oct 2026 09:02:25 +0200 Subject: [PATCH 2/5] Add COMPOSER_VERSION to install another Composer version The images come with the latest Composer version, which can break a project (e.g. Composer 2.5 rejects the packages with an invalid lock file). COMPOSER_VERSION takes an exact version or a channel (1, 2, 2.2): it is installed at the container start, or at the build of a slim image with an ARG. Closes #351 --- CHANGELOG.md | 1 + Dockerfile.slim.apache | 11 ++++++++ Dockerfile.slim.cli | 11 ++++++++ Dockerfile.slim.fpm | 11 ++++++++ README.md | 17 ++++++++++++ tests-suite/tool-composer-version.sh | 39 ++++++++++++++++++++++++++++ utils/Dockerfile.slim.blueprint | 11 ++++++++ utils/README.blueprint.md | 17 ++++++++++++ utils/docker-entrypoint-as-root.sh | 4 +++ utils/select_composer_version.sh | 20 ++++++++++++++ 10 files changed, 142 insertions(+) create mode 100755 tests-suite/tool-composer-version.sh create mode 100755 utils/select_composer_version.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index f63e0aa..b4edaec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * **2026-10-12** * Add `APACHE_HTTP_PORT` and `APACHE_HTTPS_PORT` to change the ports Apache listens on (e.g. unprivileged ports) + * Add `COMPOSER_VERSION` to install another Composer version (exact version or `1`, `2`, `2.2` channel), at the container start or at the build of a slim image * **2026-10-09** * Imagick can read SVG images (also WMF, DjVu and OpenEXR): the fat images are ~17MB larger diff --git a/Dockerfile.slim.apache b/Dockerfile.slim.apache index bad874f..d58ad92 100644 --- a/Dockerfile.slim.apache +++ b/Dockerfile.slim.apache @@ -110,6 +110,7 @@ RUN rm /etc/php/${PHP_VERSION}/cli/php.ini RUN curl -sS --retry 5 --retry-delay 2 https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer &&\ chmod +x /usr/local/bin/composer +COPY utils/select_composer_version.sh /usr/local/bin/select_composer_version.sh # TODO: utils.php in /usr/local/bin... bof! COPY utils/utils.php /usr/local/bin/utils.php @@ -395,6 +396,16 @@ ONBUILD RUN if [ -n "$INSTALL_CRON" ]; then \ && sudo ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic; \ fi +# |-------------------------------------------------------------------------- +# | Composer version +# |-------------------------------------------------------------------------- +# | +# | Installs the Composer version COMPOSER_VERSION instead of the latest one. +# | +ONBUILD ARG COMPOSER_VERSION +ONBUILD RUN if [ -n "$COMPOSER_VERSION" ]; then \ + sudo /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION"; \ + fi # |-------------------------------------------------------------------------- # | NodeJS diff --git a/Dockerfile.slim.cli b/Dockerfile.slim.cli index 5a242b8..0c8bf2f 100644 --- a/Dockerfile.slim.cli +++ b/Dockerfile.slim.cli @@ -110,6 +110,7 @@ RUN rm /etc/php/${PHP_VERSION}/cli/php.ini RUN curl -sS --retry 5 --retry-delay 2 https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer &&\ chmod +x /usr/local/bin/composer +COPY utils/select_composer_version.sh /usr/local/bin/select_composer_version.sh # TODO: utils.php in /usr/local/bin... bof! COPY utils/utils.php /usr/local/bin/utils.php @@ -293,6 +294,16 @@ ONBUILD RUN if [ -n "$INSTALL_CRON" ]; then \ && sudo ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic; \ fi +# |-------------------------------------------------------------------------- +# | Composer version +# |-------------------------------------------------------------------------- +# | +# | Installs the Composer version COMPOSER_VERSION instead of the latest one. +# | +ONBUILD ARG COMPOSER_VERSION +ONBUILD RUN if [ -n "$COMPOSER_VERSION" ]; then \ + sudo /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION"; \ + fi # |-------------------------------------------------------------------------- # | NodeJS diff --git a/Dockerfile.slim.fpm b/Dockerfile.slim.fpm index 3633cc2..2a4c346 100644 --- a/Dockerfile.slim.fpm +++ b/Dockerfile.slim.fpm @@ -110,6 +110,7 @@ RUN rm /etc/php/${PHP_VERSION}/cli/php.ini RUN curl -sS --retry 5 --retry-delay 2 https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer &&\ chmod +x /usr/local/bin/composer +COPY utils/select_composer_version.sh /usr/local/bin/select_composer_version.sh # TODO: utils.php in /usr/local/bin... bof! COPY utils/utils.php /usr/local/bin/utils.php @@ -316,6 +317,16 @@ ONBUILD RUN if [ -n "$INSTALL_CRON" ]; then \ && sudo ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic; \ fi +# |-------------------------------------------------------------------------- +# | Composer version +# |-------------------------------------------------------------------------- +# | +# | Installs the Composer version COMPOSER_VERSION instead of the latest one. +# | +ONBUILD ARG COMPOSER_VERSION +ONBUILD RUN if [ -n "$COMPOSER_VERSION" ]; then \ + sudo /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION"; \ + fi # |-------------------------------------------------------------------------- # | NodeJS diff --git a/README.md b/README.md index 6505c19..e3e1af0 100644 --- a/README.md +++ b/README.md @@ -417,6 +417,23 @@ Beware! The `ARG NODE_VERSION` command must be written before the `FROM`. This i `NODE_VERSION` can take any valid node versions (tested only with the versions provided here but should work too for some previous versions) +## Composer version + +The images come with the latest Composer version. You can install another one with the `COMPOSER_VERSION` environment +variable: an exact version (e.g. `2.4.4`) or a channel (`1`, `2`, or `2.2` for the LTS version). + +```bash +COMPOSER_VERSION=2.4.4 +``` + +The version is installed when the container starts (this requires a network access). With the *slim* images, you can +install it during the build instead, with an ARG before the `FROM`: + +```Dockerfile +ARG COMPOSER_VERSION=2.4.4 +FROM thecodingmachine/php:8.4-v5-slim-apache +``` + ## Permissions Ever faced file permission issues with Docker? Good news, this is a thing of the past! diff --git a/tests-suite/tool-composer-version.sh b/tests-suite/tool-composer-version.sh new file mode 100755 index 0000000..ffba447 --- /dev/null +++ b/tests-suite/tool-composer-version.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +. ./config + +############################################################ +## COMPOSER_VERSION installs an exact version at the container start +############################################################ +test_exactVersion() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e COMPOSER_VERSION=2.4.4 \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" composer --version --no-ansi 2>/dev/null | tail -n +1 | grep -oP '^Composer version \K\S+')" + assert_equals "2.4.4" "$RESULT" +} +############################################################ +## COMPOSER_VERSION installs the latest version of a channel +############################################################ +test_channel() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e COMPOSER_VERSION=2.2 \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" composer --version --no-ansi 2>/dev/null | tail -n +1 | grep -oP '^Composer version \K\S+')" + assert_matches "^2\.2\.[0-9]+$" "$RESULT" +} +############################################################ +## ARG COMPOSER_VERSION installs the version during the build +############################################################ +test_onbuild() { + docker ${BUILDTOOL} -t "${DOCKER1_NAME}" - > /dev/null 2>&1 </dev/null | tail -n +1 | grep -oP '^Composer version \K\S+')" + assert_equals "2.4.4" "$RESULT" +} + +setup_suite() { + export DOCKER1_NAME="test/composer_version_$(unused_port)" +} + +teardown_suite() { + docker rmi --force "${DOCKER1_NAME}" > /dev/null 2>&1 +} diff --git a/utils/Dockerfile.slim.blueprint b/utils/Dockerfile.slim.blueprint index c57fe7b..bd30256 100644 --- a/utils/Dockerfile.slim.blueprint +++ b/utils/Dockerfile.slim.blueprint @@ -109,6 +109,7 @@ RUN rm /etc/php/${PHP_VERSION}/cli/php.ini RUN curl -sS --retry 5 --retry-delay 2 https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer &&\ chmod +x /usr/local/bin/composer +COPY utils/select_composer_version.sh /usr/local/bin/select_composer_version.sh # TODO: utils.php in /usr/local/bin... bof! COPY utils/utils.php /usr/local/bin/utils.php @@ -428,6 +429,16 @@ ONBUILD RUN if [ -n "$INSTALL_CRON" ]; then \ && sudo ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic; \ fi +# |-------------------------------------------------------------------------- +# | Composer version +# |-------------------------------------------------------------------------- +# | +# | Installs the Composer version COMPOSER_VERSION instead of the latest one. +# | +ONBUILD ARG COMPOSER_VERSION +ONBUILD RUN if [ -n "$COMPOSER_VERSION" ]; then \ + sudo /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION"; \ + fi # |-------------------------------------------------------------------------- # | NodeJS diff --git a/utils/README.blueprint.md b/utils/README.blueprint.md index 7dbfdad..b6e4dd3 100644 --- a/utils/README.blueprint.md +++ b/utils/README.blueprint.md @@ -347,6 +347,23 @@ Beware! The `ARG NODE_VERSION` command must be written before the `FROM`. This i `NODE_VERSION` can take any valid node versions (tested only with the versions provided here but should work too for some previous versions) +## Composer version + +The images come with the latest Composer version. You can install another one with the `COMPOSER_VERSION` environment +variable: an exact version (e.g. `2.4.4`) or a channel (`1`, `2`, or `2.2` for the LTS version). + +```bash +COMPOSER_VERSION=2.4.4 +``` + +The version is installed when the container starts (this requires a network access). With the *slim* images, you can +install it during the build instead, with an ARG before the `FROM`: + +```Dockerfile +ARG COMPOSER_VERSION=2.4.4 +FROM thecodingmachine/php:{{ $image.php_version }}-v5-slim-apache +``` + ## Permissions Ever faced file permission issues with Docker? Good news, this is a thing of the past! diff --git a/utils/docker-entrypoint-as-root.sh b/utils/docker-entrypoint-as-root.sh index 539ea9f..88c15c2 100755 --- a/utils/docker-entrypoint-as-root.sh +++ b/utils/docker-entrypoint-as-root.sh @@ -146,6 +146,10 @@ sudo chown docker:docker /opt/php_env_var_cache.php /usr/bin/real_php -d display_errors=stderr /usr/local/bin/generate_conf.php > /etc/php/${PHP_VERSION}/mods-available/generated_conf.ini PHP_VERSION="${PHP_VERSION}" /usr/bin/real_php -d display_errors=stderr /usr/local/bin/setup_extensions.php | sudo bash +if [[ -n "$COMPOSER_VERSION" ]]; then + /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION" +fi + # output on the logs can be done by writing on the "tini" PID. Useful for CRONTAB TINI_PID=`ps -e | grep tini | awk '{print $1;}'` /usr/bin/real_php -d display_errors=stderr /usr/local/bin/generate_cron.php $TINI_PID > /tmp/generated_crontab diff --git a/utils/select_composer_version.sh b/utils/select_composer_version.sh new file mode 100755 index 0000000..bfb4dbe --- /dev/null +++ b/utils/select_composer_version.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Installs the Composer version $1: an exact version (e.g. 2.4.4) or a channel (1, 2, or 2.2 for the LTS) + +set -e + +CURRENT_VERSION=$(composer --version --no-ansi 2>/dev/null | grep -oP '^Composer version \K\S+' || true) + +case "$1" in + 1|2|2.2) + [[ "$CURRENT_VERSION" == "$1".* ]] && exit 0 + SELF_UPDATE_ARG="--$1" + ;; + *) + [[ "$CURRENT_VERSION" == "$1" ]] && exit 0 + SELF_UPDATE_ARG="$1" + ;; +esac + +HOME=/root COMPOSER_HOME=/root/.config/composer COMPOSER_ALLOW_SUPERUSER=1 \ + composer self-update --no-interaction --no-progress --clean-backups "$SELF_UPDATE_ARG" From 990f272f2930d4ac8f05afe56b6e4638ffba4aeb Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Mon, 12 Oct 2026 09:03:09 +0200 Subject: [PATCH 3/5] Add LOCALES to generate locales at the container start Only the C, C.UTF-8 and POSIX locales were available: generating another one required a custom Dockerfile. The locales package is installed in the fat images and with ARG INSTALL_LOCALES=1 in the slim images. Closes #212 --- CHANGELOG.md | 1 + Dockerfile.apache | 1 + Dockerfile.cli | 1 + Dockerfile.fpm | 1 + Dockerfile.slim.apache | 14 ++++++++++++++ Dockerfile.slim.cli | 14 ++++++++++++++ Dockerfile.slim.fpm | 14 ++++++++++++++ README.md | 19 +++++++++++++++++++ tests-suite/tool-locales.sh | 20 ++++++++++++++++++++ utils/Dockerfile.blueprint | 1 + utils/Dockerfile.slim.blueprint | 14 ++++++++++++++ utils/README.blueprint.md | 19 +++++++++++++++++++ utils/docker-entrypoint-as-root.sh | 8 ++++++++ 13 files changed, 127 insertions(+) create mode 100755 tests-suite/tool-locales.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index b4edaec..6ced343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * **2026-10-12** * Add `APACHE_HTTP_PORT` and `APACHE_HTTPS_PORT` to change the ports Apache listens on (e.g. unprivileged ports) * Add `COMPOSER_VERSION` to install another Composer version (exact version or `1`, `2`, `2.2` channel), at the container start or at the build of a slim image + * Add `LOCALES` to generate locales at the container start (e.g. `LOCALES=fr_FR.UTF-8 de_DE.UTF-8`): the `locales` package is installed in the fat images (~17MB) and with `ARG INSTALL_LOCALES=1` in the slim images * **2026-10-09** * Imagick can read SVG images (also WMF, DjVu and OpenEXR): the fat images are ~17MB larger diff --git a/Dockerfile.apache b/Dockerfile.apache index bfc9465..abd1ae2 100644 --- a/Dockerfile.apache +++ b/Dockerfile.apache @@ -2,6 +2,7 @@ # DO NOT EDIT THIS FILE : Make yours changes in /utils/Dockerfile.*.blueprint) ARG INSTALL_CRON=1 ARG INSTALL_COMPOSER=1 +ARG INSTALL_LOCALES=1 ARG PHP_VERSION="8.4" ARG GLOBAL_VERSION="v5" ARG REPO="thecodingmachine/php" diff --git a/Dockerfile.cli b/Dockerfile.cli index 6db6956..61d9127 100644 --- a/Dockerfile.cli +++ b/Dockerfile.cli @@ -2,6 +2,7 @@ # DO NOT EDIT THIS FILE : Make yours changes in /utils/Dockerfile.*.blueprint) ARG INSTALL_CRON=1 ARG INSTALL_COMPOSER=1 +ARG INSTALL_LOCALES=1 ARG PHP_VERSION="8.4" ARG GLOBAL_VERSION="v5" ARG REPO="thecodingmachine/php" diff --git a/Dockerfile.fpm b/Dockerfile.fpm index 7b3b4c2..bfa0abb 100644 --- a/Dockerfile.fpm +++ b/Dockerfile.fpm @@ -2,6 +2,7 @@ # DO NOT EDIT THIS FILE : Make yours changes in /utils/Dockerfile.*.blueprint) ARG INSTALL_CRON=1 ARG INSTALL_COMPOSER=1 +ARG INSTALL_LOCALES=1 ARG PHP_VERSION="8.4" ARG GLOBAL_VERSION="v5" ARG REPO="thecodingmachine/php" diff --git a/Dockerfile.slim.apache b/Dockerfile.slim.apache index d58ad92..d1abd62 100644 --- a/Dockerfile.slim.apache +++ b/Dockerfile.slim.apache @@ -407,6 +407,20 @@ ONBUILD RUN if [ -n "$COMPOSER_VERSION" ]; then \ sudo /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION"; \ fi +# |-------------------------------------------------------------------------- +# | Locales +# |-------------------------------------------------------------------------- +# | +# | Installs the tools generating the locales listed in LOCALES at the container start. +# | +ONBUILD ARG INSTALL_LOCALES +ONBUILD RUN if [ -n "$INSTALL_LOCALES" ]; then \ + sudo apt-get update \ + && sudo apt-get install -y --no-install-recommends locales \ + && sudo apt-get clean \ + && sudo rm -rf /var/lib/apt/lists/* /var/cache/apt/*.bin /tmp/* /var/tmp/* /usr/share/doc/*; \ + fi + # |-------------------------------------------------------------------------- # | NodeJS # |-------------------------------------------------------------------------- diff --git a/Dockerfile.slim.cli b/Dockerfile.slim.cli index 0c8bf2f..9971d30 100644 --- a/Dockerfile.slim.cli +++ b/Dockerfile.slim.cli @@ -305,6 +305,20 @@ ONBUILD RUN if [ -n "$COMPOSER_VERSION" ]; then \ sudo /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION"; \ fi +# |-------------------------------------------------------------------------- +# | Locales +# |-------------------------------------------------------------------------- +# | +# | Installs the tools generating the locales listed in LOCALES at the container start. +# | +ONBUILD ARG INSTALL_LOCALES +ONBUILD RUN if [ -n "$INSTALL_LOCALES" ]; then \ + sudo apt-get update \ + && sudo apt-get install -y --no-install-recommends locales \ + && sudo apt-get clean \ + && sudo rm -rf /var/lib/apt/lists/* /var/cache/apt/*.bin /tmp/* /var/tmp/* /usr/share/doc/*; \ + fi + # |-------------------------------------------------------------------------- # | NodeJS # |-------------------------------------------------------------------------- diff --git a/Dockerfile.slim.fpm b/Dockerfile.slim.fpm index 2a4c346..5eb7f02 100644 --- a/Dockerfile.slim.fpm +++ b/Dockerfile.slim.fpm @@ -328,6 +328,20 @@ ONBUILD RUN if [ -n "$COMPOSER_VERSION" ]; then \ sudo /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION"; \ fi +# |-------------------------------------------------------------------------- +# | Locales +# |-------------------------------------------------------------------------- +# | +# | Installs the tools generating the locales listed in LOCALES at the container start. +# | +ONBUILD ARG INSTALL_LOCALES +ONBUILD RUN if [ -n "$INSTALL_LOCALES" ]; then \ + sudo apt-get update \ + && sudo apt-get install -y --no-install-recommends locales \ + && sudo apt-get clean \ + && sudo rm -rf /var/lib/apt/lists/* /var/cache/apt/*.bin /tmp/* /var/tmp/* /usr/share/doc/*; \ + fi + # |-------------------------------------------------------------------------- # | NodeJS # |-------------------------------------------------------------------------- diff --git a/README.md b/README.md index e3e1af0..698c794 100644 --- a/README.md +++ b/README.md @@ -434,6 +434,25 @@ ARG COMPOSER_VERSION=2.4.4 FROM thecodingmachine/php:8.4-v5-slim-apache ``` +## Locales + +Only the `C`, `C.UTF-8` and `POSIX` locales are available by default. The `LOCALES` environment variable lists the +locales to generate at the container start: + +```bash +LOCALES=fr_FR.UTF-8 de_DE.UTF-8 +# Optional: the default locale of the intl extension +PHP_INI_INTL__DEFAULT_LOCALE=fr_FR.UTF-8 +``` + +The locales tools are **installed by default in the fat images**. If you are using the "*slim*" images, you need to +install them with an ARG before the `FROM`: + +```Dockerfile +ARG INSTALL_LOCALES=1 +FROM thecodingmachine/php:8.4-v5-slim-apache +``` + ## Permissions Ever faced file permission issues with Docker? Good news, this is a thing of the past! diff --git a/tests-suite/tool-locales.sh b/tests-suite/tool-locales.sh new file mode 100755 index 0000000..eb041a9 --- /dev/null +++ b/tests-suite/tool-locales.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +. ./config + +############################################################ +## The locales listed in LOCALES are available +############################################################ +test_generateLocales() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e LOCALES="fr_FR.UTF-8 de_DE.UTF-8" \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-${BRANCH_VARIANT}${ARCH_SUFFIX}" \ + php -r 'echo setlocale(LC_ALL, "fr_FR.UTF-8"), " ", setlocale(LC_ALL, "de_DE.UTF-8");' 2>/dev/null)" + assert_equals "fr_FR.UTF-8 de_DE.UTF-8" "$RESULT" +} +############################################################ +## LOCALES fails with a message in a slim image without INSTALL_LOCALES +############################################################ +test_displayErrorWhenMissing() { + RESULT=$(docker run ${RUN_OPTIONS} --rm -e LOCALES="fr_FR.UTF-8" \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" sleep 1 2>&1 | tail -n +1 | grep -o 'Locales are not available in this image') + assert_equals "Locales are not available in this image" "$RESULT" +} diff --git a/utils/Dockerfile.blueprint b/utils/Dockerfile.blueprint index 09714c9..42e49a3 100644 --- a/utils/Dockerfile.blueprint +++ b/utils/Dockerfile.blueprint @@ -1,6 +1,7 @@ #syntax=docker/dockerfile-upstream:1 ARG INSTALL_CRON=1 ARG INSTALL_COMPOSER=1 +ARG INSTALL_LOCALES=1 ARG PHP_VERSION="8.4" ARG GLOBAL_VERSION="v5" ARG REPO="thecodingmachine/php" diff --git a/utils/Dockerfile.slim.blueprint b/utils/Dockerfile.slim.blueprint index bd30256..8b95856 100644 --- a/utils/Dockerfile.slim.blueprint +++ b/utils/Dockerfile.slim.blueprint @@ -440,6 +440,20 @@ ONBUILD RUN if [ -n "$COMPOSER_VERSION" ]; then \ sudo /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION"; \ fi +# |-------------------------------------------------------------------------- +# | Locales +# |-------------------------------------------------------------------------- +# | +# | Installs the tools generating the locales listed in LOCALES at the container start. +# | +ONBUILD ARG INSTALL_LOCALES +ONBUILD RUN if [ -n "$INSTALL_LOCALES" ]; then \ + sudo apt-get update \ + && sudo apt-get install -y --no-install-recommends locales \ + && sudo apt-get clean \ + && sudo rm -rf /var/lib/apt/lists/* /var/cache/apt/*.bin /tmp/* /var/tmp/* /usr/share/doc/*; \ + fi + # |-------------------------------------------------------------------------- # | NodeJS # |-------------------------------------------------------------------------- diff --git a/utils/README.blueprint.md b/utils/README.blueprint.md index b6e4dd3..7569dc0 100644 --- a/utils/README.blueprint.md +++ b/utils/README.blueprint.md @@ -364,6 +364,25 @@ ARG COMPOSER_VERSION=2.4.4 FROM thecodingmachine/php:{{ $image.php_version }}-v5-slim-apache ``` +## Locales + +Only the `C`, `C.UTF-8` and `POSIX` locales are available by default. The `LOCALES` environment variable lists the +locales to generate at the container start: + +```bash +LOCALES=fr_FR.UTF-8 de_DE.UTF-8 +# Optional: the default locale of the intl extension +PHP_INI_INTL__DEFAULT_LOCALE=fr_FR.UTF-8 +``` + +The locales tools are **installed by default in the fat images**. If you are using the "*slim*" images, you need to +install them with an ARG before the `FROM`: + +```Dockerfile +ARG INSTALL_LOCALES=1 +FROM thecodingmachine/php:{{ $image.php_version }}-v5-slim-apache +``` + ## Permissions Ever faced file permission issues with Docker? Good news, this is a thing of the past! diff --git a/utils/docker-entrypoint-as-root.sh b/utils/docker-entrypoint-as-root.sh index 88c15c2..b2189e4 100755 --- a/utils/docker-entrypoint-as-root.sh +++ b/utils/docker-entrypoint-as-root.sh @@ -150,6 +150,14 @@ if [[ -n "$COMPOSER_VERSION" ]]; then /usr/local/bin/select_composer_version.sh "$COMPOSER_VERSION" fi +if [[ -n "$LOCALES" ]]; then + if ! command -v locale-gen > /dev/null; then + echo 'Locales are not available in this image. If you are using the thecodingmachine/php "slim" variant, do not forget to add "ARG INSTALL_LOCALES=1" in your Dockerfile. Check the documentation for more details.' >&2 + exit 1 + fi + locale-gen --keep-existing $LOCALES > /dev/null +fi + # output on the logs can be done by writing on the "tini" PID. Useful for CRONTAB TINI_PID=`ps -e | grep tini | awk '{print $1;}'` /usr/bin/real_php -d display_errors=stderr /usr/local/bin/generate_cron.php $TINI_PID > /tmp/generated_crontab From 751bdd332815d71c17bde2f66dfb2bf500f9b9ef Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Mon, 12 Oct 2026 09:06:43 +0200 Subject: [PATCH 4/5] Add DAEMON_COMMAND_* to run processes along with the main command One container per process remains the recommended setup, but some projects need a long-running process (e.g. a queue worker) in the same container. The processes are not restarted: as soon as one of them, or the main command, exits, the other ones are stopped and the container stops with its exit code, so that Docker or the orchestrator sees the failure. A stop signal is forwarded to the main command (SIGWINCH for Apache, SIGQUIT for PHP-FPM) and SIGTERM to the other processes. Supercronic runs the same way: the container no longer keeps running without cron jobs when Supercronic exits. Closes #147 --- CHANGELOG.md | 2 + Dockerfile.slim.apache | 1 + Dockerfile.slim.cli | 1 + Dockerfile.slim.fpm | 1 + README.md | 20 ++++++++ tests-suite/tool-daemon.sh | 58 ++++++++++++++++++++++ utils/Dockerfile.slim.blueprint | 1 + utils/README.blueprint.md | 20 ++++++++ utils/docker-entrypoint-as-root.sh | 40 ++++++++++++--- utils/supervise.sh | 79 ++++++++++++++++++++++++++++++ 10 files changed, 216 insertions(+), 7 deletions(-) create mode 100755 tests-suite/tool-daemon.sh create mode 100755 utils/supervise.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ced343..acdb8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ * Add `APACHE_HTTP_PORT` and `APACHE_HTTPS_PORT` to change the ports Apache listens on (e.g. unprivileged ports) * Add `COMPOSER_VERSION` to install another Composer version (exact version or `1`, `2`, `2.2` channel), at the container start or at the build of a slim image * Add `LOCALES` to generate locales at the container start (e.g. `LOCALES=fr_FR.UTF-8 de_DE.UTF-8`): the `locales` package is installed in the fat images (~17MB) and with `ARG INSTALL_LOCALES=1` in the slim images + * Add `DAEMON_COMMAND_*` (and `DAEMON_USER_*`, `docker` by default) to run long-running processes along with the main command (one container per process remains recommended): as soon as one of them exits, the container stops + * Supercronic is supervised the same way: the container stops if Supercronic exits (it kept running without cron jobs before), and the cron jobs start after the startup commands * **2026-10-09** * Imagick can read SVG images (also WMF, DjVu and OpenEXR): the fat images are ~17MB larger diff --git a/Dockerfile.slim.apache b/Dockerfile.slim.apache index d1abd62..a5c418d 100644 --- a/Dockerfile.slim.apache +++ b/Dockerfile.slim.apache @@ -330,6 +330,7 @@ COPY utils/apache-expose-envvars.sh /usr/local/bin/apache-expose-envvars.sh 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 +COPY utils/supervise.sh /usr/local/bin/supervise.sh COPY extensions/ /usr/local/lib/thecodingmachine-php/extensions RUN ln -s ${PHP_VERSION} /usr/local/lib/thecodingmachine-php/extensions/current diff --git a/Dockerfile.slim.cli b/Dockerfile.slim.cli index 9971d30..885fa37 100644 --- a/Dockerfile.slim.cli +++ b/Dockerfile.slim.cli @@ -246,6 +246,7 @@ 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 +COPY utils/supervise.sh /usr/local/bin/supervise.sh COPY extensions/ /usr/local/lib/thecodingmachine-php/extensions RUN ln -s ${PHP_VERSION} /usr/local/lib/thecodingmachine-php/extensions/current diff --git a/Dockerfile.slim.fpm b/Dockerfile.slim.fpm index 5eb7f02..2fe6690 100644 --- a/Dockerfile.slim.fpm +++ b/Dockerfile.slim.fpm @@ -263,6 +263,7 @@ 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 +COPY utils/supervise.sh /usr/local/bin/supervise.sh COPY extensions/ /usr/local/lib/thecodingmachine-php/extensions RUN ln -s ${PHP_VERSION} /usr/local/lib/thecodingmachine-php/extensions/current diff --git a/README.md b/README.md index 698c794..0545509 100644 --- a/README.md +++ b/README.md @@ -601,6 +601,26 @@ docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/sr -v $PWD/my-startup-script.sh:/etc/container/startup.sh thecodingmachine/php:8.4-v5-cli php your-script.php ``` +## Running other processes in the container + +**Prefer one container per process.** For instance, run a Laravel queue worker in its own container (the *cli* image, +with `php artisan queue:work` as command) rather than in the Apache container: Docker and your orchestrator can then +restart, scale, limit and monitor each process. + +When a separate container is not an option, the `DAEMON_COMMAND_XXX` environment variables start long-running +processes along with the main command. They are run by the `docker` user (see [Permissions](#permissions)), or by the +user set in the `DAEMON_USER_XXX` variable with the same suffix (a user name or an ID): + +```bash +DAEMON_COMMAND_1=php artisan queue:work +DAEMON_COMMAND_2=php artisan horizon +DAEMON_USER_2=www-data +``` + +These processes are not restarted: **as soon as one of them (or the main command) exits, the other ones are stopped +and the container stops** with the exit code of this process. Use a restart policy (e.g. `restart: unless-stopped`) +to restart the container. Supercronic (see [Setting up CRON jobs](#setting-up-cron-jobs)) runs the same way. + ## Using the CLI variant The CLI images (thecodingmachine/php:8.4-v5-cli) expect a command to be passed in parameter. diff --git a/tests-suite/tool-daemon.sh b/tests-suite/tool-daemon.sh new file mode 100755 index 0000000..669bece --- /dev/null +++ b/tests-suite/tool-daemon.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +. ./config + +IMAGE="${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" + +############################################################ +## DAEMON_COMMAND runs along with the main command, with the same user +############################################################ +test_runWithMainCommand() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e DAEMON_COMMAND_1="whoami; sleep 30" "${IMAGE}" sleep 2 2>&1)" + assert_equals "0" "$?" "The container should exit with the code of the main command" + assert_equals "docker" "$RESULT" +} +############################################################ +## DAEMON_USER runs the daemon with another user +############################################################ +test_daemonUser() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e DAEMON_COMMAND_1="whoami; sleep 30" -e DAEMON_USER_1="www-data" \ + -e DAEMON_COMMAND_2="id -u; sleep 30" -e DAEMON_USER_2="0" "${IMAGE}" sleep 2 2>&1 | sort | tr '\n' ' ')" + assert_equals "0 www-data " "$RESULT" +} +############################################################ +## The container stops as soon as a daemon exits +############################################################ +test_stopWhenDaemonExits() { + START=$(date +%s) + RESULT="$(docker run ${RUN_OPTIONS} --rm -e DAEMON_COMMAND_1="sleep 30" -e DAEMON_COMMAND_2="exit 3" "${IMAGE}" sleep 30 2>&1)" + CODE=$? + DURATION=$(( $(date +%s) - START )) + assert_equals "3" "$CODE" "The container should exit with the code of the daemon" + assert_matches 'DAEMON_COMMAND_2 \(exit 3\) exited with code 3' "$RESULT" + assert "test ${DURATION} -lt 10" "Stopping took ${DURATION}s" +} +############################################################ +## A docker stop stops the main command and the daemons +############################################################ +test_dockerStop() { + if [[ $VARIANT == cli* ]]; then COMMAND="sleep 300"; else COMMAND=""; fi + docker run --name "${STOP_NAME}" ${RUN_OPTIONS} -d -e DAEMON_COMMAND_1="sleep 300" "${IMAGE}" ${COMMAND} > /dev/null + sleep 3 + START=$(date +%s) + docker stop "${STOP_NAME}" > /dev/null 2>&1 + DURATION=$(( $(date +%s) - START )) + CODE="$(docker inspect -f '{{.State.ExitCode}}' "${STOP_NAME}")" + docker rm -f "${STOP_NAME}" > /dev/null 2>&1 + assert "test ${DURATION} -lt 5" "Stopping took ${DURATION}s (killed by timeout?)" + if [[ $VARIANT != cli* ]]; then + assert_equals "0" "$CODE" "The graceful stop should exit with 0" + fi +} + +setup_suite() { + export STOP_NAME="test-daemon-stop-$(unused_port)" +} + +teardown_suite() { + docker rm -f "${STOP_NAME}" > /dev/null 2>&1 +} diff --git a/utils/Dockerfile.slim.blueprint b/utils/Dockerfile.slim.blueprint index 8b95856..97021da 100644 --- a/utils/Dockerfile.slim.blueprint +++ b/utils/Dockerfile.slim.blueprint @@ -353,6 +353,7 @@ 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 COPY utils/docker-entrypoint-as-root.sh /usr/local/bin/docker-entrypoint-as-root.sh +COPY utils/supervise.sh /usr/local/bin/supervise.sh COPY extensions/ /usr/local/lib/thecodingmachine-php/extensions RUN ln -s ${PHP_VERSION} /usr/local/lib/thecodingmachine-php/extensions/current diff --git a/utils/README.blueprint.md b/utils/README.blueprint.md index 7569dc0..fdd9be5 100644 --- a/utils/README.blueprint.md +++ b/utils/README.blueprint.md @@ -531,6 +531,26 @@ docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/sr -v $PWD/my-startup-script.sh:/etc/container/startup.sh thecodingmachine/php:{{ $image.php_version }}-v5-cli php your-script.php ``` +## Running other processes in the container + +**Prefer one container per process.** For instance, run a Laravel queue worker in its own container (the *cli* image, +with `php artisan queue:work` as command) rather than in the Apache container: Docker and your orchestrator can then +restart, scale, limit and monitor each process. + +When a separate container is not an option, the `DAEMON_COMMAND_XXX` environment variables start long-running +processes along with the main command. They are run by the `docker` user (see [Permissions](#permissions)), or by the +user set in the `DAEMON_USER_XXX` variable with the same suffix (a user name or an ID): + +```bash +DAEMON_COMMAND_1=php artisan queue:work +DAEMON_COMMAND_2=php artisan horizon +DAEMON_USER_2=www-data +``` + +These processes are not restarted: **as soon as one of them (or the main command) exits, the other ones are stopped +and the container stops** with the exit code of this process. Use a restart policy (e.g. `restart: unless-stopped`) +to restart the container. Supercronic (see [Setting up CRON jobs](#setting-up-cron-jobs)) runs the same way. + ## Using the CLI variant The CLI images (thecodingmachine/php:{{ $image.php_version }}-v5-cli) expect a command to be passed in parameter. diff --git a/utils/docker-entrypoint-as-root.sh b/utils/docker-entrypoint-as-root.sh index b2189e4..c8bb392 100755 --- a/utils/docker-entrypoint-as-root.sh +++ b/utils/docker-entrypoint-as-root.sh @@ -163,11 +163,6 @@ TINI_PID=`ps -e | grep tini | awk '{print $1;}'` /usr/bin/real_php -d display_errors=stderr /usr/local/bin/generate_cron.php $TINI_PID > /tmp/generated_crontab chmod 0644 /tmp/generated_crontab -# If generated_crontab is not empty, start supercronic -if [[ -s /tmp/generated_crontab ]]; then - supercronic ${SUPERCRONIC_OPTIONS} /tmp/generated_crontab & -fi - if [[ "$IMAGE_VARIANT" == "apache" ]]; then /usr/bin/real_php -d display_errors=stderr /usr/local/bin/enable_apache_mods.php | bash fi @@ -186,7 +181,38 @@ fi # We should run the command with the user of the directory... (unless this is Apache, that must run as root...) if [[ "$@" == "apache2-foreground" ]]; then /usr/local/bin/apache-expose-envvars.sh; - exec "$@"; + MAIN_COMMAND=("$@") else - exec "sudo" "-E" "-H" "-u" "#$DOCKER_USER_ID" "$@"; + MAIN_COMMAND=("sudo" "-E" "-H" "-u" "#$DOCKER_USER_ID" "$@") +fi + +# Processes run along with the main command: Supercronic and the DAEMON_COMMAND* commands +: > /tmp/generated_processes +if [[ -s /tmp/generated_crontab ]]; then + printf '%s\0' 'Supercronic' 'exec supercronic $SUPERCRONIC_OPTIONS /tmp/generated_crontab' >> /tmp/generated_processes fi +for DAEMON_COMMAND_VAR in "${!DAEMON_COMMAND@}"; do + DAEMON_USER_VAR="DAEMON_USER${DAEMON_COMMAND_VAR#DAEMON_COMMAND}" + DAEMON_USER="${!DAEMON_USER_VAR:-#$DOCKER_USER_ID}" + if [[ "$DAEMON_USER" =~ ^[0-9]+$ ]]; then + DAEMON_USER="#$DAEMON_USER" + fi + printf '%s\0' "$DAEMON_COMMAND_VAR (${!DAEMON_COMMAND_VAR})" \ + "exec sudo -E -H -u $(printf '%q' "$DAEMON_USER") bash -c $(printf '%q' "${!DAEMON_COMMAND_VAR}")" >> /tmp/generated_processes +done + +if [[ ! -s /tmp/generated_processes ]]; then + exec "${MAIN_COMMAND[@]}" +fi + +case "$IMAGE_VARIANT" in + apache) IMAGE_STOP_SIGNAL=WINCH ;; + fpm) IMAGE_STOP_SIGNAL=QUIT ;; + *) IMAGE_STOP_SIGNAL=TERM ;; +esac +case "$1" in + apache2-foreground) STOP_SIGNAL=WINCH ;; + php-fpm*) STOP_SIGNAL=QUIT ;; + *) STOP_SIGNAL=TERM ;; +esac +exec /usr/local/bin/supervise.sh /tmp/generated_processes "$IMAGE_STOP_SIGNAL" "$STOP_SIGNAL" "${MAIN_COMMAND[@]}" diff --git a/utils/supervise.sh b/utils/supervise.sh new file mode 100755 index 0000000..24c5dce --- /dev/null +++ b/utils/supervise.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Usage: supervise.sh
+# +# Runs the main command along with the commands of the file: pairs of a name and a command, separated by NUL characters. +# As soon as one of these processes exits, or SIGTERM, SIGINT or the image stop signal is received, the others are +# stopped (the main command with its stop signal, the others with SIGTERM). The script exits with the exit code of +# the first process that exited. +# When the stop is requested by a signal, the script exits with the exit code of the main command. + +COMMANDS_FILE=$1 +IMAGE_STOP_SIGNAL=$2 +STOP_SIGNAL=$3 +shift 3 + +declare -A NAMES +MAIN_PID= +MAIN_EXIT_CODE= +STOPPING= +STOP_REQUESTED= + +stop() { + STOPPING=$1 + [[ -n "$MAIN_PID" ]] && kill -"$1" "$MAIN_PID" 2> /dev/null + for PID in "${!NAMES[@]}"; do + kill -TERM "$PID" 2> /dev/null + done +} + +for SIGNAL in HUP USR1 USR2 QUIT WINCH; do + trap "[[ -n \"\$MAIN_PID\" ]] && kill -$SIGNAL \"\$MAIN_PID\" 2> /dev/null" "$SIGNAL" +done +trap 'STOP_REQUESTED=1; stop TERM' TERM +trap 'STOP_REQUESTED=1; stop INT' INT +if [[ "$IMAGE_STOP_SIGNAL" != TERM ]]; then + trap 'STOP_REQUESTED=1; stop "$STOP_SIGNAL"' "$IMAGE_STOP_SIGNAL" +fi + +while IFS= read -r -d '' NAME && IFS= read -r -d '' COMMAND; do + bash -c "$COMMAND" & + NAMES[$!]=$NAME +done < "$COMMANDS_FILE" +"$@" <&0 & +MAIN_PID=$! + +# A stop requested while the processes were starting +[[ -n "$STOPPING" ]] && stop "$STOPPING" + +# Checks every second which processes exited (wait -n can miss a process already reaped by bash) +while [[ -z "$MAIN_EXIT_CODE" ]] || (( ${#NAMES[@]} > 0 )); do + sleep 1 & + wait $! + + RUNNING_PIDS=("${!NAMES[@]}") + [[ -z "$MAIN_EXIT_CODE" ]] && RUNNING_PIDS+=("$MAIN_PID") + for PID in "${RUNNING_PIDS[@]}"; do + kill -0 "$PID" 2> /dev/null && continue + wait "$PID" + CODE=$? + + if [[ "$PID" == "$MAIN_PID" ]]; then + MAIN_EXIT_CODE=$CODE + else + EXITED_NAME=${NAMES[$PID]} + unset "NAMES[$PID]" + fi + if [[ -z "$STOPPING" ]]; then + EXIT_CODE=$CODE + if [[ "$PID" != "$MAIN_PID" ]]; then + echo "$EXITED_NAME exited with code $CODE: stopping the container" >&2 + fi + stop "$STOP_SIGNAL" + fi + done +done + +if [[ -n "$STOP_REQUESTED" ]]; then + exit "$MAIN_EXIT_CODE" +fi +exit "$EXIT_CODE" From 32b9b8d203ea2972656b7306cee061036893399f Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Mon, 12 Oct 2026 09:06:43 +0200 Subject: [PATCH 5/5] Test that the processes left by the cron jobs are reaped The zombie processes of #368 (reported on v4) no longer happen on v5: sudo, PID 1 of the container, reaps the orphaned processes. Closes #368 --- tests-suite/tool-supercronic.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests-suite/tool-supercronic.sh b/tests-suite/tool-supercronic.sh index ba85c01..4f8b031 100755 --- a/tests-suite/tool-supercronic.sh +++ b/tests-suite/tool-supercronic.sh @@ -36,3 +36,11 @@ test_twoCommandInOneRow() { "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-${BRANCH_VARIANT}${ARCH_SUFFIX}" sleep 1 2>&1 | grep -oP 'msg=docker' | wc -l)" assert '[ "$RESULT" -gt "1" ]' } +############################################################ +## Let's check that the processes left in background by the cron jobs do not become zombies. +############################################################ +test_noZombie() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e CRON_SCHEDULE_1="* * * * * * *" -e CRON_COMMAND_1="sleep 1 &" -e CRON_USER_1="docker" \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-${BRANCH_VARIANT}${ARCH_SUFFIX}" bash -c 'sleep 8; ps -eo stat= | grep -c "^Z"' 2>/dev/null | tail -n 1)" + assert '[ "$RESULT" -le "1" ]' "${RESULT} zombie processes" +}