Configuration files

Dockerfile: the images

One Dockerfile, two images: PHP-FPM with TYPO3 and nginx with the same files. Every instruction explained.

Overview: multi-stage build

The Dockerfile contains two stages. Each stage starts with its own FROM and produces its own image:

  • app: PHP-FPM with all system packages, PHP extensions and the TYPO3 installation set up by Composer. The services php and scheduler run from it.
  • web: nginx with a copy of the files from app. The service web runs from it.

docker-compose.yml selects the stage to build with build.target. Because web copies its files from app, both images are guaranteed to be in sync. nginx can serve static files such as CSS, JavaScript and images directly, without involving PHP.

Syntax and build arguments

# syntax=docker/dockerfile:1
ARG PHP_VERSION=8.4
ARG NGINX_VERSION=1.30

The first line is a parser directive: BuildKit uses the current version 1 Dockerfile syntax. The two ARG lines come before the first FROM and may therefore be used in the FROM lines. They define which PHP and nginx versions serve as the base.

To update, change the values here and rebuild. Alternatively, set them in docker-compose.yml under build.args without touching the Dockerfile.

Base image and environment

FROM php:${PHP_VERSION}-fpm AS app

ENV TYPO3_CONTEXT=Production \
    COMPOSER_ALLOW_SUPERUSER=1 \
    COMPOSER_NO_INTERACTION=1

The base is the official PHP image in its FPM variant on Debian. AS app names the stage.

  • TYPO3_CONTEXT=Production is the default context. docker-compose.yml overrides it with the value from .env, for example Development for detailed error messages.
  • COMPOSER_ALLOW_SUPERUSER=1 suppresses the warning that Composer runs as root during the build.
  • COMPOSER_NO_INTERACTION=1 prevents prompts that would block a build.

System packages

