Docker, c'est bien, mais c'est quand même pas idéal pour tout

En y regardant de plus près, on se rend rapidement compte que Docker a avant tout été imaginé pour faire tourner des applications stateless (sans état), donc des trucs qu'on peut tuer et redémarrer à loisir.

Du coup, pour faire du statefull (base de données, applications persistentes, etc…), c'est déjà nettement plus compliqué (sans être impossible hein, mais disons que ça risque de galérer un peu). L'idée même du conteneur est d'être une structure jetable mais fournie avec les piles (toutes les librairies et une grande partie de la logique de fonctionnement).

Des cas simples…

Il existe donc des cas d'usages finalement relativement simples :

  • l'intégralité de l'application est un seul exécutable (avec un éventuel fichier de conf) :
    • on peut mettre le fichier de conf dans un volume (en lecture seule même éventuellement)
    • on peut faire un dérivé du conteneur « officiel » et y inclure le fichier de conf en question (moins souple, mais l'ensemble est monolithique)
  • l'application peut s'exécuter à 100% dans le conteneur mais requiert un volume pour ces données :
    • on peut très facilement lui coller un volume persistent
    • en cas de mise à jour, le volume persistent ne bougera pas
    • si l'application en question doit toucher à la structure des fichiers lors des mises à jour, ça va être très très compliqué pour revenir en arrière
  • l'application peut s'exécuter à 100% dans le conteneur mais requiert une base de données :
    • on peut héberger la base de données sur l'hôte (ça permet de séparer ce qui est stateless de ce qui est statefull)
    • on peut héberger la base de données dans un autre conteneur à côté et lier simplement les deux conteneurs entre eux (comme ça, la base de données ne peut mécaniquement pas être accessible par autre chose que l'application)
  • l'application consiste en plusieurs exécutables qui travaillent ensemble :
    • on peut tous les inclure dans un conteneur, mais c'est super cracra
    • on peut les séparer dans des conteneurs différents mais tous basés sur la même image, c'est plus propre mais nettement plus compliqué

Il y a probablement d'autres cas, mais ça résume, je trouve, assez bien le type d'applications que j'héberge actuellement.

et des cas plus compliqués…

Le souci avec PHP, c'est qu'en gros, ça fait un peu tout ce qui a été cité avant : généralement, il faut une base de données, des fichiers persistents et des fichiers de configuration (en plus de la configuration « système » elle-même). Et l'exécution PHP requiert que le process naisse, interprète, exécute et meurt. Nous sommes loin de l'application dans un seul exécutable…

Du coup, plusieurs approches sont possibles :

  • tout mettre dans un conteneur Apache + PHP en y incluant toute la conf PHP :
    • les changements de configuration passent obligatoirement par une reconstruction (potentiellement lourde) du conteneur
    • il faut quand même prévoir un ou plusieurs volumes persistents et en fonction des applications, cela peut être très compliqué (beaucoup d'applications PHP écrivent dans plusieurs répertoires)
    • quoiqu'il arrive, il faut prévoir au moins un conteneur MariaDB par application PHP (pas gênant en soi, mais en terme de ressources, c'est un peu bourrin)
    • faut faire du Apache et ça, ça donne vraiment envie de vomir
  • tout mettre dans un conteneur FPM et mettre l'application PHP en elle-même dans un volume à part :
    • il faut alors prévoir un conteneur supplémentaire NginX (par exemple) pour la partie présentation. Il faudra également que ce conteneur ait un accès en lecture seule au volume de données du conteneur FPM (sinon, ça ne marchera pas)
    • il faut inclure les sources PHP du logiciel dans le conteneur : encore une fois, il faut le reconstruire à chaque version et prévoir un mécanisme pour mettre à jour les fichiers du volume persistent, sans écraser les configurations/données

(re)faire de l'Apache est honnêtement au-dessus de mes forces, je me suis donc penché sur les méthodes utilisées par quelques très grosses applications PHP : NextCloud et WordPress.

Les approches sont en générales assez similaires :

  • fournir un conteneur très générique avec de l'Apache + PHP et tout ce qu'il faut dedans
  • fournir un conteneur FPM contenant au moins le programme PHP et un moyen de mettre à jour le volume persistent créé par le conteneur
  • y adjoindre systématiquement un conteneur de base de données

Dans tous les cas, ça marche très bien pour du lab. Le souci, c'est que ça fait des images relativement lourdes et surtout, c'est intéressant si l'on veut distribuer le logiciel en même temps que son environnement. Si on a déjà le logiciel (avec tout ce qu'il faut dedans), je ne vois pas bien l'intérêt.

D'autant plus qu'en cas de mise à jour, il faut de toutes manières aller écraser l'ensemble du contenu du volume persistent : la détection d'une nouvelle version me semble relativement hasardeuse en fonction des cas et surtout on perd la possibilité de faire un rollback en redémarrant simplement la version précédente du conteneur : vous démarrez NextCloud 13.0.2, un truc merde, vous ne pouvez plus revenir en arrière.

On perd du coup une grande partie de l'intérêt des conteneurs. Pareil pour les base de données : si le schéma ou les données sont touchées par la mise à jour, c'est en fini en cas de problème.

C'est un peu ce qui me fait dire que Docker et PHP ne sont peut-être pas vraiment fait pour s'entendre. Ça pourrait être bien pire, mais c'est loin, très loin d'être idéal.

Retour à la case départ

Cela me ramène donc à mon idée d'origine :

  • un dérivé d'un conteur nginx:stable qui embarquerait toute la configuration standard que j'ai l'habitude de mettre partout (ça représente 2 ou 3 fichiers en tout, c'est intéressant de les embarquer directement pour retrouver systématiquement la même structure) ;
  • un dérivé d'un conteneur PHP-FPM contenant toutes les extensions nécessaires au bon fonctionnement de 99% des applis PHP (mysqli, gd, etc…), ainsi que toutes les configurations recommandées pour FPM (opcache, durée de vie, nombre de process, etc…). Des recettes pour faire le plus propre possible (en supprimant les outils de compilation à la fin par exemple) existe. Au pire des cas, NextCloud fournit un template très très complet auquel il ne faut pratiquement rien adjoindre.
  • un conteneur mariadb:latest tout ce qu'il y a de plus standard (parce qu'a priori s'il y a un conteneur MariaDB par application, faire de l'optimisation ne va vraiment pas être nécessaire).

