diff --git a/CHANGELOG.md b/CHANGELOG.md index ca53797..c4e5076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ ### Minor changes +* **2026-10-12** + * The README only describes the images and links to the documentation of each topic in the `docs` directory (extensions, php.ini, variants, processes, debugging, advanced features, contributing) + * 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+ 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 24b60e1..a5c418d 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 @@ -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///' /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 @@ -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 @@ -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 diff --git a/Dockerfile.slim.cli b/Dockerfile.slim.cli index 5a242b8..885fa37 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 @@ -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 @@ -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 diff --git a/Dockerfile.slim.fpm b/Dockerfile.slim.fpm index 3633cc2..2fe6690 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 @@ -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 @@ -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 diff --git a/README.md b/README.md index a736dfd..567e17d 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,13 @@ This repository contains a set of developer-friendly, general purpose PHP images - 2 types available: `slim` (no extensions preloaded) or `fat` (most common PHP extensions are built-in) - 3 variants available: `CLI`, `apache` and `fpm` - 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 images come with [Composer](https://getcomposer.org/) 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 +See the [documentation](#documentation) for all the features. + ## Images @@ -138,7 +140,7 @@ Images of non-maintained versions are still available but no longer receive fixe ## Usage -These images are based on the [official PHP image](https://hub.docker.com/_/php/). +The images are based on Ubuntu 24.04, with PHP from the [ondrej/php](https://launchpad.net/~ondrej/+archive/ubuntu/php) repository. Example with CLI: @@ -170,549 +172,21 @@ RUN npm install RUN npm run build ``` -## Extensions available - -This image comes with 2 "types": the *slim* and the **fat** image. - -These extensions are enabled by default in slim image: `calendar` `ctype` `curl` `date` `dom` `exif` `fileinfo` `filter` `ftp` `gettext` `iconv` `json` `mbstring` `opcache` `openssl` `pcntl` `pcre` `PDO` `Phar` `posix` `readline` `shmop` `Reflection` `session` `shmop` `SimpleXML` `sockets` `sodium` `SPL` `sysvmsg` `sysvsem` `sysvshm` `tokenizer` `xml` `xmlreader` `xmlwriter` `xsl` `zip` - -This list can be outdated, you can verify by executing : `docker run --rm -it thecodingmachine/php:8.4-v5-slim-cli php -m` - -The slim image provides a simple way to install the other extensions. You would typically use the "slim" image in a `Dockerfile` when building your own custom image. - -The fat image contains the most commonly used extensions. You would typically use it in a local or CI environment. - -### Fat image - -Below is a list of extensions available in this image: - -**Enabled by default (in addition to extensions enabled in Slim image):** `apcu`, `hash`, `iconv`, `igbinary`, `mysqli`, `mysqlnd`, `redis`, `soap`, `xsl`, `zlib` and all enabled in slim. - -**Available (can be enabled using environment variables):** `amqp` `ast` `bcmath` `blackfire` `bz2` `dba` `decimal` `ds` `enchant` `exif` `excimer` `ffi` `mailparse` `msgpack` `gd` `gettext` `gmp` `gnupg` `grpc` `igbinary` `imagick` `imap` `inotify` `intl` `ldap` `lz4` `mcrypt` `memcached` `mongodb` `odbc` `opentelemetry` `pcov` `pdo_dblib` `pdo_odbc` `pdo_pgsql` `pdo_sqlite` `pgsql` `pspell` `rdkafka` `shmop` `snmp` `sockets` `sqlite3` `swoole` `tidy` `uploadprogress` `uuid` `xdebug` `xmlrpc` `xsl` `yaml` `zstd` - -This list can be outdated, you can verify by executing : `docker run --rm -it thecodingmachine/php:8.4-v5-cli php -m` - -**Note**: - -- *ev* is not available in PHP 8.1+ -- *mcrypt* is deprecated and its usage is discouraged: it is provided for legacy applications only -- *imagick* supports SVG, WMF, DjVu and OpenEXR images in addition to the common formats. If you process - untrusted images, you can restrict the allowed formats in the ImageMagick policy (`/etc/ImageMagick-6/policy.xml`) -- 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 - -You can enable/disable extensions using the `PHP_EXTENSION_[extension_name]` environment variable. - -For instance: - -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:8.4-v5-apache-node24 - environment: - # Enable the PostgreSQL extension - PHP_EXTENSION_PGSQL: 1 - # Disable the Mysqli extension (otherwise it is enabled by default) - PHP_EXTENSION_MYSQLI: 0 -``` - -As an alternative, you can use the `PHP_EXTENSIONS` global variable: - -```bash -PHP_EXTENSIONS=pgsql gettext imap -``` - -### Compiling extensions in the slim image - -If you are using the slim image, you can automatically compile the extensions using the `PHP_EXTENSIONS` ARG in your Dockerfile. - -```Dockerfile -ARG PHP_EXTENSIONS="apcu mysqli pdo_mysql redis soap" -FROM thecodingmachine/php:8.4-v5-slim-apache -# The build will automatically trigger the download and compilation -# of the extensions (thanks to a ONBUILD hook in the slim image) -``` - -Beware : -* The `ARG PHP_EXTENSIONS` command must be written before the `FROM`. This is not a typo. -* `ARG PHP_EXTENSIONS=""` it's not the same as `ENV PHP_EXTENSIONS=""` -* You can't use `ARG PHP_EXTENSION_MYEXT=""` like the fat image. -* **Heads up**: if you are using multistage builds, the "ARG" variable must be put at the very top of the file (before the -first FROM): - -```Dockerfile -# The PHP_EXTENSIONS ARG will apply to the "slim" image -ARG PHP_EXTENSIONS="apcu mysqli pdo_mysql soap" - -FROM thecodingmachine/php:8.4-v5-apache-node24 AS builder - -COPY --chown=docker:docker sources/web . -RUN composer install &&\ - yarn install &&\ - yarn build - -# The slim image will automatically build the extensions from the list provided at the very top of the file. -FROM thecodingmachine/php:8.4-v5-slim-apache - -ENV APP_ENV=prod \ - APACHE_DOCUMENT_ROOT=public/ - -COPY --from=builder /var/www/html . -``` - -In the sample above, we use the fat image to perform a "yarn build", but copy the result in a slim image that does -not contain Node, and contains only required extensions. - -## Setting parameters in php.ini - -By default, the base `php.ini` file used is the [*development* php.ini](https://github.com/php/php-src/blob/PHP-8.4/php.ini-development) file that comes with PHP. - -You can use the production `php.ini` file using the `TEMPLATE_PHP_INI` environment variable: - -```bash -# Use the production php.ini file as a base -TEMPLATE_PHP_INI=production -``` - -You can override parameters in `php.ini` using the PHP_INI_XXX environment variables: - -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:8.4-v5-apache-node24 - environment: - # set the parameter memory_limit=1g - PHP_INI_MEMORY_LIMIT: 1g - # set the parameter error_reporting=EALL - PHP_INI_ERROR_REPORTING: E_ALL -``` - -Absolutely all `php.ini` parameters can be set. - -Internally, the image will map all environment variables starting with `PHP_INI_`. - -If your `php.ini` parameter contains a dot ("."), you can replace it with a double underscore ("__"). - -For instance: - -```bash -# Will set the parameter xdebug.remote_autostart=1 -PHP_INI_XDEBUG__REMOTE_AUTOSTART=1 -``` - -## Default working directory - -The working directory (the directory in which you should mount/copy your application) depends on the image variant -you are using: - -| Variant | Working directory | -|---------|-------------------| -| cli | `/usr/src/app` | -| apache | `/var/www/html` | -| fpm | `/var/www/html` | - - -## Changing Apache document root - -For the *apache* variant, you can change the document root of Apache (i.e. your "public" directory) by using the -`APACHE_DOCUMENT_ROOT` variable: - -```bash -# The root of your website is in the "public" directory: -APACHE_DOCUMENT_ROOT=public/ -``` - -If the APACHE_DOCUMENT_ROOT starts with a "/", it will be considered an absolute path. -If the APACHE_DOCUMENT_ROOT does not starts with a "/", it will be a path relative to "/var/www/html". - -```bash -# These 2 variables are identical -APACHE_DOCUMENT_ROOT=public/ -APACHE_DOCUMENT_ROOT=/var/www/html/public -``` - -## Enabling/disabling Apache extensions - -You can enable/disable Apache extensions using the `APACHE_EXTENSION_[extension_name]` environment variable. - -For instance: - -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:8.4-v5-apache-node24 - environment: - # Enable the DAV extension for Apache - APACHE_EXTENSION_DAV: 1 - # Enable the SSL extension for Apache - APACHE_EXTENSION_SSL: 1 -``` - -As an alternative, you can use the `APACHE_EXTENSIONS` global variable: - -```bash -APACHE_EXTENSIONS="dav ssl" -``` - -**Apache modules enabled by default:** `access_compat` `alias` `auth_basic` `authn_core` `authn_file` `authz_core` `authz_host` `authz_user` `autoindex` `deflate` `dir` `env` `expires` `filter` `mime` `mpm_prefork` `negotiation` `php8.4 (depend of your active version)` `reqtimeout` `rewrite` `setenvif` `status` - -**Apache modules available:** `access_compat` `actions` `alias` `allowmethods` `asis` `auth_basic` `auth_digest` `auth_form` `authn_anon` `authn_core` `authn_dbd` `authn_dbm` `authn_file` `authn_socache` `authnz_fcgi` `authnz_ldap` `authz_core` `authz_dbd` `authz_dbm` `authz_groupfile` `authz_host` `authz_owner` `authz_user` `autoindex` `brotli` `buffer` `cache` `cache_disk` `cache_socache` `cern_meta` `cgi` `cgid` `charset_lite` `data` `dav` `dav_fs` `dav_lock` `dbd` `deflate` `dialup` `dir` `dump_io` `echo` `env` `ext_filter` `expires` `file_cache` `filter` `headers` `heartbeat` `heartmonitor` `http2` `ident` `imagemap` `include` `info` `lbmethod_bybusyness` `lbmethod_byrequests` `lbmethod_bytraffic` `lbmethod_heartbeat` `ldap` `log_debug` `log_forensic` `lua` `macro` `md` `mime` `mime_magic` `mpm_event` `mpm_prefork` `mpm_worker` `negotiation` `php8.4 (depend of your active version)` `proxy` `proxy_ajp` `proxy_balancer` `proxy_connect` `proxy_express` `proxy_fcgi` `proxy_fdpass` `proxy_ftp` `proxy_hcheck` `proxy_html` `proxy_http` `proxy_http2` `proxy_scgi` `proxy_wstunnel` `ratelimit` `reflector` `remoteip` `reqtimeout` `request` `rewrite` `sed` `session` `session_cookie` `session_crypto` `session_dbd` `setenvif` `slotmem_plain` `slotmem_shm` `socache_dbm` `socache_memcache` `socache_redis` `socache_shmcb` `speling` `ssl` `status` `substitute` `suexec` `unique_id` `userdir` `usertrack` `vhost_alias` `xml2enc` - -This list can be outdated, you can verify by executing : `docker run --rm -it thecodingmachine/php:8.4-v5-slim-apache a2enmod` - - -## Debugging - -To enable XDebug` `you simply have to set the environment variable: - -```bash -PHP_EXTENSION_XDEBUG=1 -``` - -If you enable XDebug, the image will do its best to configure the `xdebug.client_host` to point back to your Docker host. - -Behind the scenes, the image will: - -- set the parameter `xdebug.mode=debug` -- if you are using a Linux machine, the `xdebug.client_host` IP will point to your Docker gateway -- if you are using a Windows or MaxOS machine, the `xdebug.client_host` IP will point to [`host.docker.internal`](https://docs.docker.com/docker-for-mac/networking/#use-cases-and-workarounds) or [`docker.for.mac.localhost`](https://docs.docker.com/docker-for-mac/networking/#use-cases-and-workarounds) - -If you want to debug directly inside your container (for example if you're using [VSCode devcontainers](https://code.visualstudio.com/docs/remote/containers)) you can overwrite the [`xdebug.client_host`](https://xdebug.org/docs/all_settings#client_host) value by setting the following environment variable: - -```bash -XDEBUG_CLIENT_HOST=127.0.0.1 -``` - -In that case the manually set value takes precedence over the mentioned ones above. - -## NodeJS - -The *fat* images come with a Node variant. You can use Node : 22 24 26. If you need a previous variant, [use thecodingmachine/php v4 images](https://github.com/thecodingmachine/docker-images-php/tree/v4), [php v3 images](https://github.com/thecodingmachine/docker-images-php/tree/v3) or [v1](https://github.com/thecodingmachine/docker-images-php/tree/7.2-v1). - -If you use the *slim* images, you can install a NodeJS version with a simple ARG during the build: - -```Dockerfile -ARG NODE_VERSION=24 -FROM thecodingmachine/php:8.4-v5-slim-apache -# The build will automatically trigger the download of Node 24 -# (thanks to a ONBUILD hook in the slim image) -``` - -Beware! The `ARG NODE_VERSION` command must be written before the `FROM`. This is not a typo. - -`NODE_VERSION` can take any valid node versions (tested only with the versions provided here but should work too for some previous versions) - -## Permissions - -Ever faced file permission issues with Docker? Good news, this is a thing of the past! - -If you are used to running Docker containers with the base PHP image, you probably noticed that when running commands -(like `composer install`) within the container, files are associated to the `root` user. This is because the base user -of the image is "root". - -When you mount your project directory into `/var/www/html`, it would be great if the default user used by Docker could -be your current host user. - -The problem with Docker is that the container and the host do not share the same list of users. For instance, you might -be logged in on your host computer as `superdev` (ID: 1000), and the container has no user whose ID is 1000. - -The *thecodingmachine/php* images solve this issue with a bit of black magic: - -The image contains a user named `docker`. On container startup, the startup script will look at the owner of the -working directory (`/var/www/html` for Apache/PHP-FPM, or `/usr/src/app` for CLI). The script will then assume that -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. - -The direct result is that, in development: - - - Your PHP application can edit any file - - Your container can edit any file - - You can still edit any file created by Apache or by the container in CLI - -### Using this image in production - -By changing the Apache user to be `docker:docker`, we are lowering the security. -This is OK for a development environment, but this should be avoided in production. -Indeed, in production, Apache should not be allowed to edit PHP files of your application. If for some reason, an -attacker manages to change PHP files using a security hole, he could then run any PHP script by editing the PHP files -of your application. - -In production, you want to change back the Apache user to www-data. - -This can be done easily: - -**Dockerfile** -```Dockerfile -FROM thecodingmachine/php:8.4-v5-apache - -# ... - -# Change back Apache user and group to www-data -ENV APACHE_RUN_USER=www-data \ - APACHE_RUN_GROUP=www-data -``` - - -## Setting up CRON jobs - -You can set up CRON jobs using environment variables too. - -To do this, you need to configure 3 variables: - -```bash -# configure the user that will run cron (defaults to root) -CRON_USER=root -# configure the schedule for the cron job (here: run every minute) -CRON_SCHEDULE=* * * * * -# last but not least, configure the command -CRON_COMMAND=vendor/bin/console do:stuff -``` - -By default, CRON output will be redirected to Docker output. - -If you have more than one job to run, you can suffix your environment variable with the same string. For instance: - -```bash -CRON_USER_1=root -CRON_SCHEDULE_1=* * * * * -CRON_COMMAND_1=vendor/bin/console do:stuff - -CRON_USER_2=www-data -CRON_SCHEDULE_2=0 3 * * * -CRON_COMMAND_2=vendor/bin/console other:stuff -``` - -Cron is **installed by default in the fat images**. If you are using the "*slim*" images, you need to install it by passing -a single argument before the "FROM" clause in your Dockerfile: - -```Dockerfile -ARG INSTALL_CRON=1 -FROM thecodingmachine/php:8.4-v5-slim-apache -# The build triggers automatically the installation of Cron -``` - -**Important**: The cron runner we use is "Supercronic" and not the orginial "cron" that has a number of issues -with containers. Even with Supercronic, the architecture of cron was never designed with Docker in mind -(Cron is way older than Docker). It will run correctly on your container. If at some point you want to scale and add -more containers, it will run on all your containers. At that point, if you only want to run a Cron task once for your -application (and not once per container), you might want to have a look at alternative solutions like -[Tasker](https://github.com/opsxcq/tasker) or use the native features of your orchestrator (if you use Kubernetes, -you have a native task runner available), or one of the many other alternatives. - -Please notice that by default, containers are running in the UTC timezone. So your CRONs will run at UTC time. -If you want to change a timezone in a container, you can use the `TZ` environment variable. - -```bash -# Run this cron at 1 am, Paris time -TZ=Europe/Paris -CRON_SCHEDULE_1=0 1 * * * -CRON_COMMAND_1=vendor/bin/console do:stuff -``` - -### Supercronic options -To specify Supercronic options you can set the `SUPERCRONIC_OPTIONS` environment variable. - -This can be used to enable duplicate jobs. Per default, Supercronic will wait for a given job to finish before that job is scheduled again.\ -With the option `-overlapping` Supercronic will run duplicate instances of the jobs instead of waiting for them. - -```bash -SUPERCRONIC_OPTIONS=-overlapping - -# Or multiple options -SUPERCRONIC_OPTIONS=-overlapping -debug -``` - -For more options see see the [Supercronic Documentation](https://github.com/aptible/supercronic/blob/master/README.md). - -## Launching commands on container startup - -You can launch commands on container startup using the `STARTUP_COMMAND_XXX` environment variables. -This can be very helpful to install dependencies or apply database patches for instance: - -```bash -STARTUP_COMMAND_1=composer install -STARTUP_COMMAND_2=vendor/bin/doctrine orm:schema-tool:update -``` - -As an alternative, the images will look into the container for an executable file named `/etc/container/startup.sh`. - -If such a file is mounted in the image, it will be executed on container startup. - -```bash -docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp \ - -v $PWD/my-startup-script.sh:/etc/container/startup.sh thecodingmachine/php:8.4-v5-cli php your-script.php -``` +## Documentation -## Using the CLI variant +Most features are configured with environment variables: -The CLI images (thecodingmachine/php:8.4-v5-cli) expect a command to be passed in parameter. -You should override the Docker "command". +| Topic | Content | Main variables | +|----------------------------------------------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------| +| [PHP extensions](docs/extensions.md) | Extensions of the slim and fat images, enabling them, compiling them in a slim image | `PHP_EXTENSION_*`, `PHP_EXTENSIONS` | +| [PHP configuration](docs/php-configuration.md) | Base `php.ini` file and `php.ini` parameters | `TEMPLATE_PHP_INI`, `PHP_INI_*` | +| [Variants](docs/variants.md) | *cli*, *apache* (document root, ports, modules, user) and *fpm* | `APACHE_DOCUMENT_ROOT`, `APACHE_HTTP_PORT`, `APACHE_EXTENSION_*` | +| [Processes](docs/processes.md) | Startup commands, cron jobs and daemons run along with the main command | `STARTUP_COMMAND_*`, `CRON_*`, `DAEMON_*` | +| [Debugging and profiling](docs/debugging.md) | Xdebug and Blackfire | `PHP_EXTENSION_XDEBUG`, `XDEBUG_CLIENT_HOST`, `BLACKFIRE_AGENT` | +| [Advanced features](docs/advanced.md) | Permissions, NodeJS, Composer version, locales, SSH keys, Kubernetes | `DOCKER_USER`, `COMPOSER_VERSION`, `LOCALES` | -Important! You should not override the Docker "entrypoint". - -**Usage in a Dockerfile:** - -```Dockerfile -FROM thecodingmachine/php:8.4-v5-cli - -CMD ["php", "myprogram.php", "some_param"] -``` - -**Usage with Docker compose:** - -**docker-compose.yml** -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:8.4-v5-cli - command: php myprogram.php some_param -``` - -## Registering SSH private keys - -If your PHP project as a dependency on [a package stored in a private GIT repository](https://getcomposer.org/doc/05-repositories.md#using-private-repositories), -your `composer install` commands will not work unless you register your private key in the container. - -You have several options to do this. - -### Option 1: mount your keys in the container directly - -This option is the easiest way to go if you are using the image on a development environment. - -**docker-compose.yml** -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:8.4-v5-apache-node24 - volumes: - - ~/.ssh:/home/docker/.ssh -``` - -### Option 2: store the keys from environment variables or build arguments - -Look at this option if you are building a Dockerfile from this image. - -The first thing to do is to get the signature of the server you want to connect to. - -```bash -$ ssh-keyscan myserver.com -``` - -Copy the output and put it in an environment variable. We assume the content is stored in `$SSH_KNOWN_HOSTS`. - -Now, let's write a Dockerfile. - -**Dockerfile** -```yml -FROM thecodingmachine/php:8.4-v5-apache - -ARG SSH_PRIVATE_KEY -ARG SSH_KNOWN_HOSTS - -# Let's register the private key -RUN ssh-add <(echo "$SSH_PRIVATE_KEY") -# Let's add the server to the list of known hosts. -RUN echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts -``` - -Finally, when triggering the build, you must pass the 2 variables as [build arguments](https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables-build-arg): - -```bash -$ docker build -t my_image --build-arg SSH_PRIVATE_KEY="$SSH_PRIVATE_KEY" --build-arg SSH_KNOWN_HOSTS="$SSH_KNOWN_HOSTS" . -``` - -## Usage in Kubernetes - -If you plan to use this image in Kubernetes, please be aware that the image internally uses `sudo`. This is because the -default user (`docker`) needs to be able to edit php config files as `root`. - -Kubernetes has a security setting (`allowPrivilegeEscalation`) that can disallow the use of `sudo`. The use of this flag -breaks the image and in the logs, you will find the message: - -``` -sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges? -``` - -Please be sure that this option is never set to false: - -```yml -apiVersion: v1 -kind: Pod -# ... -spec: - containers: - - name: foobar - image: thecodingmachine/php:8.4-v5-apache - securityContext: - allowPrivilegeEscalation: true # never use "false" here. -``` - -## Profiling with Blackfire - -This image comes with the [Blackfire]() PHP probe. You can install it using: - -```bash -PHP_EXTENSION_BLACKFIRE=1 -``` - -By default, the image expects that the blackfire agent is started in another container. - -Your `docker-compose.yml` file will typically look like this: - -**docker-compose.yml** -```yml -version: '3.3' -services: - php: - image: thecodingmachine/php:8.4-v5-apache - ports: - - "80:80" - environment: - PHP_EXTENSION_BLACKFIRE: 1 - blackfire: - image: blackfire/blackfire - environment: - # Exposes the host BLACKFIRE_SERVER_ID and TOKEN environment variables. - - BLACKFIRE_SERVER_ID - - BLACKFIRE_SERVER_TOKEN - # You can also use global environment credentials : - # BLACKFIRE_SERVER_ID: SERVER-ID - # BLACKFIRE_SERVER_TOKEN: SERVER-TOKEN -``` - -See [Blackfire Docker documentation](https://blackfire.io/docs/integrations/docker#running-the-agent) for more information. - -The image assumes that the Blackfire agent is accessible via the `blackfire` URL (like in the exemple above). -If for some reason, the container name is not "blackfire", you can customize the agent URL with the `BLACKFIRE_AGENT` environment variable: - -**docker-compose.yml** -```yml -version: '3.3' -services: - php: - image: thecodingmachine/php:8.4-v5-apache - environment: - PHP_EXTENSION_BLACKFIRE: 1 - BLACKFIRE_AGENT: myblackfire - # ... - myblackfire: - image: blackfire/blackfire - environment: - # ... -``` +The slim images install some tools during the build of your image with an ARG before the `FROM`: `PHP_EXTENSIONS`, +`INSTALL_CRON`, `INSTALL_LOCALES`, `NODE_VERSION` and `COMPOSER_VERSION`. ## Migrating from older image versions @@ -720,54 +194,8 @@ Check the [migration notes](MIGRATING.md). ## Contributing -There is one branch per minor PHP version and version of the image. - -Please submit your pull requests to the lowest branch where is applies. - -The Dockerfiles and the README are generated from a template using [Orbit](https://github.com/gulien/orbit). - -If you want to modify a Dockerfile or the README, you should instead edit the `utils/Dockerfile.blueprint` -or `utils/README.blueprint.md` and then run the command: - -```bash -$ orbit run generate -``` - -This command will generate all the files from the "blueprint" templates. - -You can then test your changes using the `build-and-test.sh` command: - -```bash -PHP_VERSION=8.4 BRANCH=v5 VARIANT=apache ./build-and-test.sh -``` - -### Additional environment in build-and-test.sh - -- BUILDER: either build or buildx depending on your configuration. -Defaults to build -- BLACKFIRE_VERSION: defaults to 2. You can install v1 but will include some CVEs in the build. -- PLATFORM: Docker will default to your architecture for building images. However, if you have QEMU set up in your machine, you can try building for another architecture like linux/arm64 - -Only one platform at a time is supported during the build and test script execution. - -*APPLE SILICON CONSIDERATIONS* - -Filesystem management works differently in Apple's macOS, so, if you're trying to build a linux/arm64 image (that is best suited for using in a M1/M2 Mac than a linux/amd64 one), there's a high likelihood that the filesystem user and permissions with busybox are going to fail. - -Although the test fails, when using the built image, everything works as expected. - -Run a virtual machine with linux/arm64 with Docker installed in it and, then, build and test the image. You'll take advantage of the ARM speed and will also be able to run the unit tests properly. - - -### Adding additional images - -To add a new version (php, node, apache, ...), please edit the following files : - -- utils/README.blueprint.md - - Add your image in this section: Images -- orbit.yml: Your image in generation task -- .travis.yml: To check the new image -- build-and-test.sh: Add your image in test +The Dockerfiles and this README are generated from the templates of the `utils` directory: see the +[contributing guide](docs/contributing.md) to edit, build and test the images. ## Special thanks diff --git a/docs/advanced.md b/docs/advanced.md new file mode 100644 index 0000000..52ddb8f --- /dev/null +++ b/docs/advanced.md @@ -0,0 +1,142 @@ +# Advanced features + +## Permissions + +The images limit the file permission issues that often arise with Docker. + +The commands of a container usually run as `root`, so the files they create (e.g. by `composer install`) belong to +`root` on your host. The container and the host do not share their users: you might be logged in as `superdev` +(ID 1000) on your host while the container has no user with ID 1000. + +The images contain a `docker` user. On startup, the entrypoint looks at the owner of the working directory +(`/var/www/html` for *apache* and *fpm*, `/usr/src/app` for *cli*) and **changes the ID of the `docker` user** to the +ID of this owner. The commands, Apache and PHP-FPM then run as `docker`: + +- your PHP application can edit any file of your project; +- you can edit any file created by the container. + +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. + +`DOCKER_USER` sets the user explicitly, with a user name of the image or an ID: + +```bash +DOCKER_USER=1001 +``` + +**In production**, Apache should not be allowed to edit the PHP files of your application: if an attacker can change +them through a security hole, they can run any PHP code. Run Apache as `www-data` (see +[Apache user](variants.md#apache-user)). + +## NodeJS + +The fat images have NodeJS variants (e.g. `thecodingmachine/php:8.4-v5-apache-node24`), see the +[list of images](../README.md#images). + +With a slim image, the `NODE_VERSION` ARG installs NodeJS (with npm and yarn) during the build: + +```Dockerfile +ARG NODE_VERSION=24 +FROM thecodingmachine/php:8.4-v5-slim-apache +``` + +The `ARG NODE_VERSION` instruction must be written before the `FROM`. This is not a typo. + +The binaries of `node_modules/.bin` and of Composer (`vendor/bin`, global binaries) can be run without their path, +including with `docker exec` / `docker compose exec` (e.g. `docker compose exec app phpstan`). + +## Composer version + +The images come with the latest Composer version. `COMPOSER_VERSION` installs another one: 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 a slim image, 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. `LOCALES` lists the locales to generate when +the container starts: + +```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**. With a slim image, install them with an ARG before +the `FROM`: + +```Dockerfile +ARG INSTALL_LOCALES=1 +FROM thecodingmachine/php:8.4-v5-slim-apache +``` + +## SSH private keys + +If your project depends on a [package stored in a private Git repository](https://getcomposer.org/doc/05-repositories.md#using-private-repositories), +`composer install` needs your SSH private key. + +**In development**, mount your keys in the container: + +```yml +services: + my_app: + image: thecodingmachine/php:8.4-v5-apache + volumes: + - ~/.ssh:/home/docker/.ssh +``` + +**In a Dockerfile**, pass the key and the signature of the server (`ssh-keyscan myserver.com`) as build arguments: + +```Dockerfile +FROM thecodingmachine/php:8.4-v5-apache + +ARG SSH_PRIVATE_KEY +ARG SSH_KNOWN_HOSTS + +RUN ssh-add <(echo "$SSH_PRIVATE_KEY") +RUN echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts +``` + +```bash +$ docker build -t my_image --build-arg SSH_PRIVATE_KEY="$SSH_PRIVATE_KEY" --build-arg SSH_KNOWN_HOSTS="$SSH_KNOWN_HOSTS" . +``` + +Beware: the build arguments are visible in the image history. Prefer +[build secrets](https://docs.docker.com/build/building/secrets/) for an image you distribute. + +## Kubernetes + +The entrypoint uses `sudo`: the `docker` user needs to edit the PHP configuration as `root`. The Kubernetes +`allowPrivilegeEscalation: false` security setting forbids `sudo` and breaks the image, with this message in the logs: + +``` +sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges? +``` + +Never set this option to `false`: + +```yml +apiVersion: v1 +kind: Pod +# ... +spec: + containers: + - name: foobar + image: thecodingmachine/php:8.4-v5-apache + securityContext: + allowPrivilegeEscalation: true # never use "false" here +``` + +If your cluster forbids binding the ports below 1024, see [Apache ports](variants.md#ports). diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..890c331 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,47 @@ +# Contributing + +Submit your pull requests to the `v5` branch. + +## Generated files + +The `Dockerfile.*` files, `docker-bake.hcl` and `README.md` are generated from the templates of the `utils` directory +with [Orbit](https://github.com/gulien/orbit): edit `utils/Dockerfile.*.blueprint`, `utils/docker-bake.blueprint.hcl` +or `utils/README.blueprint.md`, then run: + +```bash +$ make blueprint +``` + +The pages of the `docs` directory are not generated: edit them directly. + +## Building and testing + +The tests use [bash_unit](https://github.com/pgrange/bash_unit) (see the [test suite](../tests-suite/README.md)). +Build and test a PHP version (*cli*, *apache* and *fpm* variants): + +```bash +$ make test-8.4 +``` + +`make test-quick` builds and tests the *cli* variant of the maintained PHP versions, without NodeJS. + +To test images already built, run the test suite directly: + +```bash +$ PHP_VERSION=8.4 BRANCH=v5 VARIANT=apache ./tests-suite/bash_unit -f tap ./tests-suite/*.sh +``` + +The variables of [`tests-suite/config`](../tests-suite/config) select the images to test, e.g. `TAG_PREFIX` (images +built with `TAG_PREFIX=mytest- docker buildx bake ...`) or `PLATFORM` (e.g. `linux/arm64` with QEMU). + +**Apple Silicon:** the file system of macOS manages the users and permissions differently: the permission tests of +a `linux/arm64` image can fail on a Mac, while the images work as expected. Run the tests in a `linux/arm64` virtual +machine instead. + +## Adding a PHP or NodeJS version + +- `utils/README.blueprint.md`: the version lists of the "Images" section +- `utils/docker-bake.blueprint.hcl`: the build targets +- `.github/workflows/workflow.yml` (maintained versions) or `.github/workflows/legacy-v5.yml` (non-maintained + versions): the CI matrix +- `extensions/`: the extensions available for this PHP version diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 0000000..a016682 --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,73 @@ +# Debugging and profiling + +## Xdebug + +Enable the Xdebug extension: + +```bash +PHP_EXTENSION_XDEBUG=1 +``` + +The image then configures Xdebug to connect back to your Docker host: + +- `xdebug.mode` is set to `debug` +- on Linux, `xdebug.client_host` is the IP of your Docker gateway +- on Windows and macOS, `xdebug.client_host` is the IP of + [`host.docker.internal`](https://docs.docker.com/desktop/features/networking/#use-cases-and-workarounds) + (or `docker.for.mac.localhost` on old Docker versions) + +To debug from inside the container (for instance with [VSCode devcontainers](https://code.visualstudio.com/docs/remote/containers)), +set `xdebug.client_host` yourself with the `XDEBUG_CLIENT_HOST` variable: + +```bash +XDEBUG_CLIENT_HOST=127.0.0.1 +``` + +The other Xdebug parameters can be set like any `php.ini` parameter (see [PHP configuration](php-configuration.md)), +e.g. `PHP_INI_XDEBUG__START_WITH_REQUEST=yes`. + +## Blackfire + +The fat images come with the [Blackfire](https://blackfire.io) PHP probe and CLI. Enable the probe with: + +```bash +PHP_EXTENSION_BLACKFIRE=1 +``` + +The probe expects the Blackfire agent to run in another container, reachable with the `blackfire` host name: + +```yml +services: + php: + image: thecodingmachine/php:8.4-v5-apache + ports: + - "80:80" + environment: + PHP_EXTENSION_BLACKFIRE: 1 + blackfire: + image: blackfire/blackfire + environment: + # Exposes the BLACKFIRE_SERVER_ID and BLACKFIRE_SERVER_TOKEN environment variables of the host + - BLACKFIRE_SERVER_ID + - BLACKFIRE_SERVER_TOKEN +``` + +If the agent has another host name, set it with the `BLACKFIRE_AGENT` variable: + +```yml +services: + php: + image: thecodingmachine/php:8.4-v5-apache + environment: + PHP_EXTENSION_BLACKFIRE: 1 + BLACKFIRE_AGENT: myblackfire + myblackfire: + image: blackfire/blackfire + # ... +``` + +See the [Blackfire Docker documentation](https://blackfire.io/docs/integrations/docker) for more information. + +With a slim image, add `blackfire` to the [`PHP_EXTENSIONS` ARG](extensions.md#compiling-extensions-in-the-slim-images). +The probe version 2 is installed by default; the `BLACKFIRE_VERSION=1` build ARG installs the version 1, which has +known security issues. diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 0000000..8057d62 --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,103 @@ +# PHP extensions + +The images come in 2 *types*: + +- the **slim** images contain only the basic extensions: you would typically use them in a `Dockerfile` that compiles + the extensions your application needs; +- the **fat** images contain the most commonly used extensions, ready to be enabled with environment variables: you + would typically use them in a local or CI environment. + +## Extensions of the slim images + +These extensions are enabled by default in the slim images: `calendar` `ctype` `curl` `date` `dom` `exif` `fileinfo` +`filter` `ftp` `gettext` `iconv` `json` `mbstring` `opcache` `openssl` `pcntl` `pcre` `PDO` `Phar` `posix` `readline` +`Reflection` `session` `shmop` `SimpleXML` `sockets` `sodium` `SPL` `sysvmsg` `sysvsem` `sysvshm` `tokenizer` `xml` +`xmlreader` `xmlwriter` `xsl` `zip` + +This list can be outdated, you can verify it with: `docker run --rm thecodingmachine/php:8.4-v5-slim-cli php -m` + +## Extensions of the fat images + +**Enabled by default (in addition to the extensions of the slim images):** `apcu` `hash` `iconv` `igbinary` `mysqli` +`mysqlnd` `pdo_mysql` `redis` `soap` `xsl` `zlib` + +**Available (can be enabled with environment variables):** `amqp` `ast` `bcmath` `blackfire` `bz2` `dba` `decimal` +`ds` `enchant` `exif` `excimer` `ffi` `mailparse` `msgpack` `gd` `gettext` `gmp` `gnupg` `grpc` `igbinary` `imagick` +`imap` `inotify` `intl` `ldap` `lz4` `mcrypt` `memcached` `mongodb` `odbc` `opentelemetry` `pcov` `pdo_dblib` +`pdo_odbc` `pdo_pgsql` `pdo_sqlite` `pgsql` `pspell` `rdkafka` `shmop` `snmp` `sockets` `sqlite3` `swoole` `tidy` +`uploadprogress` `uuid` `xdebug` `xmlrpc` `xsl` `yaml` `zstd` + +This list can be outdated, you can verify it with: `docker run --rm thecodingmachine/php:8.4-v5-cli php -m` + +**Notes:** + +- *ev* is not available in PHP 8.1+ +- *mcrypt* is deprecated and its usage is discouraged: it is provided for legacy applications only +- *imagick* supports SVG, WMF, DjVu and OpenEXR images in addition to the common formats. If you process untrusted + images, you can restrict the allowed formats in the ImageMagick policy (`/etc/ImageMagick-6/policy.xml`) +- 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* +- To debug or profile your application with *xdebug* or *blackfire*, see [Debugging and profiling](debugging.md) + +## Enabling/disabling extensions in the fat images + +Use the `PHP_EXTENSION_` environment variables: + +```yml +services: + my_app: + image: thecodingmachine/php:8.4-v5-apache + environment: + # Enable the PostgreSQL extension + PHP_EXTENSION_PGSQL: 1 + # Disable the Mysqli extension (enabled by default) + PHP_EXTENSION_MYSQLI: 0 +``` + +As an alternative, the `PHP_EXTENSIONS` variable lists the extensions to enable: + +```bash +PHP_EXTENSIONS=pgsql gettext imap +``` + +## Compiling extensions in the slim images + +With a slim image, the `PHP_EXTENSIONS` ARG lists the extensions to install during the build: + +```Dockerfile +ARG PHP_EXTENSIONS="apcu mysqli pdo_mysql redis soap" +FROM thecodingmachine/php:8.4-v5-slim-apache +# The build automatically downloads and compiles the extensions +# (thanks to an ONBUILD hook in the slim image) +``` + +Beware: + +- The `ARG PHP_EXTENSIONS` instruction must be written before the `FROM`. This is not a typo. +- `ARG PHP_EXTENSIONS=""` is not the same as `ENV PHP_EXTENSIONS=""`. +- `ARG PHP_EXTENSION_` (like the environment variables of the fat images) is not supported. +- With a multi-stage build, the `ARG` must be at the very top of the file (before the first `FROM`): + +```Dockerfile +# The PHP_EXTENSIONS ARG applies to the slim image +ARG PHP_EXTENSIONS="apcu mysqli pdo_mysql soap" + +FROM thecodingmachine/php:8.4-v5-apache-node24 AS builder + +COPY --chown=docker:docker sources/web . +RUN composer install &&\ + yarn install &&\ + yarn build + +# The slim image builds the extensions listed at the very top of the file +FROM thecodingmachine/php:8.4-v5-slim-apache + +ENV APP_ENV=prod \ + APACHE_DOCUMENT_ROOT=public/ + +COPY --from=builder /var/www/html . +``` + +In this example, the fat image runs the `yarn build`, and the result is copied in a slim image that does not contain +NodeJS and contains only the required extensions. diff --git a/docs/php-configuration.md b/docs/php-configuration.md new file mode 100644 index 0000000..8ee9b7d --- /dev/null +++ b/docs/php-configuration.md @@ -0,0 +1,36 @@ +# PHP configuration (php.ini) + +## Base php.ini file + +By default, the base `php.ini` file is the [*development* php.ini](https://github.com/php/php-src/blob/PHP-8.4/php.ini-development) +file that comes with PHP. The `TEMPLATE_PHP_INI` variable selects the *production* one instead: + +```bash +TEMPLATE_PHP_INI=production +``` + +## Setting php.ini parameters + +The `PHP_INI_` environment variables set any `php.ini` parameter: + +```yml +services: + my_app: + image: thecodingmachine/php:8.4-v5-apache + environment: + # memory_limit=1g + PHP_INI_MEMORY_LIMIT: 1g + # error_reporting=E_ALL + PHP_INI_ERROR_REPORTING: E_ALL +``` + +If the parameter contains a dot (`.`), replace it with a double underscore (`__`): + +```bash +# opcache.validate_timestamps=0 +PHP_INI_OPCACHE__VALIDATE_TIMESTAMPS=0 +# intl.default_locale=fr_FR.UTF-8 +PHP_INI_INTL__DEFAULT_LOCALE=fr_FR.UTF-8 +``` + +The parameters are applied to PHP CLI and to the PHP of the *apache* or *fpm* variant. diff --git a/docs/processes.md b/docs/processes.md new file mode 100644 index 0000000..3ab7416 --- /dev/null +++ b/docs/processes.md @@ -0,0 +1,115 @@ +# Processes: startup commands, cron jobs and daemons + +On each start, the entrypoint of the image: + +1. configures PHP (extensions, `php.ini`), Apache, Composer and the locales from the environment variables; +2. runs the [startup commands](#startup-commands); +3. runs the main command (the Docker *command*: Apache, PHP-FPM or your CLI command), along with + [Supercronic](#cron-jobs) and the [daemons](#daemons) if any. + +## Startup commands + +The `STARTUP_COMMAND_` variables are run in the order of their suffix before the main command, for instance +to install dependencies or to apply database migrations: + +```bash +STARTUP_COMMAND_1=composer install +STARTUP_COMMAND_2=vendor/bin/doctrine orm:schema-tool:update +``` + +They are run by the `docker` user (see [Permissions](advanced.md#permissions)), or by the user of ID `UID` if this +variable is set (e.g. `UID=0` for `root`). The container stops if a command fails. + +As an alternative, an executable file mounted at `/etc/container/startup.sh` is run before these commands: + +```bash +$ docker run --rm -v "$PWD":/usr/src/app -v "$PWD/my-startup-script.sh":/etc/container/startup.sh \ + thecodingmachine/php:8.4-v5-cli php your-script.php +``` + +## Cron jobs + +The `CRON_SCHEDULE`, `CRON_COMMAND` and (optionally) `CRON_USER` variables define a cron job: + +```bash +# The user that runs the job (defaults to root) +CRON_USER=root +# The schedule (here: every minute) +CRON_SCHEDULE=* * * * * +# The command +CRON_COMMAND=vendor/bin/console do:stuff +``` + +For several jobs, add the same suffix to the variables of each job: + +```bash +CRON_USER_1=root +CRON_SCHEDULE_1=* * * * * +CRON_COMMAND_1=vendor/bin/console do:stuff + +CRON_USER_2=www-data +CRON_SCHEDULE_2=0 3 * * * +CRON_COMMAND_2=vendor/bin/console other:stuff +``` + +The output of the jobs is sent to the Docker logs. + +The containers run in the UTC timezone: the `TZ` variable changes it. + +```bash +# Run this job at 1 am, Paris time +TZ=Europe/Paris +CRON_SCHEDULE_1=0 1 * * * +CRON_COMMAND_1=vendor/bin/console do:stuff +``` + +The jobs are run by [Supercronic](https://github.com/aptible/supercronic), a cron replacement designed for containers. +It is **installed by default in the fat images**. With a slim image, install it with an ARG before the `FROM`: + +```Dockerfile +ARG INSTALL_CRON=1 +FROM thecodingmachine/php:8.4-v5-slim-apache +``` + +`SUPERCRONIC_OPTIONS` passes options to Supercronic. For instance, by default, a job is not started again while its +previous run is not finished; `-overlapping` allows it: + +```bash +SUPERCRONIC_OPTIONS=-overlapping -debug +``` + +See the [Supercronic documentation](https://github.com/aptible/supercronic/blob/master/README.md) for the other +options. + +Supercronic is supervised like the [daemons](#daemons): if it exits, the container stops. + +**Important**: the jobs run in every container started from your configuration. If you scale your application to +several containers, a job that must run once per application (not once per container) belongs to your orchestrator +(e.g. a Kubernetes CronJob) or to a dedicated container. + +## Daemons + +**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_` variables start long-running processes along +with the main command. They are run by the `docker` user (see [Permissions](advanced.md#permissions)), or by the user +set in the `DAEMON_USER_` 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 +``` + +A command that finishes, like `composer install`, is a [startup command](#startup-commands), not a daemon: its end +would stop the container. + +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. + +When the container stops (`docker stop` or a process that exited), Apache and PHP-FPM are stopped gracefully (the +running requests are completed), and the other processes (another main command, the daemons and Supercronic) receive +`SIGTERM`. diff --git a/docs/variants.md b/docs/variants.md new file mode 100644 index 0000000..c172afb --- /dev/null +++ b/docs/variants.md @@ -0,0 +1,114 @@ +# Variants: cli, apache and fpm + +| Variant | Default command | Working directory | Port | +|---------|----------------------|-------------------|---------------------------| +| cli | `php -v` | `/usr/src/app` | - | +| apache | `apache2-foreground` | `/var/www/html` | `80` (HTTP) | +| fpm | `php-fpm` | `/var/www/html` | `9000` (FastCGI, no HTTP) | + +Mount or copy your application in the working directory. + +## cli + +The *cli* images expect a command: override the Docker *command*, never the *entrypoint* (it configures PHP, the +user, the cron jobs... see [Processes](processes.md)). + +```Dockerfile +FROM thecodingmachine/php:8.4-v5-cli + +CMD ["php", "myprogram.php", "some_param"] +``` + +```yml +services: + my_app: + image: thecodingmachine/php:8.4-v5-cli + command: php myprogram.php some_param +``` + +## apache + +Apache runs PHP with `mod_php`. + +### Document root + +`APACHE_DOCUMENT_ROOT` sets the document root (your "public" directory). A relative path is relative to +`/var/www/html`: + +```bash +# These 2 variables are identical +APACHE_DOCUMENT_ROOT=public/ +APACHE_DOCUMENT_ROOT=/var/www/html/public +``` + +### Ports + +Apache listens on port 80 (and 443 when the `ssl` module is enabled). `APACHE_HTTP_PORT` and `APACHE_HTTPS_PORT` +change these ports, for instance to use unprivileged ports: + +```bash +APACHE_HTTP_PORT=8080 +APACHE_HTTPS_PORT=8443 +``` + +### Apache modules + +Use the `APACHE_EXTENSION_` environment variables: + +```yml +services: + my_app: + image: thecodingmachine/php:8.4-v5-apache + environment: + APACHE_EXTENSION_DAV: 1 + APACHE_EXTENSION_SSL: 1 +``` + +As an alternative, the `APACHE_EXTENSIONS` variable lists the modules to enable: + +```bash +APACHE_EXTENSIONS="dav ssl" +``` + +**Modules enabled by default:** `access_compat` `alias` `auth_basic` `authn_core` `authn_file` `authz_core` +`authz_host` `authz_user` `autoindex` `deflate` `dir` `env` `expires` `filter` `mime` `mpm_prefork` `negotiation` +`php8.x` (your PHP version) `reqtimeout` `rewrite` `setenvif` `status` + +**Modules available:** `access_compat` `actions` `alias` `allowmethods` `asis` `auth_basic` `auth_digest` `auth_form` +`authn_anon` `authn_core` `authn_dbd` `authn_dbm` `authn_file` `authn_socache` `authnz_fcgi` `authnz_ldap` +`authz_core` `authz_dbd` `authz_dbm` `authz_groupfile` `authz_host` `authz_owner` `authz_user` `autoindex` `brotli` +`buffer` `cache` `cache_disk` `cache_socache` `cern_meta` `cgi` `cgid` `charset_lite` `data` `dav` `dav_fs` +`dav_lock` `dbd` `deflate` `dialup` `dir` `dump_io` `echo` `env` `ext_filter` `expires` `file_cache` `filter` +`headers` `heartbeat` `heartmonitor` `ident` `imagemap` `include` `info` `lbmethod_bybusyness` +`lbmethod_byrequests` `lbmethod_bytraffic` `lbmethod_heartbeat` `ldap` `log_debug` `log_forensic` `lua` `macro` `md` +`mime` `mime_magic` `mpm_event` `mpm_prefork` `mpm_worker` `negotiation` `php8.x` `proxy` `proxy_ajp` +`proxy_balancer` `proxy_connect` `proxy_express` `proxy_fcgi` `proxy_fdpass` `proxy_ftp` `proxy_hcheck` `proxy_html` +`proxy_http` `proxy_scgi` `proxy_uwsgi` `proxy_wstunnel` `ratelimit` `reflector` `remoteip` `reqtimeout` `request` +`rewrite` `sed` `session` `session_cookie` `session_crypto` `session_dbd` `setenvif` `slotmem_plain` `slotmem_shm` +`socache_dbm` `socache_memcache` `socache_redis` `socache_shmcb` `speling` `ssl` `status` `substitute` `suexec` +`unique_id` `userdir` `usertrack` `vhost_alias` `xml2enc` + +This list can be outdated, you can verify it with: `docker run --rm thecodingmachine/php:8.4-v5-slim-apache a2enmod` + +### Apache user + +Apache runs as the `docker` user, which has the rights of the owner of your files (see +[Permissions](advanced.md#permissions)). In production, run it as `www-data` instead: + +```Dockerfile +FROM thecodingmachine/php:8.4-v5-apache + +ENV APACHE_RUN_USER=www-data \ + APACHE_RUN_GROUP=www-data +``` + +## fpm + +PHP-FPM listens on port 9000 and speaks FastCGI, not HTTP: put a web server (nginx, Caddy, Apache...) in front of it, +in another container, and point it to `fpm-container:9000`. + +```bash +$ docker run -p 9000:9000 --rm -v "$PWD":/var/www/html thecodingmachine/php:8.4-v5-fpm +``` + +A `docker stop` stops PHP-FPM gracefully: the running requests are completed. 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/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/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/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" +} 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.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 198ddd9..97021da 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 @@ -222,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 @@ -345,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 @@ -421,6 +430,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 diff --git a/utils/README.blueprint.md b/utils/README.blueprint.md index 67c628d..11b2949 100644 --- a/utils/README.blueprint.md +++ b/utils/README.blueprint.md @@ -9,11 +9,13 @@ This repository contains a set of developer-friendly, general purpose PHP images - 2 types available: `slim` (no extensions preloaded) or `fat` (most common PHP extensions are built-in) - 3 variants available: `CLI`, `apache` and `fpm` - 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 images come with [Composer](https://getcomposer.org/) 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 +See the [documentation](#documentation) for all the features. + {{ $image := .Orbit.Images }} ## Images @@ -68,7 +70,7 @@ Images of non-maintained versions are still available but no longer receive fixe ## Usage -These images are based on the [official PHP image](https://hub.docker.com/_/php/). +The images are based on Ubuntu 24.04, with PHP from the [ondrej/php](https://launchpad.net/~ondrej/+archive/ubuntu/php) repository. Example with CLI: @@ -100,549 +102,21 @@ RUN npm install RUN npm run build ``` -## Extensions available - -This image comes with 2 "types": the *slim* and the **fat** image. - -These extensions are enabled by default in slim image: `calendar` `ctype` `curl` `date` `dom` `exif` `fileinfo` `filter` `ftp` `gettext` `iconv` `json` `mbstring` `opcache` `openssl` `pcntl` `pcre` `PDO` `Phar` `posix` `readline` `shmop` `Reflection` `session` `shmop` `SimpleXML` `sockets` `sodium` `SPL` `sysvmsg` `sysvsem` `sysvshm` `tokenizer` `xml` `xmlreader` `xmlwriter` `xsl` `zip` - -This list can be outdated, you can verify by executing : `docker run --rm -it thecodingmachine/php:{{ $image.php_version }}-v5-slim-cli php -m` - -The slim image provides a simple way to install the other extensions. You would typically use the "slim" image in a `Dockerfile` when building your own custom image. - -The fat image contains the most commonly used extensions. You would typically use it in a local or CI environment. - -### Fat image - -Below is a list of extensions available in this image: - -**Enabled by default (in addition to extensions enabled in Slim image):** `apcu`, `hash`, `iconv`, `igbinary`, `mysqli`, `mysqlnd`, `redis`, `soap`, `xsl`, `zlib` and all enabled in slim. - -**Available (can be enabled using environment variables):** `amqp` `ast` `bcmath` `blackfire` `bz2` `dba` `decimal` `ds` `enchant` `exif` `excimer` `ffi` `mailparse` `msgpack` `gd` `gettext` `gmp` `gnupg` `grpc` `igbinary` `imagick` `imap` `inotify` `intl` `ldap` `lz4` `mcrypt` `memcached` `mongodb` `odbc` `opentelemetry` `pcov` `pdo_dblib` `pdo_odbc` `pdo_pgsql` `pdo_sqlite` `pgsql` `pspell` `rdkafka` `shmop` `snmp` `sockets` `sqlite3` `swoole` `tidy` `uploadprogress` `uuid` `xdebug` `xmlrpc` `xsl` `yaml` `zstd` - -This list can be outdated, you can verify by executing : `docker run --rm -it thecodingmachine/php:{{ $image.php_version }}-v5-cli php -m` - -**Note**: - -- *ev* is not available in PHP 8.1+ -- *mcrypt* is deprecated and its usage is discouraged: it is provided for legacy applications only -- *imagick* supports SVG, WMF, DjVu and OpenEXR images in addition to the common formats. If you process - untrusted images, you can restrict the allowed formats in the ImageMagick policy (`/etc/ImageMagick-6/policy.xml`) -- 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 - -You can enable/disable extensions using the `PHP_EXTENSION_[extension_name]` environment variable. - -For instance: - -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:{{ $image.php_version }}-v5-apache-node{{ $image.node_version }} - environment: - # Enable the PostgreSQL extension - PHP_EXTENSION_PGSQL: 1 - # Disable the Mysqli extension (otherwise it is enabled by default) - PHP_EXTENSION_MYSQLI: 0 -``` - -As an alternative, you can use the `PHP_EXTENSIONS` global variable: - -```bash -PHP_EXTENSIONS=pgsql gettext imap -``` - -### Compiling extensions in the slim image - -If you are using the slim image, you can automatically compile the extensions using the `PHP_EXTENSIONS` ARG in your Dockerfile. - -```Dockerfile -ARG PHP_EXTENSIONS="apcu mysqli pdo_mysql redis soap" -FROM thecodingmachine/php:{{ $image.php_version }}-v5-slim-apache -# The build will automatically trigger the download and compilation -# of the extensions (thanks to a ONBUILD hook in the slim image) -``` - -Beware : -* The `ARG PHP_EXTENSIONS` command must be written before the `FROM`. This is not a typo. -* `ARG PHP_EXTENSIONS=""` it's not the same as `ENV PHP_EXTENSIONS=""` -* You can't use `ARG PHP_EXTENSION_MYEXT=""` like the fat image. -* **Heads up**: if you are using multistage builds, the "ARG" variable must be put at the very top of the file (before the -first FROM): - -```Dockerfile -# The PHP_EXTENSIONS ARG will apply to the "slim" image -ARG PHP_EXTENSIONS="apcu mysqli pdo_mysql soap" - -FROM thecodingmachine/php:{{ $image.php_version }}-v5-apache-node{{ $image.node_version }} AS builder - -COPY --chown=docker:docker sources/web . -RUN composer install &&\ - yarn install &&\ - yarn build - -# The slim image will automatically build the extensions from the list provided at the very top of the file. -FROM thecodingmachine/php:{{ $image.php_version }}-v5-slim-apache - -ENV APP_ENV=prod \ - APACHE_DOCUMENT_ROOT=public/ - -COPY --from=builder /var/www/html . -``` - -In the sample above, we use the fat image to perform a "yarn build", but copy the result in a slim image that does -not contain Node, and contains only required extensions. - -## Setting parameters in php.ini - -By default, the base `php.ini` file used is the [*development* php.ini](https://github.com/php/php-src/blob/PHP-{{ $image.php_version }}/php.ini-development) file that comes with PHP. - -You can use the production `php.ini` file using the `TEMPLATE_PHP_INI` environment variable: - -```bash -# Use the production php.ini file as a base -TEMPLATE_PHP_INI=production -``` - -You can override parameters in `php.ini` using the PHP_INI_XXX environment variables: - -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:{{ $image.php_version }}-v5-apache-node{{ $image.node_version }} - environment: - # set the parameter memory_limit=1g - PHP_INI_MEMORY_LIMIT: 1g - # set the parameter error_reporting=EALL - PHP_INI_ERROR_REPORTING: E_ALL -``` - -Absolutely all `php.ini` parameters can be set. - -Internally, the image will map all environment variables starting with `PHP_INI_`. - -If your `php.ini` parameter contains a dot ("."), you can replace it with a double underscore ("__"). - -For instance: - -```bash -# Will set the parameter xdebug.remote_autostart=1 -PHP_INI_XDEBUG__REMOTE_AUTOSTART=1 -``` - -## Default working directory - -The working directory (the directory in which you should mount/copy your application) depends on the image variant -you are using: - -| Variant | Working directory | -|---------|-------------------| -| cli | `/usr/src/app` | -| apache | `/var/www/html` | -| fpm | `/var/www/html` | - - -## Changing Apache document root - -For the *apache* variant, you can change the document root of Apache (i.e. your "public" directory) by using the -`APACHE_DOCUMENT_ROOT` variable: - -```bash -# The root of your website is in the "public" directory: -APACHE_DOCUMENT_ROOT=public/ -``` - -If the APACHE_DOCUMENT_ROOT starts with a "/", it will be considered an absolute path. -If the APACHE_DOCUMENT_ROOT does not starts with a "/", it will be a path relative to "/var/www/html". - -```bash -# These 2 variables are identical -APACHE_DOCUMENT_ROOT=public/ -APACHE_DOCUMENT_ROOT=/var/www/html/public -``` - -## Enabling/disabling Apache extensions - -You can enable/disable Apache extensions using the `APACHE_EXTENSION_[extension_name]` environment variable. - -For instance: - -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:{{ $image.php_version }}-v5-apache-node{{ $image.node_version }} - environment: - # Enable the DAV extension for Apache - APACHE_EXTENSION_DAV: 1 - # Enable the SSL extension for Apache - APACHE_EXTENSION_SSL: 1 -``` - -As an alternative, you can use the `APACHE_EXTENSIONS` global variable: - -```bash -APACHE_EXTENSIONS="dav ssl" -``` - -**Apache modules enabled by default:** `access_compat` `alias` `auth_basic` `authn_core` `authn_file` `authz_core` `authz_host` `authz_user` `autoindex` `deflate` `dir` `env` `expires` `filter` `mime` `mpm_prefork` `negotiation` `php{{ $image.php_version }} (depend of your active version)` `reqtimeout` `rewrite` `setenvif` `status` - -**Apache modules available:** `access_compat` `actions` `alias` `allowmethods` `asis` `auth_basic` `auth_digest` `auth_form` `authn_anon` `authn_core` `authn_dbd` `authn_dbm` `authn_file` `authn_socache` `authnz_fcgi` `authnz_ldap` `authz_core` `authz_dbd` `authz_dbm` `authz_groupfile` `authz_host` `authz_owner` `authz_user` `autoindex` `brotli` `buffer` `cache` `cache_disk` `cache_socache` `cern_meta` `cgi` `cgid` `charset_lite` `data` `dav` `dav_fs` `dav_lock` `dbd` `deflate` `dialup` `dir` `dump_io` `echo` `env` `ext_filter` `expires` `file_cache` `filter` `headers` `heartbeat` `heartmonitor` `http2` `ident` `imagemap` `include` `info` `lbmethod_bybusyness` `lbmethod_byrequests` `lbmethod_bytraffic` `lbmethod_heartbeat` `ldap` `log_debug` `log_forensic` `lua` `macro` `md` `mime` `mime_magic` `mpm_event` `mpm_prefork` `mpm_worker` `negotiation` `php{{ $image.php_version }} (depend of your active version)` `proxy` `proxy_ajp` `proxy_balancer` `proxy_connect` `proxy_express` `proxy_fcgi` `proxy_fdpass` `proxy_ftp` `proxy_hcheck` `proxy_html` `proxy_http` `proxy_http2` `proxy_scgi` `proxy_wstunnel` `ratelimit` `reflector` `remoteip` `reqtimeout` `request` `rewrite` `sed` `session` `session_cookie` `session_crypto` `session_dbd` `setenvif` `slotmem_plain` `slotmem_shm` `socache_dbm` `socache_memcache` `socache_redis` `socache_shmcb` `speling` `ssl` `status` `substitute` `suexec` `unique_id` `userdir` `usertrack` `vhost_alias` `xml2enc` - -This list can be outdated, you can verify by executing : `docker run --rm -it thecodingmachine/php:{{ $image.php_version }}-v5-slim-apache a2enmod` - - -## Debugging - -To enable XDebug` `you simply have to set the environment variable: - -```bash -PHP_EXTENSION_XDEBUG=1 -``` - -If you enable XDebug, the image will do its best to configure the `xdebug.client_host` to point back to your Docker host. - -Behind the scenes, the image will: - -- set the parameter `xdebug.mode=debug` -- if you are using a Linux machine, the `xdebug.client_host` IP will point to your Docker gateway -- if you are using a Windows or MaxOS machine, the `xdebug.client_host` IP will point to [`host.docker.internal`](https://docs.docker.com/docker-for-mac/networking/#use-cases-and-workarounds) or [`docker.for.mac.localhost`](https://docs.docker.com/docker-for-mac/networking/#use-cases-and-workarounds) - -If you want to debug directly inside your container (for example if you're using [VSCode devcontainers](https://code.visualstudio.com/docs/remote/containers)) you can overwrite the [`xdebug.client_host`](https://xdebug.org/docs/all_settings#client_host) value by setting the following environment variable: - -```bash -XDEBUG_CLIENT_HOST=127.0.0.1 -``` - -In that case the manually set value takes precedence over the mentioned ones above. - -## NodeJS - -The *fat* images come with a Node variant. You can use Node :{{range $nodeV := $nodeVersions}} {{$nodeV}}{{end}}. If you need a previous variant, [use thecodingmachine/php v4 images](https://github.com/thecodingmachine/docker-images-php/tree/v4), [php v3 images](https://github.com/thecodingmachine/docker-images-php/tree/v3) or [v1](https://github.com/thecodingmachine/docker-images-php/tree/7.2-v1). - -If you use the *slim* images, you can install a NodeJS version with a simple ARG during the build: - -```Dockerfile -ARG NODE_VERSION=24 -FROM thecodingmachine/php:{{ $image.php_version }}-v5-slim-apache -# The build will automatically trigger the download of Node 24 -# (thanks to a ONBUILD hook in the slim image) -``` - -Beware! The `ARG NODE_VERSION` command must be written before the `FROM`. This is not a typo. - -`NODE_VERSION` can take any valid node versions (tested only with the versions provided here but should work too for some previous versions) - -## Permissions - -Ever faced file permission issues with Docker? Good news, this is a thing of the past! - -If you are used to running Docker containers with the base PHP image, you probably noticed that when running commands -(like `composer install`) within the container, files are associated to the `root` user. This is because the base user -of the image is "root". - -When you mount your project directory into `/var/www/html`, it would be great if the default user used by Docker could -be your current host user. - -The problem with Docker is that the container and the host do not share the same list of users. For instance, you might -be logged in on your host computer as `superdev` (ID: 1000), and the container has no user whose ID is 1000. - -The *thecodingmachine/php* images solve this issue with a bit of black magic: - -The image contains a user named `docker`. On container startup, the startup script will look at the owner of the -working directory (`/var/www/html` for Apache/PHP-FPM, or `/usr/src/app` for CLI). The script will then assume that -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. - -The direct result is that, in development: - - - Your PHP application can edit any file - - Your container can edit any file - - You can still edit any file created by Apache or by the container in CLI - -### Using this image in production - -By changing the Apache user to be `docker:docker`, we are lowering the security. -This is OK for a development environment, but this should be avoided in production. -Indeed, in production, Apache should not be allowed to edit PHP files of your application. If for some reason, an -attacker manages to change PHP files using a security hole, he could then run any PHP script by editing the PHP files -of your application. - -In production, you want to change back the Apache user to www-data. - -This can be done easily: - -**Dockerfile** -```Dockerfile -FROM thecodingmachine/php:{{ $image.php_version }}-v5-apache - -# ... - -# Change back Apache user and group to www-data -ENV APACHE_RUN_USER=www-data \ - APACHE_RUN_GROUP=www-data -``` - - -## Setting up CRON jobs - -You can set up CRON jobs using environment variables too. - -To do this, you need to configure 3 variables: - -```bash -# configure the user that will run cron (defaults to root) -CRON_USER=root -# configure the schedule for the cron job (here: run every minute) -CRON_SCHEDULE=* * * * * -# last but not least, configure the command -CRON_COMMAND=vendor/bin/console do:stuff -``` - -By default, CRON output will be redirected to Docker output. - -If you have more than one job to run, you can suffix your environment variable with the same string. For instance: - -```bash -CRON_USER_1=root -CRON_SCHEDULE_1=* * * * * -CRON_COMMAND_1=vendor/bin/console do:stuff - -CRON_USER_2=www-data -CRON_SCHEDULE_2=0 3 * * * -CRON_COMMAND_2=vendor/bin/console other:stuff -``` - -Cron is **installed by default in the fat images**. If you are using the "*slim*" images, you need to install it by passing -a single argument before the "FROM" clause in your Dockerfile: - -```Dockerfile -ARG INSTALL_CRON=1 -FROM thecodingmachine/php:{{ $image.php_version }}-v5-slim-apache -# The build triggers automatically the installation of Cron -``` - -**Important**: The cron runner we use is "Supercronic" and not the orginial "cron" that has a number of issues -with containers. Even with Supercronic, the architecture of cron was never designed with Docker in mind -(Cron is way older than Docker). It will run correctly on your container. If at some point you want to scale and add -more containers, it will run on all your containers. At that point, if you only want to run a Cron task once for your -application (and not once per container), you might want to have a look at alternative solutions like -[Tasker](https://github.com/opsxcq/tasker) or use the native features of your orchestrator (if you use Kubernetes, -you have a native task runner available), or one of the many other alternatives. - -Please notice that by default, containers are running in the UTC timezone. So your CRONs will run at UTC time. -If you want to change a timezone in a container, you can use the `TZ` environment variable. - -```bash -# Run this cron at 1 am, Paris time -TZ=Europe/Paris -CRON_SCHEDULE_1=0 1 * * * -CRON_COMMAND_1=vendor/bin/console do:stuff -``` - -### Supercronic options -To specify Supercronic options you can set the `SUPERCRONIC_OPTIONS` environment variable. - -This can be used to enable duplicate jobs. Per default, Supercronic will wait for a given job to finish before that job is scheduled again.\ -With the option `-overlapping` Supercronic will run duplicate instances of the jobs instead of waiting for them. - -```bash -SUPERCRONIC_OPTIONS=-overlapping - -# Or multiple options -SUPERCRONIC_OPTIONS=-overlapping -debug -``` - -For more options see see the [Supercronic Documentation](https://github.com/aptible/supercronic/blob/master/README.md). - -## Launching commands on container startup - -You can launch commands on container startup using the `STARTUP_COMMAND_XXX` environment variables. -This can be very helpful to install dependencies or apply database patches for instance: - -```bash -STARTUP_COMMAND_1=composer install -STARTUP_COMMAND_2=vendor/bin/doctrine orm:schema-tool:update -``` - -As an alternative, the images will look into the container for an executable file named `/etc/container/startup.sh`. - -If such a file is mounted in the image, it will be executed on container startup. - -```bash -docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp \ - -v $PWD/my-startup-script.sh:/etc/container/startup.sh thecodingmachine/php:{{ $image.php_version }}-v5-cli php your-script.php -``` +## Documentation -## Using the CLI variant +Most features are configured with environment variables: -The CLI images (thecodingmachine/php:{{ $image.php_version }}-v5-cli) expect a command to be passed in parameter. -You should override the Docker "command". +| Topic | Content | Main variables | +|----------------------------------------------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------| +| [PHP extensions](docs/extensions.md) | Extensions of the slim and fat images, enabling them, compiling them in a slim image | `PHP_EXTENSION_*`, `PHP_EXTENSIONS` | +| [PHP configuration](docs/php-configuration.md) | Base `php.ini` file and `php.ini` parameters | `TEMPLATE_PHP_INI`, `PHP_INI_*` | +| [Variants](docs/variants.md) | *cli*, *apache* (document root, ports, modules, user) and *fpm* | `APACHE_DOCUMENT_ROOT`, `APACHE_HTTP_PORT`, `APACHE_EXTENSION_*` | +| [Processes](docs/processes.md) | Startup commands, cron jobs and daemons run along with the main command | `STARTUP_COMMAND_*`, `CRON_*`, `DAEMON_*` | +| [Debugging and profiling](docs/debugging.md) | Xdebug and Blackfire | `PHP_EXTENSION_XDEBUG`, `XDEBUG_CLIENT_HOST`, `BLACKFIRE_AGENT` | +| [Advanced features](docs/advanced.md) | Permissions, NodeJS, Composer version, locales, SSH keys, Kubernetes | `DOCKER_USER`, `COMPOSER_VERSION`, `LOCALES` | -Important! You should not override the Docker "entrypoint". - -**Usage in a Dockerfile:** - -```Dockerfile -FROM thecodingmachine/php:{{ $image.php_version }}-v5-cli - -CMD ["php", "myprogram.php", "some_param"] -``` - -**Usage with Docker compose:** - -**docker-compose.yml** -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:{{ $image.php_version }}-v5-cli - command: php myprogram.php some_param -``` - -## Registering SSH private keys - -If your PHP project as a dependency on [a package stored in a private GIT repository](https://getcomposer.org/doc/05-repositories.md#using-private-repositories), -your `composer install` commands will not work unless you register your private key in the container. - -You have several options to do this. - -### Option 1: mount your keys in the container directly - -This option is the easiest way to go if you are using the image on a development environment. - -**docker-compose.yml** -```yml -version: '3' -services: - my_app: - image: thecodingmachine/php:{{ $image.php_version }}-v5-apache-node{{ $image.node_version }} - volumes: - - ~/.ssh:/home/docker/.ssh -``` - -### Option 2: store the keys from environment variables or build arguments - -Look at this option if you are building a Dockerfile from this image. - -The first thing to do is to get the signature of the server you want to connect to. - -```bash -$ ssh-keyscan myserver.com -``` - -Copy the output and put it in an environment variable. We assume the content is stored in `$SSH_KNOWN_HOSTS`. - -Now, let's write a Dockerfile. - -**Dockerfile** -```yml -FROM thecodingmachine/php:{{ $image.php_version }}-v5-apache - -ARG SSH_PRIVATE_KEY -ARG SSH_KNOWN_HOSTS - -# Let's register the private key -RUN ssh-add <(echo "$SSH_PRIVATE_KEY") -# Let's add the server to the list of known hosts. -RUN echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts -``` - -Finally, when triggering the build, you must pass the 2 variables as [build arguments](https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables-build-arg): - -```bash -$ docker build -t my_image --build-arg SSH_PRIVATE_KEY="$SSH_PRIVATE_KEY" --build-arg SSH_KNOWN_HOSTS="$SSH_KNOWN_HOSTS" . -``` - -## Usage in Kubernetes - -If you plan to use this image in Kubernetes, please be aware that the image internally uses `sudo`. This is because the -default user (`docker`) needs to be able to edit php config files as `root`. - -Kubernetes has a security setting (`allowPrivilegeEscalation`) that can disallow the use of `sudo`. The use of this flag -breaks the image and in the logs, you will find the message: - -``` -sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges? -``` - -Please be sure that this option is never set to false: - -```yml -apiVersion: v1 -kind: Pod -# ... -spec: - containers: - - name: foobar - image: thecodingmachine/php:{{ $image.php_version }}-v5-apache - securityContext: - allowPrivilegeEscalation: true # never use "false" here. -``` - -## Profiling with Blackfire - -This image comes with the [Blackfire]() PHP probe. You can install it using: - -```bash -PHP_EXTENSION_BLACKFIRE=1 -``` - -By default, the image expects that the blackfire agent is started in another container. - -Your `docker-compose.yml` file will typically look like this: - -**docker-compose.yml** -```yml -version: '3.3' -services: - php: - image: thecodingmachine/php:{{ $image.php_version }}-v5-apache - ports: - - "80:80" - environment: - PHP_EXTENSION_BLACKFIRE: 1 - blackfire: - image: blackfire/blackfire - environment: - # Exposes the host BLACKFIRE_SERVER_ID and TOKEN environment variables. - - BLACKFIRE_SERVER_ID - - BLACKFIRE_SERVER_TOKEN - # You can also use global environment credentials : - # BLACKFIRE_SERVER_ID: SERVER-ID - # BLACKFIRE_SERVER_TOKEN: SERVER-TOKEN -``` - -See [Blackfire Docker documentation](https://blackfire.io/docs/integrations/docker#running-the-agent) for more information. - -The image assumes that the Blackfire agent is accessible via the `blackfire` URL (like in the exemple above). -If for some reason, the container name is not "blackfire", you can customize the agent URL with the `BLACKFIRE_AGENT` environment variable: - -**docker-compose.yml** -```yml -version: '3.3' -services: - php: - image: thecodingmachine/php:{{ $image.php_version }}-v5-apache - environment: - PHP_EXTENSION_BLACKFIRE: 1 - BLACKFIRE_AGENT: myblackfire - # ... - myblackfire: - image: blackfire/blackfire - environment: - # ... -``` +The slim images install some tools during the build of your image with an ARG before the `FROM`: `PHP_EXTENSIONS`, +`INSTALL_CRON`, `INSTALL_LOCALES`, `NODE_VERSION` and `COMPOSER_VERSION`. ## Migrating from older image versions @@ -650,54 +124,8 @@ Check the [migration notes](MIGRATING.md). ## Contributing -There is one branch per minor PHP version and version of the image. - -Please submit your pull requests to the lowest branch where is applies. - -The Dockerfiles and the README are generated from a template using [Orbit](https://github.com/gulien/orbit). - -If you want to modify a Dockerfile or the README, you should instead edit the `utils/Dockerfile.blueprint` -or `utils/README.blueprint.md` and then run the command: - -```bash -$ orbit run generate -``` - -This command will generate all the files from the "blueprint" templates. - -You can then test your changes using the `build-and-test.sh` command: - -```bash -PHP_VERSION={{ $image.php_version }} BRANCH=v5 VARIANT=apache ./build-and-test.sh -``` - -### Additional environment in build-and-test.sh - -- BUILDER: either build or buildx depending on your configuration. -Defaults to build -- BLACKFIRE_VERSION: defaults to 2. You can install v1 but will include some CVEs in the build. -- PLATFORM: Docker will default to your architecture for building images. However, if you have QEMU set up in your machine, you can try building for another architecture like linux/arm64 - -Only one platform at a time is supported during the build and test script execution. - -*APPLE SILICON CONSIDERATIONS* - -Filesystem management works differently in Apple's macOS, so, if you're trying to build a linux/arm64 image (that is best suited for using in a M1/M2 Mac than a linux/amd64 one), there's a high likelihood that the filesystem user and permissions with busybox are going to fail. - -Although the test fails, when using the built image, everything works as expected. - -Run a virtual machine with linux/arm64 with Docker installed in it and, then, build and test the image. You'll take advantage of the ARM speed and will also be able to run the unit tests properly. - - -### Adding additional images - -To add a new version (php, node, apache, ...), please edit the following files : - -- utils/README.blueprint.md - - Add your image in this section: Images -- orbit.yml: Your image in generation task -- .travis.yml: To check the new image -- build-and-test.sh: Add your image in test +The Dockerfiles and this README are generated from the templates of the `utils` directory: see the +[contributing guide](docs/contributing.md) to edit, build and test the images. ## Special thanks diff --git a/utils/docker-entrypoint-as-root.sh b/utils/docker-entrypoint-as-root.sh index 539ea9f..c8bb392 100755 --- a/utils/docker-entrypoint-as-root.sh +++ b/utils/docker-entrypoint-as-root.sh @@ -146,16 +146,23 @@ 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 + +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 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 @@ -174,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/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" 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"