RUN apt-get update \
 && apt-get install -y --no-install-recommends \
        graphicsmagick \
        ghostscript \
        git \
        locales \
        unzip \
 && rm -rf /var/lib/apt/lists/*
PackagePurpose
graphicsmagickimage processing: scaling, cropping, thumbnails. additional.php configures TYPO3 to use it.
ghostscriptthumbnails of PDF files
git, unzipComposer downloads packages as Git clones or ZIP archives
localessystem locales for date and number formats (next section)

Everything sits in a single RUN instruction. This creates only one image layer, and the package lists in /var/lib/apt/lists are deleted again in the same layer. --no-install-recommends skips recommended but unneeded packages. Both keep the image small.

System locales

RUN sed -i -E 's/^# *(en_US\.UTF-8|de_DE\.UTF-8) UTF-8/\1 UTF-8/' /etc/locale.gen \
 && locale-gen

The site languages use the locales de_DE.UTF-8 and en_US.UTF-8. TYPO3 sets them via setlocale(), for example for month names and number formats. The Debian image does not generate them. The sed command removes the comment character in front of both entries in /etc/locale.gen, and locale-gen generates them. Without this step, TYPO3 logs “Locale … not found”.

For another language, add it to the regular expression, for example fr_FR\.UTF-8.

Additional CA certificates

COPY config/certs/ /usr/local/share/ca-certificates/
RUN update-ca-certificates

TYPO3 talks to Keycloak server-side: token endpoint, userinfo and signing keys. The certificate of id.dev.sonia.de is issued by the Ostfalia RZ Root CA, which Debian does not know. update-ca-certificates adds all *.crt files from the folder to the system certificate store. PHP (cURL, OpenSSL) trusts them afterwards.

To add more internal CAs, place them as .crt files in config/certs/ and rebuild.

PHP extensions

COPY --from=mlocati/php-extension-installer:2 /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions \
        apcu \
        exif \
        gd \
        intl \
        mysqli \
        opcache \
        pdo_mysql \
        zip

COPY --from takes only the helper install-php-extensions from another image. It installs the required libraries, compiles the extensions and removes the build tools afterwards.

ExtensionPurpose
apcufast in-memory cache
exifmetadata from photos, such as orientation and capture date
gdimage functions in PHP, complements GraphicsMagick
intlinternationalisation, required by TYPO3
mysqli, pdo_mysqldatabase access to MariaDB. The appliance uses the mysqli driver.
opcachekeeps compiled PHP code in memory, much faster
zipZIP archives, for example language packs and extension uploads

The PHP image already ships standard extensions such as mbstring, xml and curl.

Composer and PHP configuration

COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer

COPY config/php/typo3.ini /usr/local/etc/php/conf.d/zz-typo3.ini
COPY config/php/fpm-pool.conf /usr/local/etc/php-fpm.d/zz-typo3.conf

Composer also arrives via COPY --from, from the official Composer image, version 2.

  • typo3.ini sets the PHP values according to the TYPO3 system requirements: memory limit, execution time, upload size, time zone, OPcache and APCu.
  • fpm-pool.conf adjusts the FPM pool. The key setting is clear_env = no: otherwise PHP would not see the environment variables from Compose, and additional.php would have no configuration. It also sets the number of processes and the ping endpoint for the health check.

The prefix zz- makes sure the files are loaded last and override the defaults.

Installing TYPO3 with Composer

WORKDIR /var/www/html

# Abhängigkeiten zuerst installieren (Layer-Caching)
COPY composer.json composer.lock* ./
COPY packages/ packages/
RUN composer install --no-dev --optimize-autoloader --no-progress \
 && composer clear-cache \
 && mkdir -p config/system config/sites var public/fileadmin public/typo3temp \
 && chown -R www-data:www-data /var/www/html

This is where the actual TYPO3 installation is created. Only composer.json, composer.lock and packages/ go into the image. Composer generates the rest, including vendor/ and the web root public/ with index.php and the public extension files under public/_assets.

  • composer.lock* with an asterisk: the file is optional. If it exists, Composer installs exactly the locked versions.
  • packages/ must be copied before composer install, because the custom extensions are included as local packages.
  • --no-dev leaves out development tools, --optimize-autoloader speeds up class loading, and clear-cache removes the download cache from the image.
  • mkdir creates the folders that the volumes are later mounted on. chown hands them to the user www-data, which PHP-FPM runs as.

Layer caching: Docker only rebuilds a layer when its inputs have changed. As long as composer.json, composer.lock and packages/ stay the same, Docker takes this lengthy step from the cache.

Start scripts, entrypoint and command

COPY config/typo3/additional.php /usr/local/share/typo3/additional.php
COPY config/typo3/entrypoint.sh /usr/local/bin/typo3-entrypoint
COPY config/typo3/healthcheck.sh /usr/local/bin/typo3-healthcheck
RUN chmod +x /usr/local/bin/typo3-entrypoint /usr/local/bin/typo3-healthcheck

EXPOSE 9000

ENTRYPOINT ["typo3-entrypoint"]
CMD ["php-fpm"]
  • additional.php reads database, host names, mail and OIDC from environment variables. It initially sits outside config/, because at runtime the typo3-config volume covers that folder and would hide the file. The entrypoint copies it to config/system/ on every start, so the version from the image always applies.
  • entrypoint.sh waits for the database. On the first start, it runs typo3 setup and sets up the site, languages and content. On every start, it updates the database schema, backend groups and language pack and flushes the caches.
  • healthcheck.sh reports “healthy” once the entrypoint has finished and PHP-FPM answers on port 9000.
  • EXPOSE 9000 documents the FPM port. It is not published, nginx reaches it through the internal Docker network.
  • ENTRYPOINT and CMD work together: Docker starts typo3-entrypoint php-fpm. The script ends with exec "$@", so PHP-FPM replaces the script process and receives Docker’s stop signals directly.

Stage web: nginx

FROM nginx:${NGINX_VERSION}-alpine AS web

COPY config/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=app /var/www/html /var/www/html

The second stage starts from a lean nginx image on Alpine Linux. It adds the TYPO3 server block and uses COPY --from=app to copy the complete /var/www/html directory from the first stage.

nginx needs these files for two reasons. First, it serves public/_assets and other static files itself. Second, the server block checks that the requested PHP file exists before forwarding a request to PHP. PHP itself is not part of this image.

Uploads (fileadmin) and generated files (typo3temp) only appear at runtime. They therefore do not come from the image but as volumes from docker-compose.yml.

What stays out of the image

.git
.env
.env.*
!.env.example
vendor/
public/
var/
*.md
docker-compose*.yml
backups/
BACKUP.sh
RESTORE.sh
CREATE_SHELL_ARCHIVE_BACKUP.sh

The .dockerignore file excludes files from the build context. Docker does not even send them to the build.

  • Security: .env with passwords and the client secret never ends up in an image. The values arrive at runtime via Compose.
  • Cleanliness: a local vendor/, public/ or var/ would distort the Composer run in the image.
  • Speed: backups, documentation and scripts are not needed in the image and only enlarge the build context.