Aller au contenu

Référence du Dockerfile multi-stage

Inventaire factuel du Dockerfile racine de Kirexo. Pour comprendre pourquoi l'image est bâtie sur Alpine et pourquoi ce découpage en stages, voir l'explication Pourquoi une image Alpine multi-stage. Pour l'outillage spécifique au devcontainer (pare-feu, cycle de vie, cibles Castor), voir la Référence du Dev Container.

Cette page décrit l'état réel du Dockerfile ; en cas de doute, le fichier fait foi.

Image de base

Un unique point de pin, réutilisé par tous les stages :

FROM dunglas/frankenphp:1-php8.5-alpine AS frankenphp_upstream
Élément Valeur
Image amont dunglas/frankenphp:1-php8.5-alpine
Distribution Alpine Linux (branche release courante de l'amont, v3.24)
libc musl
Serveur applicatif FrankenPHP (Caddy + worker PHP)

Le tag est figé par une instruction FROMpas un ARG. La détection d'un tag plus récent est faite par castor devcontainer:upgrade-tools via ToolsUpgrader::selectFrankenphpCandidate(), dont le parsing reconnaît le suffixe -alpine (cf. Surveillance de l'image base FrankenPHP).

Les trois stages

Le Dockerfile définit trois stages construits en images séparées, chaînés par héritage.

Stage Hérite de Rôle Utilisateur d'exécution
frankenphp_base frankenphp_upstream Extensions PHP, Composer, Castor. Socle commun. root
frankenphp_dev frankenphp_base Xdebug, Chromium, Node, outillage devcontainer et pare-feu. app (UID/GID hôte, défaut 1000:1000)
frankenphp_prod frankenphp_base composer install --no-dev, sources applicatives, durcissement CVE. app (1000:1000, shell /bin/sh)

frankenphp_dev et frankenphp_prod dérivent tous deux de frankenphp_base (et non l'un de l'autre) : le stage dev n'est jamais embarqué en production. Ces trois stages correspondent aux images kirexo-base, kirexo-dev, kirexo-prod publiées dans le registry (cf. Images publiées dans le registry).

Paquets système par stage

Tous les paquets sont installés via apk add --no-cache (un index apk qui n'est jamais écrit dans l'image). Chaque stage a son ou ses appels dédiés.

frankenphp_base

