From f4676cf3724ed093cf8ebc653ba8e1c5e62bc4eb Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 16:21:09 +0000 Subject: [PATCH 1/8] Send PHP errors to stderr in the scripts generating the startup configuration The output of these scripts is written to generated_conf.ini and to the crontab, or run by bash. A PHP warning (e.g. an extension that cannot be loaded) was written in this output: syntax error in generated_conf.ini, supercronic exiting on a "bad crontab line" and bash syntax errors stopping the container. The warnings are now only logged. --- CHANGELOG.md | 1 + tests-suite/assets/php-startup-warning.ini | 2 ++ tests-suite/tool-startup-command.sh | 10 ++++++++++ utils/docker-entrypoint-as-root.sh | 12 ++++++------ utils/php_proxy.sh | 4 ++-- 5 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 tests-suite/assets/php-startup-warning.ini diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5a083d..4ba22643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Minor changes * **2026-10-09** + * Fix PHP warnings (e.g. an extension that cannot be loaded) breaking the container start: they were written in `generated_conf.ini`, in the crontab and in the startup commands * Fix the fpm variant stop: PHP-FPM is now stopped gracefully (`SIGQUIT`: running requests are completed) and port 9000 is exposed * Fix stop requests sent while the apache or fpm container is starting: the stop signal (`SIGWINCH` / `SIGQUIT`) was ignored by the entrypoint and the container was killed after the stop timeout * Fix Apache modules listed in the documentation but rejected by `APACHE_EXTENSION_*`: `brotli`, `cern_meta`, `imagemap`, `md`, `proxy_hcheck`, `proxy_uwsgi`, `socache_redis` diff --git a/tests-suite/assets/php-startup-warning.ini b/tests-suite/assets/php-startup-warning.ini new file mode 100644 index 00000000..d2429109 --- /dev/null +++ b/tests-suite/assets/php-startup-warning.ini @@ -0,0 +1,2 @@ +; Loading an extension that does not exist makes PHP display a startup warning +extension=does_not_exist diff --git a/tests-suite/tool-startup-command.sh b/tests-suite/tool-startup-command.sh index bce45d74..ec6f6150 100755 --- a/tests-suite/tool-startup-command.sh +++ b/tests-suite/tool-startup-command.sh @@ -25,3 +25,13 @@ test_withFile() { "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" php -m 2>/dev/null | grep -q "startup.sh executed" assert_equals "0" "$?" } +############################################################ +## Tests that a PHP startup warning does not prevent the container from starting +############################################################ +test_phpStartupWarning() { + RESULT="$(docker run ${RUN_OPTIONS} --rm -e STARTUP_COMMAND_1="echo startup-ok" \ + -v "${SCRIPT_DIR}/assets/php-startup-warning.ini":"/etc/php/${PHP_VERSION}/cli/conf.d/99-startup-warning.ini" \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" sleep 1 2>/dev/null)" + assert_equals "0" "$?" "Docker run failed" + assert_equals "startup-ok" "$RESULT" +} diff --git a/utils/docker-entrypoint-as-root.sh b/utils/docker-entrypoint-as-root.sh index d2e3b13d..ae0df012 100755 --- a/utils/docker-entrypoint-as-root.sh +++ b/utils/docker-entrypoint-as-root.sh @@ -123,14 +123,14 @@ unset DOCKER_FOR_MAC_REMOTE_HOST unset REMOTE_HOST_FOUND sudo chown docker:docker /opt/php_env_var_cache.php -/usr/bin/real_php /usr/local/bin/check_php_env_var_changes.php &> /dev/null +/usr/bin/real_php -d display_errors=stderr /usr/local/bin/check_php_env_var_changes.php &> /dev/null -/usr/bin/real_php /usr/local/bin/generate_conf.php > /etc/php/${PHP_VERSION}/mods-available/generated_conf.ini -PHP_VERSION="${PHP_VERSION}" /usr/bin/real_php /usr/local/bin/setup_extensions.php | sudo bash +/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 # 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 /usr/local/bin/generate_cron.php $TINI_PID > /tmp/generated_crontab +/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 @@ -139,13 +139,13 @@ if [[ -s /tmp/generated_crontab ]]; then fi if [[ "$IMAGE_VARIANT" == "apache" ]]; then - /usr/bin/real_php /usr/local/bin/enable_apache_mods.php | bash + /usr/bin/real_php -d display_errors=stderr /usr/local/bin/enable_apache_mods.php | bash fi if [ -e /etc/container/startup.sh ]; then sudo -E -u "#$DOCKER_USER_ID" /etc/container/startup.sh fi -sudo -E -u "#$DOCKER_USER_ID" sh -c "/usr/bin/real_php /usr/local/bin/startup_commands.php | bash" +sudo -E -u "#$DOCKER_USER_ID" sh -c "/usr/bin/real_php -d display_errors=stderr /usr/local/bin/startup_commands.php | bash" if [[ "$APACHE_DOCUMENT_ROOT" == /* ]]; then export ABSOLUTE_APACHE_DOCUMENT_ROOT="$APACHE_DOCUMENT_ROOT" diff --git a/utils/php_proxy.sh b/utils/php_proxy.sh index d3dc5a03..05986a75 100755 --- a/utils/php_proxy.sh +++ b/utils/php_proxy.sh @@ -11,8 +11,8 @@ if [[ "$REGENERATE" != "0" ]] && [[ "$REGENERATE" != "1" ]]; then fi if [[ "$REGENERATE" == "1" ]]; then - /usr/bin/real_php /usr/local/bin/generate_conf.php | sudo tee "/etc/php/${PHP_VERSION}/mods-available/generated_conf.ini" > /dev/null - PHP_VERSION="${PHP_VERSION}" /usr/bin/real_php /usr/local/bin/setup_extensions.php | sudo bash + /usr/bin/real_php -d display_errors=stderr /usr/local/bin/generate_conf.php | sudo tee "/etc/php/${PHP_VERSION}/mods-available/generated_conf.ini" > /dev/null + PHP_VERSION="${PHP_VERSION}" /usr/bin/real_php -d display_errors=stderr /usr/local/bin/setup_extensions.php | sudo bash fi exec /usr/bin/real_php "$@" From 238f332c856459f5edd84fefca6f7751bfe6c0ee Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 16:21:09 +0000 Subject: [PATCH 2/8] Run PHP without regenerating its configuration for users that cannot use sudo "sudo -u www-data php ..." printed a sudo password error on each call, and failed with "Unexpected PHP proxy output" when a PHP_* variable had changed (the cache file is not writable by www-data). The proxy now runs PHP with the current configuration when sudo is not available. Fixes #253 --- CHANGELOG.md | 1 + tests-suite/users-rights.sh | 11 +++++++++++ utils/php_proxy.sh | 7 +++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ba22643..44088f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Minor changes * **2026-10-09** + * Fix `php` run by a user that cannot use sudo (e.g. `sudo -u www-data php ...`): sudo password errors, and failure when a `PHP_*` variable had changed * Fix PHP warnings (e.g. an extension that cannot be loaded) breaking the container start: they were written in `generated_conf.ini`, in the crontab and in the startup commands * Fix the fpm variant stop: PHP-FPM is now stopped gracefully (`SIGQUIT`: running requests are completed) and port 9000 is exposed * Fix stop requests sent while the apache or fpm container is starting: the stop signal (`SIGWINCH` / `SIGQUIT`) was ignored by the entrypoint and the container was killed after the stop timeout diff --git a/tests-suite/users-rights.sh b/tests-suite/users-rights.sh index abdfc61d..544dec46 100755 --- a/tests-suite/users-rights.sh +++ b/tests-suite/users-rights.sh @@ -58,6 +58,17 @@ EOF } +############################################################ +## Users that cannot use sudo can run PHP, even when a PHP_* +## environment variable has changed +############################################################ +test_userWithoutSudoCanRunPhp() { + RESULT="$(docker run ${RUN_OPTIONS} --rm "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" \ + sudo -E -u www-data PHP_INI_MEMORY_LIMIT=1G php -r 'echo "OK";' 2>&1)" + assert_equals "0" "$?" "Docker run failed" + assert_equals "OK" "${RESULT}" "PHP run as www-data failed or printed errors" +} + setup_suite() { export TMP_DIR="$(mktemp -d)" if [[ $VARIANT == cli* ]]; then export CONTAINER_CWD=/usr/src/app; else export CONTAINER_CWD=/var/www/html; fi diff --git a/utils/php_proxy.sh b/utils/php_proxy.sh index 05986a75..f1ba90cf 100755 --- a/utils/php_proxy.sh +++ b/utils/php_proxy.sh @@ -1,8 +1,11 @@ #!/bin/bash -sudo chown docker:docker /opt/php_env_var_cache.php +# Users that cannot use sudo run PHP with the current configuration +if ! sudo -n chown docker:docker /opt/php_env_var_cache.php 2> /dev/null; then + exec /usr/bin/real_php "$@" +fi -REGENERATE=$(/usr/bin/real_php /usr/local/bin/check_php_env_var_changes.php) +REGENERATE=$(/usr/bin/real_php -d display_errors=stderr /usr/local/bin/check_php_env_var_changes.php) if [[ "$REGENERATE" != "0" ]] && [[ "$REGENERATE" != "1" ]]; then >&2 echo "Unexpected PHP proxy output:" From e581551bb1d813c8daa94544001c79700e836eed Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 16:25:33 +0000 Subject: [PATCH 3/8] Give the ID of the mounted directory to the docker user when a system account has it The user running the commands is the owner of the working directory. When its ID belongs to a system account of the image (e.g. 998 is systemd-network in Ubuntu 24.04, and the ID of the gitlab-runner user on many hosts), the commands were run with this account: no home directory, no sudo ("touch ~/.startup_done: Permission denied"). Setting DOCKER_USER to such an ID failed ("usermod: UID '998' already exists"). The system account is now moved to a free ID, so that the docker user takes the ID of the host user. Its ID is changed in /etc/passwd: usermod would also change the owner of the files of its home directory ("/"). Also rename a test that had the same name as another one (only the last one was run). Fixes #408 --- CHANGELOG.md | 2 ++ tests-suite/users-rights.sh | 17 ++++++++++++++++- utils/docker-entrypoint-as-root.sh | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44088f1a..136e80d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Minor changes * **2026-10-09** + * Fix the container start when the mounted directory belongs to an ID used by a system account of the image (e.g. 998 for a `gitlab-runner` user): the commands were run with this account (no home directory, no sudo) instead of the `docker` user + * Fix `DOCKER_USER` set to an ID used by a system account of the image (`usermod: UID already exists`) * Fix `php` run by a user that cannot use sudo (e.g. `sudo -u www-data php ...`): sudo password errors, and failure when a `PHP_*` variable had changed * Fix PHP warnings (e.g. an extension that cannot be loaded) breaking the container start: they were written in `generated_conf.ini`, in the crontab and in the startup commands * Fix the fpm variant stop: PHP-FPM is now stopped gracefully (`SIGQUIT`: running requests are completed) and port 9000 is exposed diff --git a/tests-suite/users-rights.sh b/tests-suite/users-rights.sh index 544dec46..aa80845b 100755 --- a/tests-suite/users-rights.sh +++ b/tests-suite/users-rights.sh @@ -36,7 +36,7 @@ test_defaultUserCanWriteOnStdoutAndStderr() { ############################################################ ## It's also works for users with existing IDs in the container ############################################################ -test_defaultUserCanWriteOnStdoutAndStderr() { +test_defaultUserCanBeAnExistingUser() { mkdir -p "${TMP_DIR}/user33" cat << EOF > "${TMP_DIR}/user33/composer.json" { @@ -58,6 +58,21 @@ EOF } +############################################################ +## The system accounts of the image (systemd-network, polkitd...) +## do not take the place of the default user when they have the +## ID of the mounted directory (e.g. 998 for gitlab-runner) +############################################################ +test_defaultUserTakesUidOfSystemAccount() { + mkdir -p "${TMP_DIR}/user998" + docker run ${RUN_OPTIONS} --rm -v /tmp:/tmp busybox chown 998:998 "${TMP_DIR}/user998" > /dev/null 2>&1 + RESULT="$(docker run ${RUN_OPTIONS} --rm -v "${TMP_DIR}/user998":"${CONTAINER_CWD}" -e STARTUP_COMMAND_1='touch ~/.startup_done' \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" \ + bash -c 'echo "$(id -un):$(id -ur)"')" + assert_equals "0" "$?" "Docker run failed" + assert_equals "docker:998" "${RESULT}" "Default user mismatch with a mounted directory owned by a system account ID" +} + ############################################################ ## Users that cannot use sudo can run PHP, even when a PHP_* ## environment variable has changed diff --git a/utils/docker-entrypoint-as-root.sh b/utils/docker-entrypoint-as-root.sh index ae0df012..539ea9fd 100755 --- a/utils/docker-entrypoint-as-root.sh +++ b/utils/docker-entrypoint-as-root.sh @@ -66,6 +66,24 @@ fi # DOCKER_USER is a user name if the user exists in the container, otherwise, it is a user ID (from a user on the host). +# A system account of the image that has the ID of DOCKER_USER is moved to a free ID: the docker user takes this ID +SYS_UID_MIN=$(awk '$1 == "SYS_UID_MIN" { print $2 }' /etc/login.defs) +SYS_UID_MAX=$(awk '$1 == "SYS_UID_MAX" { print $2 }' /etc/login.defs) +SYS_UID_MIN=${SYS_UID_MIN:-100} +SYS_UID_MAX=${SYS_UID_MAX:-999} +SYSTEM_ACCOUNT=$(getent passwd "$DOCKER_USER" | cut -d: -f1,3) +SYSTEM_ACCOUNT_NAME=${SYSTEM_ACCOUNT%%:*} +SYSTEM_ACCOUNT_ID=${SYSTEM_ACCOUNT##*:} +if [[ -n "$SYSTEM_ACCOUNT" ]] && [[ "$SYSTEM_ACCOUNT_NAME" != "docker" ]] && (( SYSTEM_ACCOUNT_ID >= SYS_UID_MIN && SYSTEM_ACCOUNT_ID <= SYS_UID_MAX )); then + FREE_ID=$SYS_UID_MAX + while getent passwd "$FREE_ID" > /dev/null; do + FREE_ID=$((FREE_ID - 1)) + done + sed -i "s/^${SYSTEM_ACCOUNT_NAME}:\([^:]*\):${SYSTEM_ACCOUNT_ID}:/${SYSTEM_ACCOUNT_NAME}:\1:${FREE_ID}:/" /etc/passwd + DOCKER_USER=$SYSTEM_ACCOUNT_ID +fi +unset SYS_UID_MIN SYS_UID_MAX SYSTEM_ACCOUNT SYSTEM_ACCOUNT_NAME SYSTEM_ACCOUNT_ID FREE_ID + # If DOCKER_USER is an ID, let's if [[ "$DOCKER_USER" =~ ^[0-9]+$ ]] ; then # MAIN_DIR_USER is a user ID. From ad4e0b2815242950ceff434de2605a56d5dd410a Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 16:45:46 +0000 Subject: [PATCH 4/8] Document the system accounts that have the ID of the working directory owner --- README.md | 4 ++++ utils/README.blueprint.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 684615e2..0054d373 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,10 @@ working directory (`/var/www/html` for Apache/PHP-FPM, or `/usr/src/app` for CLI you want to run commands as this user. So it will **dynamically change the ID of the docker user** to match the ID of the current working directory user. +If this ID is already used by a system account of the image (e.g. `998`, used by `systemd-network` in the image and +often by the `gitlab-runner` user on CI hosts), this system account is moved to another ID: the commands are still run +by the `docker` user. + Furthermore, the image is changing the Apache default user/group to be `docker/docker` (instead if `www-data/www-data`). So Apache will run with the same rights as the user on your host. diff --git a/utils/README.blueprint.md b/utils/README.blueprint.md index bf7d61d8..64c6971a 100644 --- a/utils/README.blueprint.md +++ b/utils/README.blueprint.md @@ -353,6 +353,10 @@ working directory (`/var/www/html` for Apache/PHP-FPM, or `/usr/src/app` for CLI you want to run commands as this user. So it will **dynamically change the ID of the docker user** to match the ID of the current working directory user. +If this ID is already used by a system account of the image (e.g. `998`, used by `systemd-network` in the image and +often by the `gitlab-runner` user on CI hosts), this system account is moved to another ID: the commands are still run +by the `docker` user. + Furthermore, the image is changing the Apache default user/group to be `docker/docker` (instead if `www-data/www-data`). So Apache will run with the same rights as the user on your host. From ac20c0142ea1d9f68eb8963608c4a24c83c5a7de Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 16:27:43 +0000 Subject: [PATCH 5/8] Enable the extensions required by an enabled extension memcached requires igbinary and msgpack, redis igbinary, mailparse mbstring and swoole curl: enabled alone (or with the required extension disabled), the extension could not be loaded ("undefined symbol: php_msgpack_serialize"). The required extensions are now enabled with it, like mysqlnd for mysqli, pdo_mysql and swoole. Fixes #321 --- CHANGELOG.md | 1 + tests-suite/php-extensions.sh | 13 +++++++++++++ utils/setup_extensions.php | 23 +++++++++++++++++++---- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 136e80d1..ce7a1b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Minor changes * **2026-10-09** + * 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`) * Fix the container start when the mounted directory belongs to an ID used by a system account of the image (e.g. 998 for a `gitlab-runner` user): the commands were run with this account (no home directory, no sudo) instead of the `docker` user * Fix `DOCKER_USER` set to an ID used by a system account of the image (`usermod: UID already exists`) * Fix `php` run by a user that cannot use sudo (e.g. `sudo -u www-data php ...`): sudo password errors, and failure when a `PHP_*` variable had changed diff --git a/tests-suite/php-extensions.sh b/tests-suite/php-extensions.sh index c52d88c9..a6f56b13 100755 --- a/tests-suite/php-extensions.sh +++ b/tests-suite/php-extensions.sh @@ -46,6 +46,19 @@ test_presenceOfPhp82ExtensionsOnFat() { assert_equals "${EXTENSIONS[$EXTENSION]}" "${RESULT}" "Missing php-${EXTENSION}" done } +################################################################# +## Let's check that the extensions required by an enabled +## extension are enabled too (even if explicitly disabled) +################################################################# +test_dependenciesOfExtensionsOnFat() { + MODULES=$(docker run ${RUN_OPTIONS} -e "PHP_EXTENSIONS=memcached mailparse" -e PHP_EXTENSION_IGBINARY=0 -e PHP_EXTENSION_MBSTRING=0 \ + --rm "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-${BRANCH_VARIANT}${ARCH_SUFFIX}" php -m 2>&1 | tail -n +1) + assert_equals "0" "$(echo "${MODULES}" | grep -c 'Unable to load dynamic library')" "An extension cannot be loaded" + for EXTENSION in memcached igbinary msgpack mailparse mbstring redis; do + RESULT=$(echo "${MODULES}" | grep --color=never -x "${EXTENSION}") + assert_equals "${EXTENSION}" "${RESULT}" "Missing php-${EXTENSION}" + done +} ############################################################ ## Let's check that the extensions are enabled when composer is run ############################################################ diff --git a/utils/setup_extensions.php b/utils/setup_extensions.php index dc3ebddf..19a31c3b 100755 --- a/utils/setup_extensions.php +++ b/utils/setup_extensions.php @@ -55,10 +55,25 @@ } } -// mysqlnd is a dependency required for mysqli, pdo_mysql or swoole -if (enableExtension('mysqli') || enableExtension('pdo_mysql') || enableExtension('swoole')) { - $toEnable['mysqlnd'] = 'mysqlnd'; - unset($toDisable['mysqlnd']); +// Extensions that cannot be loaded without other extensions (dependencies of the Ubuntu packages) +$dependencies = [ + 'mailparse' => ['mbstring'], + 'memcached' => ['igbinary', 'msgpack'], + 'mysqli' => ['mysqlnd'], + 'pdo_mysql' => ['mysqlnd'], + 'redis' => ['igbinary'], + 'swoole' => ['curl', 'mysqlnd'], +]; +foreach ($dependencies as $extension => $requiredExtensions) { + if (!isset($toEnable[$extension])) { + continue; + } + foreach ($requiredExtensions as $requiredExtension) { + if (in_array($requiredExtension, $availableExtensions, true)) { + $toEnable[$requiredExtension] = $requiredExtension; + unset($toDisable[$requiredExtension]); + } + } } // curl is a dependency required for blackfire 8 (see https://blog.blackfire.io/php-8-support.html) From 9455666d19b31b19b31643f10c0b8d45ee0476f2 Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 16:29:20 +0000 Subject: [PATCH 6/8] Run Composer and NodeJS binaries without their path with docker exec Since Docker 23, "docker exec" (and "docker compose exec") refuses to run a binary found with a relative path of the PATH ("cannot run executable found relative to current directory"): vendor/bin and node_modules/.bin were only added as relative paths. The global Composer binaries were not found at all outside bash ("~" is only expanded by bash). The absolute paths of the working directory and of the global Composer binaries are now added before the relative paths, which are kept for the other directories. Fixes #363 Fixes #310 --- CHANGELOG.md | 1 + Dockerfile.slim.apache | 8 ++++++-- Dockerfile.slim.cli | 8 ++++++-- Dockerfile.slim.fpm | 8 ++++++-- tests-suite/tool-composer-bin.sh | 27 +++++++++++++++++++++++++++ utils/Dockerfile.slim.blueprint | 12 ++++++++++-- 6 files changed, 56 insertions(+), 8 deletions(-) create mode 100755 tests-suite/tool-composer-bin.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7a1b27..b4862da1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Minor changes * **2026-10-09** + * 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`) * Fix the container start when the mounted directory belongs to an ID used by a system account of the image (e.g. 998 for a `gitlab-runner` user): the commands were run with this account (no home directory, no sudo) instead of the `docker` user * Fix `DOCKER_USER` set to an ID used by a system account of the image (`usermod: UID already exists`) diff --git a/Dockerfile.slim.apache b/Dockerfile.slim.apache index 87061918..0273c4fb 100644 --- a/Dockerfile.slim.apache +++ b/Dockerfile.slim.apache @@ -252,7 +252,9 @@ WORKDIR /var/www/html # | # | Let's add ./vendor/bin to the PATH (utility function to use Composer bin easily) # | -ENV PATH="$PATH:./vendor/bin:~/.composer/vendor/bin" + +ENV PATH="$PATH:/var/www/html/vendor/bin:/home/docker/.composer/vendor/bin:./vendor/bin:~/.composer/vendor/bin" + RUN sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:./vendor/bin:~/.composer/vendor/bin#g' /etc/sudoers USER docker @@ -291,7 +293,9 @@ USER root # | # | NodeJS path registration (if we install NodeJS, this is useful). # | -ENV PATH="$PATH:./node_modules/.bin" + +ENV PATH="$PATH:/var/www/html/node_modules/.bin:./node_modules/.bin" + RUN sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:./node_modules/.bin#g' /etc/sudoers # |-------------------------------------------------------------------------- diff --git a/Dockerfile.slim.cli b/Dockerfile.slim.cli index dc40e27b..0eec9b34 100644 --- a/Dockerfile.slim.cli +++ b/Dockerfile.slim.cli @@ -178,7 +178,9 @@ WORKDIR /usr/src/app # | # | Let's add ./vendor/bin to the PATH (utility function to use Composer bin easily) # | -ENV PATH="$PATH:./vendor/bin:~/.composer/vendor/bin" + +ENV PATH="$PATH:/usr/src/app/vendor/bin:/home/docker/.composer/vendor/bin:./vendor/bin:~/.composer/vendor/bin" + RUN sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:./vendor/bin:~/.composer/vendor/bin#g' /etc/sudoers USER docker @@ -217,7 +219,9 @@ USER root # | # | NodeJS path registration (if we install NodeJS, this is useful). # | -ENV PATH="$PATH:./node_modules/.bin" + +ENV PATH="$PATH:/usr/src/app/node_modules/.bin:./node_modules/.bin" + RUN sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:./node_modules/.bin#g' /etc/sudoers # |-------------------------------------------------------------------------- diff --git a/Dockerfile.slim.fpm b/Dockerfile.slim.fpm index 7b454d75..d1798427 100644 --- a/Dockerfile.slim.fpm +++ b/Dockerfile.slim.fpm @@ -195,7 +195,9 @@ WORKDIR /var/www/html # | # | Let's add ./vendor/bin to the PATH (utility function to use Composer bin easily) # | -ENV PATH="$PATH:./vendor/bin:~/.composer/vendor/bin" + +ENV PATH="$PATH:/var/www/html/vendor/bin:/home/docker/.composer/vendor/bin:./vendor/bin:~/.composer/vendor/bin" + RUN sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:./vendor/bin:~/.composer/vendor/bin#g' /etc/sudoers USER docker @@ -234,7 +236,9 @@ USER root # | # | NodeJS path registration (if we install NodeJS, this is useful). # | -ENV PATH="$PATH:./node_modules/.bin" + +ENV PATH="$PATH:/var/www/html/node_modules/.bin:./node_modules/.bin" + RUN sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:./node_modules/.bin#g' /etc/sudoers # |-------------------------------------------------------------------------- diff --git a/tests-suite/tool-composer-bin.sh b/tests-suite/tool-composer-bin.sh new file mode 100755 index 00000000..49fd8fa9 --- /dev/null +++ b/tests-suite/tool-composer-bin.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +. ./config + +############################################################ +## Composer binaries can be run by "docker exec" without their path +############################################################ +test_composerBinaryWithDockerExec() { + mkdir -p "${TMP_DIR}/vendor/bin" + printf '#!/bin/sh\necho composer-bin-ok\n' > "${TMP_DIR}/vendor/bin/composer-bin-test" + chmod -R a+rX "${TMP_DIR}" && chmod a+x "${TMP_DIR}/vendor/bin/composer-bin-test" + docker run --name "${COMPOSER_BIN_CONTAINER_NAME}" ${RUN_OPTIONS} --rm -d -v "${TMP_DIR}":"${CONTAINER_CWD}" \ + "${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}" sleep 30 > /dev/null + assert_equals "0" "$?" "Docker run failed" + RESULT="$(docker exec "${COMPOSER_BIN_CONTAINER_NAME}" composer-bin-test 2>&1)" + assert_equals "composer-bin-ok" "${RESULT}" +} + +setup_suite() { + export TMP_DIR="$(mktemp -d)" + export COMPOSER_BIN_CONTAINER_NAME="test-composer-bin-$(unused_port)" + if [[ $VARIANT == cli* ]]; then export CONTAINER_CWD=/usr/src/app; else export CONTAINER_CWD=/var/www/html; fi +} + +teardown_suite() { + docker rm -f "${COMPOSER_BIN_CONTAINER_NAME}" > /dev/null 2>&1 + if [[ "" != ${TMP_DIR} ]]; then docker run ${RUN_OPTIONS} --rm -v "/tmp":/tmp busybox rm -rf "${TMP_DIR}" > /dev/null 2>&1; fi +} diff --git a/utils/Dockerfile.slim.blueprint b/utils/Dockerfile.slim.blueprint index 0a70de97..70c8d432 100644 --- a/utils/Dockerfile.slim.blueprint +++ b/utils/Dockerfile.slim.blueprint @@ -271,7 +271,11 @@ WORKDIR /var/www/html # | # | Let's add ./vendor/bin to the PATH (utility function to use Composer bin easily) # | -ENV PATH="$PATH:./vendor/bin:~/.composer/vendor/bin" +{{if eq .Orbit.variant "cli" }} +ENV PATH="$PATH:/usr/src/app/vendor/bin:/home/docker/.composer/vendor/bin:./vendor/bin:~/.composer/vendor/bin" +{{else}} +ENV PATH="$PATH:/var/www/html/vendor/bin:/home/docker/.composer/vendor/bin:./vendor/bin:~/.composer/vendor/bin" +{{end}} RUN sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:./vendor/bin:~/.composer/vendor/bin#g' /etc/sudoers USER docker @@ -310,7 +314,11 @@ USER root # | # | NodeJS path registration (if we install NodeJS, this is useful). # | -ENV PATH="$PATH:./node_modules/.bin" +{{if eq .Orbit.variant "cli" }} +ENV PATH="$PATH:/usr/src/app/node_modules/.bin:./node_modules/.bin" +{{else}} +ENV PATH="$PATH:/var/www/html/node_modules/.bin:./node_modules/.bin" +{{end}} RUN sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:./node_modules/.bin#g' /etc/sudoers # |-------------------------------------------------------------------------- From 5b1cb15920f5383f41d0d219a99ee7c58ff64309 Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 16:30:38 +0000 Subject: [PATCH 7/8] Upgrade Supercronic from 0.1.9 to 0.2.49 Supercronic 0.1.9 (2019) was built with Go 1.14 and is reported by the vulnerability scanners (e.g. CVE-2022-23806 in the Go standard library). 0.2.49 is built with Go 1.26. Refs #342 --- CHANGELOG.md | 1 + Dockerfile.slim.apache | 6 +++--- Dockerfile.slim.cli | 6 +++--- Dockerfile.slim.fpm | 6 +++--- utils/Dockerfile.slim.blueprint | 6 +++--- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4862da1..35bda063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Minor changes * **2026-10-09** + * 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`) * Fix the container start when the mounted directory belongs to an ID used by a system account of the image (e.g. 998 for a `gitlab-runner` user): the commands were run with this account (no home directory, no sudo) instead of the `docker` user diff --git a/Dockerfile.slim.apache b/Dockerfile.slim.apache index 0273c4fb..24b60e1d 100644 --- a/Dockerfile.slim.apache +++ b/Dockerfile.slim.apache @@ -376,10 +376,10 @@ ENV SUPERCRONIC_OPTIONS="" ONBUILD ARG INSTALL_CRON ONBUILD RUN if [ -n "$INSTALL_CRON" ]; then \ SUPERCRONIC="supercronic-${TARGETOS}-${TARGETARCH}" \ - && SUPERCRONIC_URL="https://github.com/aptible/supercronic/releases/download/v0.1.9/${SUPERCRONIC}" \ + && SUPERCRONIC_URL="https://github.com/aptible/supercronic/releases/download/v0.2.49/${SUPERCRONIC}" \ && echo ${SUPERCRONIC_URL} \ - && if [ "$TARGETARCH" = "arm64" ]; then SUPERCRONIC_SHA1SUM=e2714c43e7781bf1579c85aa61259245f56dbba1; \ - elif [ "$TARGETARCH" = "amd64" ]; then SUPERCRONIC_SHA1SUM=5ddf8ea26b56d4a7ff6faecdd8966610d5cb9d85; \ + && if [ "$TARGETARCH" = "arm64" ]; then SUPERCRONIC_SHA1SUM=0b6c5bb743e0b0dafed1132198c81807927ac413; \ + elif [ "$TARGETARCH" = "amd64" ]; then SUPERCRONIC_SHA1SUM=e63c11a9726b775a6a11801e81af4f3fb926aa68; \ else echo "Target arch '${TARGETARCH}' is not supported"; exit 1; fi \ && curl -fsSLO --retry 5 --retry-delay 2 "${SUPERCRONIC_URL}" \ && echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \ diff --git a/Dockerfile.slim.cli b/Dockerfile.slim.cli index 0eec9b34..5a242b8f 100644 --- a/Dockerfile.slim.cli +++ b/Dockerfile.slim.cli @@ -281,10 +281,10 @@ ENV SUPERCRONIC_OPTIONS="" ONBUILD ARG INSTALL_CRON ONBUILD RUN if [ -n "$INSTALL_CRON" ]; then \ SUPERCRONIC="supercronic-${TARGETOS}-${TARGETARCH}" \ - && SUPERCRONIC_URL="https://github.com/aptible/supercronic/releases/download/v0.1.9/${SUPERCRONIC}" \ + && SUPERCRONIC_URL="https://github.com/aptible/supercronic/releases/download/v0.2.49/${SUPERCRONIC}" \ && echo ${SUPERCRONIC_URL} \ - && if [ "$TARGETARCH" = "arm64" ]; then SUPERCRONIC_SHA1SUM=e2714c43e7781bf1579c85aa61259245f56dbba1; \ - elif [ "$TARGETARCH" = "amd64" ]; then SUPERCRONIC_SHA1SUM=5ddf8ea26b56d4a7ff6faecdd8966610d5cb9d85; \ + && if [ "$TARGETARCH" = "arm64" ]; then SUPERCRONIC_SHA1SUM=0b6c5bb743e0b0dafed1132198c81807927ac413; \ + elif [ "$TARGETARCH" = "amd64" ]; then SUPERCRONIC_SHA1SUM=e63c11a9726b775a6a11801e81af4f3fb926aa68; \ else echo "Target arch '${TARGETARCH}' is not supported"; exit 1; fi \ && curl -fsSLO --retry 5 --retry-delay 2 "${SUPERCRONIC_URL}" \ && echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \ diff --git a/Dockerfile.slim.fpm b/Dockerfile.slim.fpm index d1798427..3633cc2d 100644 --- a/Dockerfile.slim.fpm +++ b/Dockerfile.slim.fpm @@ -304,10 +304,10 @@ ENV SUPERCRONIC_OPTIONS="" ONBUILD ARG INSTALL_CRON ONBUILD RUN if [ -n "$INSTALL_CRON" ]; then \ SUPERCRONIC="supercronic-${TARGETOS}-${TARGETARCH}" \ - && SUPERCRONIC_URL="https://github.com/aptible/supercronic/releases/download/v0.1.9/${SUPERCRONIC}" \ + && SUPERCRONIC_URL="https://github.com/aptible/supercronic/releases/download/v0.2.49/${SUPERCRONIC}" \ && echo ${SUPERCRONIC_URL} \ - && if [ "$TARGETARCH" = "arm64" ]; then SUPERCRONIC_SHA1SUM=e2714c43e7781bf1579c85aa61259245f56dbba1; \ - elif [ "$TARGETARCH" = "amd64" ]; then SUPERCRONIC_SHA1SUM=5ddf8ea26b56d4a7ff6faecdd8966610d5cb9d85; \ + && if [ "$TARGETARCH" = "arm64" ]; then SUPERCRONIC_SHA1SUM=0b6c5bb743e0b0dafed1132198c81807927ac413; \ + elif [ "$TARGETARCH" = "amd64" ]; then SUPERCRONIC_SHA1SUM=e63c11a9726b775a6a11801e81af4f3fb926aa68; \ else echo "Target arch '${TARGETARCH}' is not supported"; exit 1; fi \ && curl -fsSLO --retry 5 --retry-delay 2 "${SUPERCRONIC_URL}" \ && echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \ diff --git a/utils/Dockerfile.slim.blueprint b/utils/Dockerfile.slim.blueprint index 70c8d432..198ddd94 100644 --- a/utils/Dockerfile.slim.blueprint +++ b/utils/Dockerfile.slim.blueprint @@ -409,10 +409,10 @@ ENV SUPERCRONIC_OPTIONS="" ONBUILD ARG INSTALL_CRON ONBUILD RUN if [ -n "$INSTALL_CRON" ]; then \ SUPERCRONIC="supercronic-${TARGETOS}-${TARGETARCH}" \ - && SUPERCRONIC_URL="https://github.com/aptible/supercronic/releases/download/v0.1.9/${SUPERCRONIC}" \ + && SUPERCRONIC_URL="https://github.com/aptible/supercronic/releases/download/v0.2.49/${SUPERCRONIC}" \ && echo ${SUPERCRONIC_URL} \ - && if [ "$TARGETARCH" = "arm64" ]; then SUPERCRONIC_SHA1SUM=e2714c43e7781bf1579c85aa61259245f56dbba1; \ - elif [ "$TARGETARCH" = "amd64" ]; then SUPERCRONIC_SHA1SUM=5ddf8ea26b56d4a7ff6faecdd8966610d5cb9d85; \ + && if [ "$TARGETARCH" = "arm64" ]; then SUPERCRONIC_SHA1SUM=0b6c5bb743e0b0dafed1132198c81807927ac413; \ + elif [ "$TARGETARCH" = "amd64" ]; then SUPERCRONIC_SHA1SUM=e63c11a9726b775a6a11801e81af4f3fb926aa68; \ else echo "Target arch '${TARGETARCH}' is not supported"; exit 1; fi \ && curl -fsSLO --retry 5 --retry-delay 2 "${SUPERCRONIC_URL}" \ && echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \ From 6cdeb1361735a42be8344e867a36dac5f0f6dc38 Mon Sep 17 00:00:00 2001 From: Mistral OZ - MIO Date: Thu, 8 Oct 2026 16:45:55 +0000 Subject: [PATCH 8/8] Document the extension dependencies and the binaries available without their path --- README.md | 2 ++ utils/README.blueprint.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 0054d373..8328d55d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This repository contains a set of developer-friendly, general purpose PHP images - 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). + - Composer binaries (`vendor/bin` and global binaries) and NodeJS binaries (`node_modules/.bin`) can be run without their path, including with `docker exec` / `docker compose exec` (e.g. `docker compose exec app phpstan`) - Everything is done to limit file permission issues that often arise when using Docker. The image is actively tested on Linux, Windows and MacOS @@ -195,6 +196,7 @@ This list can be outdated, you can verify by executing : `docker run --rm -it th - *ev* is not available in PHP 8.1+ - *mcrypt* is deprecated and its usage is discouraged: it is provided for legacy applications only +- The extensions required by an enabled extension are enabled with it, even if they are disabled: `igbinary` and `msgpack` for *memcached*, `igbinary` for *redis*, `mbstring` for *mailparse*, `curl` and `mysqlnd` for *swoole*, `mysqlnd` for *mysqli* and *pdo_mysql* ### Enabling/disabling extensions in the fat image diff --git a/utils/README.blueprint.md b/utils/README.blueprint.md index 64c6971a..ac6f54c3 100644 --- a/utils/README.blueprint.md +++ b/utils/README.blueprint.md @@ -11,6 +11,7 @@ This repository contains a set of developer-friendly, general purpose PHP images - 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). + - Composer binaries (`vendor/bin` and global binaries) and NodeJS binaries (`node_modules/.bin`) can be run without their path, including with `docker exec` / `docker compose exec` (e.g. `docker compose exec app phpstan`) - Everything is done to limit file permission issues that often arise when using Docker. The image is actively tested on Linux, Windows and MacOS {{ $image := .Orbit.Images }} @@ -125,6 +126,7 @@ This list can be outdated, you can verify by executing : `docker run --rm -it th - *ev* is not available in PHP 8.1+ - *mcrypt* is deprecated and its usage is discouraged: it is provided for legacy applications only +- The extensions required by an enabled extension are enabled with it, even if they are disabled: `igbinary` and `msgpack` for *memcached*, `igbinary` for *redis*, `mbstring` for *mailparse*, `curl` and `mysqlnd` for *swoole*, `mysqlnd` for *mysqli* and *pdo_mysql* ### Enabling/disabling extensions in the fat image