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 @@ -3,6 +3,13 @@

### Minor changes

* **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
* 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
* Add the `opentelemetry` extension on PHP 8.2+
Expand Down
1 change: 1 addition & 0 deletions Dockerfile.apache
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions Dockerfile.cli
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions Dockerfile.fpm
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 33 additions & 0 deletions Dockerfile.slim.apache
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -223,6 +224,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/<VirtualHost \*:80>/<VirtualHost *:${APACHE_HTTP_PORT}>/' /etc/apache2/sites-available/000-default.conf && \
sed -i 's/<VirtualHost \*:443>/<VirtualHost *:${APACHE_HTTPS_PORT}>/' /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

Expand Down Expand Up @@ -322,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
Expand Down Expand Up @@ -388,6 +397,30 @@ 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

# |--------------------------------------------------------------------------
# | 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
Expand Down
26 changes: 26 additions & 0 deletions Dockerfile.slim.cli
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -245,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
Expand Down Expand Up @@ -293,6 +295,30 @@ 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

# |--------------------------------------------------------------------------
# | 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
Expand Down
26 changes: 26 additions & 0 deletions Dockerfile.slim.fpm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -262,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
Expand Down Expand Up @@ -316,6 +318,30 @@ 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

# |--------------------------------------------------------------------------
# | 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
Expand Down
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -406,6 +417,42 @@ 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
```

## 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!
Expand Down Expand Up @@ -554,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.
Expand Down
39 changes: 39 additions & 0 deletions tests-suite/tool-composer-version.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
ARG COMPOSER_VERSION=2.4.4
FROM ${REPO}:${TAG_PREFIX}${PHP_VERSION}-${BRANCH}-slim-${BRANCH_VARIANT}${ARCH_SUFFIX}
EOF
assert_equals "0" "$?" "Image build failed"
RESULT="$(docker run ${RUN_OPTIONS} --rm "${DOCKER1_NAME}" composer --version --no-ansi 2>/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
}
58 changes: 58 additions & 0 deletions tests-suite/tool-daemon.sh
Original file line number Diff line number Diff line change
@@ -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
}
Loading