Faire tourner WordPress sur du FROM scratch demande de répondre à une question simple : sans shell, sans cp, sans gestionnaire de paquets, qui met les fichiers de WordPress à leur place ? La réponse tient dans un chart Helm de quatre conteneurs. Voici ce que contient l'image, et comment elle se déploie.
Ce que l'image contient
Trois exécutables : tini en PID 1, le binaire Go d'entrée, et php-fpm. Rien d'autre — pas de shell, pas de php en ligne de commande, pas de gestionnaire de paquets. L'utilisateur est 1999, non-root, et l'élévation de privilèges est refusée au niveau du pod.
Côté extensions, WordPress a besoin de gd, imagick, mysqli, intl, zip, redis, exif, sodium, opcache et quelques autres. Douze d'entre elles sont livrées en objets partagés — bcmath, bz2, curl, exif, gd, gmp, imagick, intl, mysqli, redis, sodium, zip — les autres étant compilées dans le binaire, donc invisibles en tant que fichiers.
Elles ne voyagent pas seules : php-fpm porte 49 bibliothèques qu'aucune autre image du parc ne porte, parce qu'intl traîne ICU derrière lui et imagick tout ImageMagick. Résultat, 74 fichiers ELF pour trois exécutables. C'est la forme normale d'un PHP — le moteur est un socle, les extensions sont le produit — et c'est là que part le poids.
Le healthcheck parle FastCGI
Sans shell, pas de cgi-fcgi ni de curl pour sonder le service. Le binaire Go d'entrée implémente donc directement le protocole FastCGI : il ouvre le socket Unix du pool, envoie une requête sur ping.path, et vérifie que la réponse est bien pong.
C'est une vraie requête traitée par un worker php-fpm, pas une ouverture de socket. Un pool saturé ou bloqué échoue au healthcheck ; un pool qui écoute mais ne répond plus aussi.
Le déploiement : un chart Helm
L'image seule ne sert à rien : il lui faut un serveur web devant, un volume partagé, et quelqu'un pour y déposer WordPress. Le chart qui fait tourner ce site est publié — wordpress-hardened — et décrit un pod à un initContainer et quatre conteneurs.
flowchart TB
Init["initContainer<br/>alpine + curl"] -->|"depose le core"| Vol["emptyDir<br/>/var/www/html"]
Vol --> Nginx["nginx-waf-hardened<br/>statiques + ModSecurity"]
Vol --> PHP["php-fpm-hardened<br/>UID 1999"]
Nginx -->|"FastCGI"| PHP
PVC["PVC Longhorn<br/>wp-content"] --> PHP
PHP -.-> Exp1["php-fpm exporter"]
Nginx -.-> Exp2["nginx exporter"]
nginx sert les fichiers statiques et applique les règles ModSecurity, php-fpm exécute le PHP, les deux exporters exposent les métriques Prometheus. Un Redis pour le cache d'objets s'ajoute en option.
L'essentiel du chart tient dans ses valeurs :
initContainer:
image: "alpine:TAG"
pullPolicy: Always
nginx:
image: "jbsky/nginx-waf-hardened:TAG"
containerPort: 8080
securityContext:
runAsUser: 1999
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
phpfpm:
image: "jbsky/php-fpm-hardened:TAG"
containerPort: 9000
securityContext:
runAsUser: 1999
runAsNonRoot: true
allowPrivilegeEscalation: false
podSecurityContext:
fsGroup: 1999
redis:
enabled: false
Le même runAsUser: 1999 dans les deux conteneurs n'est pas une coquetterie : c'est ce qui rend le volume partagé lisible des deux côtés.
Les trois points qui ne vont pas de soi
Sans shell, c'est l'initContainer qui fait le travail de fichiers
Une image FROM scratch ne peut pas copier, décompresser ni changer un propriétaire au démarrage. Tout ce qui ressemble à de la préparation de fichiers est donc sorti de l'image et confié à un initContainer Alpine, qui vit le temps de son script et disparaît avant que le service ne démarre.
C'est aussi ce qui permet de garder l'image de production sans outils : l'outillage existe, il n'est simplement jamais dans le conteneur qui écoute.
emptyDir n'est pas un détail d'implémentation
nginx doit servir les fichiers statiques de WordPress sans passer par PHP. Il lui faut donc les voir. Le volume est monté au même chemin dans les deux conteneurs, et c'est un emptyDir : éphémère, recréé à chaque démarrage du pod.
Seul wp-content — les uploads, les plugins, les thèmes — vit sur un volume persistant. La séparation est nette : ce qui se retélécharge est jetable, ce qui a été produit par le site est gardé.
Le core est retéléchargé à chaque démarrage
L'initContainer ne porte pas WordPress : il va chercher la dernière version stable et la dépose dans le volume. Le choix est assumé — pas d'image à reconstruire à chaque sortie de version, au prix de la reproductibilité : deux démarrages à deux semaines d'écart ne donnent pas le même core.
Un détail qui a son importance ici : les pods n'ont pas d'accès direct à Internet, tout le HTTPS sortant passe par un proxy avec interception TLS. L'initContainer doit donc faire confiance à la CA du proxy avant son premier appel réseau — y compris avant apk, qui en a besoin pour son propre miroir.
Liens
- Code source : Depot php-fpm-hardened
- Image Docker : Docker Hub
- Deploiement : chart Helm wordpress-hardened