apk add --no-cache acl curl file gettext git jq unzip
Paquet Rôle
acl Gestion des ACL POSIX (héritée du template amont).
curl Client HTTP — requis par le HEALTHCHECK (curl -f http://localhost:2019/metrics) et le téléchargement du PHAR Castor. Paquet Alpine distinct de busybox.
file Détection de type MIME.
gettext envsubst (substitution de variables dans les templates de conf).
git Requis par Composer pour les dépendances de type source.
jq Parsing JSON — hooks Claude, statusline, plusieurs jobs CI. Présent dès kirexo-base, donc pas de apk add à la volée côté pipeline.
unzip Décompression des archives dist Composer.

frankenphp_dev

Le stage dev fait plusieurs apk add séparés (pour maximiser le cache de layer) :

apk add --no-cache \
    sudo iptables ip6tables ipset dnsmasq ca-certificates iproute2 \
    musl-utils coreutils procps findutils shadow libcap \
    shellcheck bash-completion bats ffmpeg xz
Paquet Rôle
sudo post-start.sh lance sudo init-firewall.sh (NOPASSWD ciblé, cf. Convention sudo).
iptables / ip6tables / ipset / dnsmasq / iproute2 Cœur du pare-feu sortant du devcontainer.
ca-certificates Validation TLS des smoke-tests curl du pare-feu.
musl-utils Fournit getent (absent de busybox/musl) — requis par init-firewall.sh et la création d'utilisateur.
coreutils date -Iseconds, mktemp, seq — les applets busybox ne garantissent pas ces options.
procps pgrep -x / pkill -x (busybox ne fournit pas -x).
findutils xargs -L 1 (busybox xargs n'a pas -L).
shadow useradd / groupadd (ce stage) et usermod / groupmod (overlay compose) — busybox n'a qu'adduser / addgroup.
libcap setcap cap_net_bind_service sur le binaire FrankenPHP.
shellcheck Linter des scripts shell (cible castor lint:shell).
bash-completion Framework requis par la complétion Castor / Symfony Console.
bats castor test:bash (bats-core 1.x, paquet Alpine bats).
ffmpeg Capture vidéo des démos E2E.
xz Extraction du tarball Node .tar.xz (busybox tar délègue xz à un binaire externe).

git, jq et curl viennent déjà de frankenphp_base — ils ne sont pas redemandés.

frankenphp_prod

Le stage prod n'ajoute aucun paquet applicatif ; il exécute un seul appel de durcissement et un paquet de build temporaire :

apk upgrade --no-cache

apk upgrade --no-cache tire les correctifs de sécurité Alpine sur tous les paquets hérités de frankenphp_base (curl, libxml2, expat, ncurses, openssl…). C'est le cœur du gain CVE sur l'image livrée. Il remplace l'ancien apt-get purge --autoremove linux-libc-dev du stage Debian : ce paquet n'existe pas sous Alpine (les headers kernel sont dans linux-headers, non installé par défaut), la purge est donc sans objet.

libcap est installé comme dépendance de build virtuelle (.setcap-deps) puis retiré après le setcap : la capability gravée dans les xattr du binaire survit à la suppression de l'outil, sans laisser libcap en surface d'exécution.

Extensions PHP

Installées via install-php-extensions (compatible Alpine — il résout les libs musl requises).

Stage Extensions Lib musl résolue automatiquement
frankenphp_base apcu fileinfo intl opcache zip pdo_pgsql redis amqp icu-libs (intl), libpq / postgresql-libs (pdo_pgsql), rabbitmq-c (amqp), libzip (zip)
frankenphp_dev xdebug

L'extension amqp (liée à rabbitmq-c sous Alpine) était le candidat le plus à risque de la migration ; elle est bien résolue par install-php-extensions.

Bloc recipes Symfony Flex

Un install-php-extensions pdo_pgsql supplémentaire figure dans le bloc ###> doctrine/doctrine-bundle ### du Dockerfile (recette Flex). Il est redondant avec l'appel groupé ci-dessus et sans effet (extension déjà installée).

Binaires installés hors gestionnaire de paquets

Trois binaires sont téléchargés directement (pas via apk), chacun figé par version et vérifié par SHA-256 avant installation.

Binaire Stage ARG version Source Vérification
Composer frankenphp_base composer:2.10 (tag d'image) COPY --from=composer:2.10 Image officielle Composer (pas de téléchargement réseau).
Castor frankenphp_base CASTOR_VERSION (v1.6.1) github.com/jolicode/castor (PHAR) CASTOR_SHA256 via sha256sum -c.
glab frankenphp_dev GLAB_VERSION (1.108.0) gitlab.com/gitlab-org/cli (Go statique) GLAB_SHA256 via sha256sum -c.
Node / npm / npx frankenphp_dev NODE_VERSION (24.18.0) unofficial-builds.nodejs.org — variante musl NODE_SHA256 via sha256sum -c.
Claude Code frankenphp_dev CLAUDE_CODE_VERSION (2.1.211) npm install -g @anthropic-ai/claude-code dist.integrity SRI validé nativement par npm.

Le tarball Node utilise la variante musl — le build glibc de nodejs.org/dist ne tourne pas sous Alpine :

https://unofficial-builds.nodejs.org/download/release/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64-musl.tar.xz

ToolsUpgrader::buildDockerfileUrls() (castor/src/DevContainer/ToolsUpgrader.php) reconstruit cette même URL musl pour le pré-check host:check. Le pin version + SHA-256 est bumpé par castor devcontainer:upgrade-tools.

Chromium épinglé sur une branche Alpine figée

Le stage frankenphp_dev installe Chromium et son driver depuis la branche release figée v3.24 d'Alpine, pas depuis le dépôt community « rolling » de la base :

ARG CHROMIUM_ALPINE_BRANCH=v3.24
ARG CHROMIUM_VERSION=150.0.7871.128-r0
RUN apk add --no-cache \
        --repository "https://dl-cdn.alpinelinux.org/alpine/${CHROMIUM_ALPINE_BRANCH}/community" \
        "chromium=${CHROMIUM_VERSION}" \
        "chromium-chromedriver=${CHROMIUM_VERSION}"
Élément Valeur
Version Chromium 150.0.7871.128-r0 (Alpine v3.24)
Branche du dépôt v3.24 (release figée, mêmes libs que la base)
Binaires /usr/bin/chromium et /usr/bin/chromedriver
Paquets chromium + chromium-chromedriver (même version source → toujours alignés)

Un smoke-test fail-fast suit immédiatement : il fait échouer docker build si le navigateur ne démarre pas.

chromium --headless=new --no-sandbox --disable-dev-shm-usage --dump-dom about:blank > /dev/null
chromedriver --version

Les flags sont alignés sur PANTHER_CHROME_ARGUMENTS de .env.test. Le rationale du pin sur une branche figée (plutôt qu'un pin nu qui casse au refresh) est dans l'explication.

Renommages de paquets Debian vers Alpine

La migration a changé le nom de plusieurs paquets et outils. Ces correspondances sont utiles pour relire l'historique ou un ancien Dockerfile Debian.

Rôle Nom Debian Nom Alpine
Driver Chromium (Panther/CI) chromium-driver chromium-chromedriver
Lib AMQP (extension amqp) librabbitmq rabbitmq-c
Lib PostgreSQL (extension pdo_pgsql) libpq postgresql-libs
getent (résolution hosts) fourni par libc-bin (glibc) fourni par musl-utils
Création d'utilisateur (dev) useradd / groupadd (paquet passwd) useradd / groupadd (paquet shadow)
Création d'utilisateur (prod) useradd / groupadd adduser / addgroup (busybox)
Headers kernel linux-libc-dev linux-headers (non installé)

Note musl : bash, shell et création d'utilisateur

Alpine ne préinstalle pas bash — /bin/sh est busybox ash. Le Dockerfile en tient compte :

  • apk add bash avant SHELL : le stage frankenphp_dev bascule sur SHELL ["/bin/bash", "-o", "pipefail", "-c"] (nécessaire pour pipefail sur les pipelines curl … | sha256sum -c). Comme la directive SHELL échouerait sur /bin/bash: not found, bash est installé juste avant, avec le shell par défaut (ash). bash est aussi requis par init-firewall.sh (arrays, mapfile, [[ =~ ]]).
  • Complétion Castor dans /etc/profile.d/ : Alpine ne garantit pas de /etc/bash.bashrc. La complétion est écrite dans /etc/profile.d/castor-completion.sh, sourcé par tout shell de login. Cf. Autocomplétion Castor.
  • useradd / groupadd en dev, adduser / addgroup en prod : le stage dev installe shadow (pour aligner l'UID/GID hôte via useradd/groupadd et getent) ; le stage prod, plus minimal, se contente des applets busybox adduser/addgroup avec un UID/GID fixe 1000:1000 et un shell /bin/sh (bash n'est pas installé en prod).