Cette approche permet d'avoir finalement une pile d'exécution PHP assez générique et qui convient à presque tous les usages : les conteneurs permettent d'avoir des environnements d'exécution bien séparés, les base de données sont clairement disjointes, les données persistentes sont bien rangées dans un coin.

Cela pose quand même quelques autres petits inconvénients :

  • il est nécessaire de peupler le volume persistent avec le programme PHP avant de lancer le conteneur quoiqu'il arrive, on ne peut pas simplement se contenter de lancer le conteneur et le laisser se débrouiller ;
  • une seule image PHP-FPM, cela veut dire toutes les librairies embarquées avec potentiellement des librairies complètement inutiles voire dangeureuses dedans.

Mais au moins, de cette manière, pour faire tourner une application, est relativiment simple. Cela nécessite :

  • deux volumes persistents, un pour les données de l'application PHP et un pour les données de la base de données MariaDB
  • présenter le volume de données à un conteneur MariaDB
  • présenter le volume applicatif à un conteneur PHP-FPM en écriture
  • présenter le même volume à un NginX en lecture seule
  • ajouter les configurations nécessaires pour NginX (1 fichier de conf pour l'appli, une clé privée et un certificat public pour la partie HTTPS) en lecture seule
  • et évidemment rajouter toute la communication entre ces différents conteneurs
  • le tout décrit dans un fichier YAML correctement commenté.

Bref, ça fait quand même beaucoup beaucoup de bordel pour pouvoir faire tourner chaque application. Je suis par contre complètement incapable de dire si c'est plus ou moins efficace en terme de maintenabilité :

  • être systématiquement sur les toutes dernières version de NginX, MariaDB et PHP peut présenter des avantages considérables dans certains cas…
  • … mais aussi faire bien chier dans d'autres. S'il faut que je maintienne 3 conteneurs PHP différents, ça va vite devenir très lourd…
  • il faut construire (et donc reconstruire régulièrement) au moins deux conteneurs différents : un pour PHP et un pour NginX. Ce n'est jamais grand-chose, mais j'ignore si l'on peut automatiser ce processus (en mode quand PHP-FPM est mis à jour, va faire un docker build).

Foutaise !

Si tu as bien tout suivi, tu as retenu au moins une chose : tout dockériser, c'est surtout maîtriser l'ensemble de la chaîne de montage de chaque image. Partir du principe que les images toutes faites vont convenir me semble hautement illusoire. Si l'on ne veut pas s'éparpiller, on a grandement intérêt à acquérir les bases logistiques nécessaires pour produire de l'image à tour de bras.

Finalement, peu importe qu'on le déploie avec simplement du Docker, avec du Docker-compose, du Docker Swarm, du Kubernetes, du RancherOS ou n'importe quel autre orchestrateur de conteneurs. Maîtriser le contenu des images est finalement plus important que dans maîtriser le contenant.

Au prochain numéro, on essaiera (tant bien que mal) de voir quelle solution sont adoptables pour faire du reverse-proxy.