Produktions-PHP-Docker-Images bauen — Custom-Module & Variationen (Teil 2/3)
In Teil 1 haben wir das Base-Image gebaut — PHP-FPM mit OPcache-Tuning, strukturiertem Log-Routing, Socket-Konfiguration und allen Compile-Flags als Build-Argumente verfügbar gemacht. Jetzt wird draufgestapelt.
Nicht jede Anwendung braucht die gleichen PHP-Extensions. Ein Projekt braucht eine proprietäre Dokumentenverarbeitungsbibliothek zur Generierung von Geschäftsdokumenten. Ein anderes nur Xdebug für die Entwicklung. Ein drittes braucht SOAP, weil Legacy-APIs sich weigern zu sterben. Die Lösung: Variation-Images, die vom Base erben und genau das hinzufügen, was jedes Projekt braucht.
Die Layer-Architektur
┌─────────────────────────────────────────┐
│ Variation: Amber │
│ + customlib, SOAP, Custom-Ext │
│ + optional: Xdebug (Dev-Builds) │
├─────────────────────────────────────────┤
│ Variation: Dev │
│ + Xdebug │
│ (eigenständiges Dev-Image, keine │
│ Custom-Erweiterungen) │
├─────────────────────────────────────────┤
│ Base-Image │
│ + sockets, intl, bcmath │
│ + OPcache, FPM, Log-Routing │
│ + Debian-Pakete │
├─────────────────────────────────────────┤
│ serversideup/php:8.x-fpm │
└─────────────────────────────────────────┘
Jede Schicht fügt nur hinzu, was sie braucht. Ein Rebuild des Base erfordert kein Rebuild jeder Variation — Docker-Layer-Caching kümmert sich darum. Ein Rebuild einer Variation zieht nur das bereits gecachte Base.
Die Amber-Variation
Amber ist die Projekt-Variation für unsere Hauptanwendung. Sie fügt SOAP (für Legacy-Integrationen) und eine proprietäre Extension (für Dokumentenverarbeitung) hinzu. Hier ist das Dockerfile:
ARG PHP_VERSION='8.4'
ARG BASE_VERSION='1.0.0'
FROM registry.example.com/php:${PHP_VERSION}-${BASE_VERSION}
ARG DEPENDENCY_EXTRA_PHP_EXTENSIONS='soap'
ARG DEPENDENCY_DEVELOPMENT_EXTENSIONS
USER root
COPY src/variations/common/scripts/install-custom-ext /scripts/install-custom-ext
COPY src/variations/amber/custom-ext /scripts/custom-ext
RUN /scripts/install-custom-ext customlib
RUN install-php-extensions "${DEPENDENCY_EXTRA_PHP_EXTENSIONS}"
RUN if [ -z "$DEPENDENCY_DEVELOPMENT_EXTENSIONS" ] ; then \
echo Argument 'DEPENDENCY_DEVELOPMENT_EXTENSIONS' not provided; \
else \
echo Argument is $DEPENDENCY_DEVELOPMENT_EXTENSIONS; \
install-php-extensions "${DEPENDENCY_DEVELOPMENT_EXTENSIONS}"; \
fi
USER www-data
Die interessanten Teile:
BASE_VERSION als Build-Arg. Die Variation hardcodiert nicht, welches Base-Image sie erweitert. Man kann eine bestimmte Base-Version zum Testen pinnen und dann unabhängig bumpen. Das Build-Skript übernimmt dieses Mapping.
DEPENDENCY_DEVELOPMENT_EXTENSIONS ist optional. Wenn leer, überspringt der bedingte RUN die Installation komplett. Wenn auf xdebug gesetzt, bekommt man einen Dev-Build aus demselben Dockerfile. Ein Dockerfile, zwei Image-Varianten — Produktion und Entwicklung.
Custom-Extensions bekommen ihren eigenen Installer. install-php-extensions (vom serversideup-Base) handhabt Standard-PECL/Distro-Extensions. Aber manche Bibliotheken sind proprietär und liefern vorkompilierte Binaries. Die brauchen ein eigenes Installationsskript.
Der Custom-Extension-Installer
Manche Extensions können nicht über install-php-extensions oder pecl installiert werden. Es sind proprietäre Binaries, die heruntergeladen, architekturgenau zugeordnet und im richtigen Verzeichnis platziert werden müssen. Wir haben einen generischen Installer dafür gebaut:
#!/bin/sh
SCRIPT_DIR=$(dirname "$0")
if [ $# -eq 0 ]; then
echo "Extension names are not provided"
exit 1
fi
if [ ! -d "$SCRIPT_DIR/custom-ext" ]; then
echo "Custom extension directory is not found $SCRIPT_DIR"
exit 1
fi
for EXTENSION_NAME in "$@"
do
if [ ! -f "$SCRIPT_DIR/custom-ext/$EXTENSION_NAME" ]; then
echo "Script for extension $EXTENSION_NAME is not found"
exit 1
fi
echo "Running $SCRIPT_DIR/custom-ext/$EXTENSION_NAME"
if ! sh "$SCRIPT_DIR/custom-ext/$EXTENSION_NAME"; then
echo "Error running $SCRIPT_DIR/custom-ext/$EXTENSION_NAME"
exit 1
fi
done
Das Muster ist einfach: Jede Custom-Extension hat ein Skript, das nach ihr benannt ist, in einem custom-ext/-Verzeichnis. Der Installer iteriert über die Argumente, findet das passende Skript und führt es aus. Eine neue Custom-Extension hinzuzufügen bedeutet, eine einzige Skript-Datei hinzuzufügen — keine Dockerfile-Änderungen nötig.
Proprietäre Extensions: Multi-Architektur-Installation
Manche proprietäre Extensions werden als vorkompilierte .so-Dateien für bestimmte PHP-Versionen und CPU-Architekturen geliefert. Der Installer muss beides erkennen und die richtige Binärdatei herunterladen:
#!/bin/sh
set -e
set -x
PHP_VERSION=$(php -r "echo sprintf('%s%s0', PHP_MAJOR_VERSION, PHP_MINOR_VERSION);")
PHP_NTS=$(php -r "echo ((int)PHP_ZTS === 0) ? '-nts' : '';")
LIB_VERSION="10.0.3"
LIB_VPATH="1003"
ARCHITECTURE=$(uname -m)
# Manche Anbieter nutzen 'x64' statt 'x86_64'
if [ "$ARCHITECTURE" = "x86_64" ]; then
ARCHITECTURE="x64"
fi
# Cache für lokale Entwicklung — nicht bei jedem Build erneut herunterladen
if [ -f "$SCRIPT_DIR/customlib-$LIB_VERSION-Linux-$ARCHITECTURE-php.tar.gz" ]; then
cp "$SCRIPT_DIR/customlib-$LIB_VERSION-Linux-$ARCHITECTURE-php.tar.gz" customlib.tar.gz
else
LIB_URL="https://vendor.example.com/binaries/customlib/$LIB_VPATH/customlib-$LIB_VERSION-Linux-$ARCHITECTURE-php.tar.gz"
curl -L "$LIB_URL" -o customlib.tar.gz
fi
tar -xzf customlib.tar.gz
EXTENSION_DIR="$(php -d display_errors=stderr -r 'echo realpath(ini_get("extension_dir"));')"
FROM="customlib-$LIB_VERSION-Linux-$ARCHITECTURE-php/bind/php/php-$PHP_VERSION$PHP_NTS/php_customlib.so"
TO="$EXTENSION_DIR/customlib.so"
mv "$FROM" "$TO"
rm -rf customlib.tar.gz "customlib-$LIB_VERSION-Linux-$ARCHITECTURE-php"
docker-php-ext-enable customlib
Mehrere Details machen das Multi-Plattform-fähig:
uname -m zur Architektur-Erkennung. Wenn docker buildx für linux/arm64 baut, meldet der Build-Container aarch64. Beim Bau für AMD64 meldet er x86_64. Das Skript mappt x86_64 auf die x64-Benennung des Anbieters.
PHP-Versionsformatierung. Der Anbieter organisiert seine Binaries nach PHP-Version in einem bestimmten Format (8.40 für PHP 8.4). Der sprintf('%s%s0', ...)-Trick erzeugt genau dieses Format.
Thread-Safety-Erkennung. PHP_ZTS verrät, ob PHP mit Zend Thread Safety kompiliert wurde. Das -nts-Suffix wählt die Non-Thread-Safe-Binärdatei, die FPM verwendet.
Lokales Caching. Wenn das Tarball bereits im Build-Context existiert (von einem vorherigen Download oder manuellen Platzierung), wird der Download übersprungen. Das beschleunigt iterative Entwicklung, ohne CI-Builds zu beeinflussen.
Die Dev-Variation
Für die Entwicklung haben wir ein eigenständiges Dev-Image, das Xdebug auf das Base aufschichtet — keine projektspezifischen Extensions:
ARG PHP_VERSION='8.4'
ARG BASE_VERSION='1.0.0'
FROM registry.example.com/php:${PHP_VERSION}-${BASE_VERSION}
ARG DEPENDENCY_DEVELOPMENT_EXTENSIONS
USER root
RUN install-php-extensions "${DEPENDENCY_EXTRA_PHP_EXTENSIONS}"
RUN if [ -z "$DEPENDENCY_DEVELOPMENT_EXTENSIONS" ] ; then \
echo Argument 'DEPENDENCY_DEVELOPMENT_EXTENSIONS' not provided; \
else \
echo Argument is $DEPENDENCY_DEVELOPMENT_EXTENSIONS; \
install-php-extensions "${DEPENDENCY_DEVELOPMENT_EXTENSIONS}"; \
fi
USER www-data
Das gibt Teams ein sauberes Entwicklungsimage mit Xdebug, das exakt zum Produktions-Base passt — gleiche PHP-Version, gleiche OPcache-Einstellungen, gleiche FPM-Konfiguration. Der einzige Unterschied ist der Debugger.
Das Build-Skript: Flags für alles
Das Variation-Build-Skript ist, wo die Flexibilität zusammenkommt. Es unterstützt mehrere PHP-Versionen, Dev-Builds, CI-Hashes und Multi-Arch — alles über CLI-Flags:
bash build.sh
bash build.sh --base-version 1.0.0 --release-version 1.0.2
bash build.sh --base-version 1.0.0 --release-version 1.0.2 --php-versions "8.4,8.1" --with-dev
bash build.sh --base-version 1.0.0 --release-version 1.0.2 --php-versions "8.4,8.1" --hash abcdef123456
Die Flags:
| Flag | Standard | Zweck |
|---|---|---|
--base-version | 1.0.0 | Auf welchem Base-Image aufgebaut wird |
--release-version | Gleich wie Base | Tag für die Output-Images |
--php-versions | 8.4,8.1 | Komma-getrennte PHP-Versionen zum Bauen |
--with-dev | Deaktiviert | Auch Dev-Images mit Xdebug bauen |
--hash | Keiner | CI-Commit-Hash, angehängt an Release-Tag |
Das --hash-Flag ist besonders nützlich für CI. Bei Pull Requests werden Images mit dem Commit-SHA getaggt, damit Reviewer genau den Build testen können. Auf dem Main-Branch werden saubere Release-Tags verwendet.
So handhabt die Build-Funktion alle Kombinationen:
function build_php_version {
local php_version="$1"
local with_dev="$2"
# Immer das Produktions-Amber-Image bauen
build_image \
"${AMBER_DOCKERFILE}" \
"registry.example.com/php:amber-${php_version}-${RELEASE_TAG_VERSION}" \
"${php_version}" \
"false"
if [[ ${with_dev} != "true" ]]; then
return
fi
# Dev Amber: Produktions-Extensions + Xdebug
build_image \
"${AMBER_DOCKERFILE}" \
"registry.example.com/php:amber-${php_version}-${DEV_TAG_VERSION}" \
"${php_version}" \
"true"
# Dev Base: nur Xdebug, keine Projekt-Extensions
build_image \
"${DEV_DOCKERFILE}" \
"registry.example.com/php:${php_version}-${DEV_TAG_VERSION}" \
"${php_version}" \
"true"
}
Eine Funktion, drei Image-Varianten. Das --with-dev-Flag steuert, ob Dev-Images überhaupt gebaut werden, und hält CI-Pipelines schnell, wenn man nur Produktions-Images braucht.
Die Push-Skripte spiegeln die Build-Skripte
Jedes Build-Flag hat ein entsprechendes Push-Flag. Die Push-Skripte verwenden dieselbe Argument-Analyse und Versions-Logik:
bash push.sh --base-version 1.0.0 --release-version 1.0.2 --with-dev
bash push.sh --base-version 1.0.0 --release-version 1.0.2 --hash abcdef123456
Diese Symmetrie ist beabsichtigt. Was auch immer du gebaut hast, du pushst es mit den gleichen Flags. Kein mentales Mapping zwischen Build-Tags und Push-Tags.
Eine neue Variation hinzufügen
Eine neue Variation hinzufügen folgt einem vorhersehbaren Muster:
- Erstelle ein Verzeichnis unter
src/variations/<name>/ - Schreibe ein
Dockerfile, das das Base-Image erweitert - Wenn du Custom-Extensions brauchst, füge Skripte zu
custom-ext/hinzu - Füge die Variation zum Build-Skript hinzu
Der Custom-Extension-Installer ist wiederverwendbar — jede Variation kann ihn nutzen, indem sie das gemeinsame install-custom-ext-Skript kopiert und eigene Extension-Skripte bereitstellt.
Code-Formatierung
Selbst Shell-Skripte und YAML-Dateien verdienen Formatierung. Wir erzwingen Konsistenz mit shfmt und yamlfmt:
shfmt -l -w -s ./*.sh
yamlfmt -dstar **/*.{yaml,yml} -formatter indent=2,eof_newline=true
Das läuft in CI neben den Builds. Wenn dein Shell-Skript shfmt nicht besteht, scheitert die Pipeline, bevor sie auch nur versucht, ein Image zu bauen.
Was als Nächstes kommt
Mit den Base- und Variation-Images haben wir eine vollständige Lokal-Build-Geschichte. Aber niemand will sich in einen Server einloggen und bash build.sh manuell ausführen. In Teil 3 automatisieren wir alles mit GitHub Actions: Multi-Arch-Builds auf Self-Hosted-Runnern, PR-Preview-Images, Slack-Benachrichtigungen und ein workflow_dispatch-Release-Flow für Base-Images.
Weiter voran und genieße jeden Schritt deiner Coding-Reise.
