Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
### 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
* 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
* 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`
Expand Down
14 changes: 9 additions & 5 deletions Dockerfile.slim.apache
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

# |--------------------------------------------------------------------------
Expand Down Expand Up @@ -372,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 - \
Expand Down
14 changes: 9 additions & 5 deletions Dockerfile.slim.cli
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

# |--------------------------------------------------------------------------
Expand Down Expand Up @@ -277,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 - \
Expand Down
14 changes: 9 additions & 5 deletions Dockerfile.slim.fpm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

# |--------------------------------------------------------------------------
Expand Down Expand Up @@ -300,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 - \
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -423,6 +425,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.

Expand Down
2 changes: 2 additions & 0 deletions tests-suite/assets/php-startup-warning.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Loading an extension that does not exist makes PHP display a startup warning
extension=does_not_exist
13 changes: 13 additions & 0 deletions tests-suite/php-extensions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
############################################################
Expand Down
27 changes: 27 additions & 0 deletions tests-suite/tool-composer-bin.sh
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 10 additions & 0 deletions tests-suite/tool-startup-command.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
28 changes: 27 additions & 1 deletion tests-suite/users-rights.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
{
Expand All @@ -58,6 +58,32 @@ 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
############################################################
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
Expand Down
18 changes: 13 additions & 5 deletions utils/Dockerfile.slim.blueprint
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

# |--------------------------------------------------------------------------
Expand Down Expand Up @@ -401,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 - \
Expand Down
Loading