Docker, Compose et CI/CD : conteneuriser puis automatiser
Mission DevOps du stage : conteneuriser une application, puis automatiser sa vérification et sa publication. L'exercice avait une contrainte inhabituelle et c'est ce qui en a fait l'intérêt : monter la même chaîne trois fois, sur trois forges différentes - GitHub Actions, Gitea auto-hébergé, puis GitLab.
Conteneuriser : expliciter l'environnement
Conteneuriser une application existante revient à expliciter tous ses prérequis : versions, variables d'environnement, dépendances système, volumes de données. Résultat : un environnement reproductible, identique en développement et en production, et une mise en service qui tient en une commande.
# la stack de déploiement ne construit plus rien :
# elle consomme l'image publiée par le pipeline
services:
app:
image: <registre>/mon-app:latest
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c",
"import urllib.request; urllib.request.urlopen('http://localhost:5000/api/health')"]
interval: 10s
retries: 5
networks: [appnet]
proxy:
image: nginx:1.27-alpine
ports: ["8080:80"]
depends_on:
app: { condition: service_healthy }
networks: [appnet]
networks:
appnet:Un détail de ce fichier m'a coûté une demi-heure avant que je comprenne : depends_on seul ne garantit que l'ordre de démarrage, pas que le service soit prêt à répondre. Sans le couple healthcheck / condition: service_healthy, le proxy démarre avant l'application et sert une erreur 502. La différence entre « démarré » et « prêt » n'est pas une subtilité de documentation.
Le pipeline, trois fois plutôt qu'une
La chaîne est toujours la même : un push construit l'image, la teste, puis la publie sur un registre. Ce qui change d'une forge à l'autre est la syntaxe et, surtout, la façon dont le travail est exécuté.
- GitHub Actions : runner auto-hébergé installé sur le serveur. Le job s'exécute directement sur la machine, donc Docker et les dossiers de déploiement sont accessibles tels quels.
- Gitea : forge auto-hébergée, jobs exécutés dans un conteneur. Le localhost du job n'est plus celui du serveur - le test de fumée a dû être réécrit pour s'exécuter à l'intérieur du conteneur testé.
- GitLab : runner en mode shell, retour à une exécution directe sur la machine, mais avec une syntaxe de pipeline entièrement différente (stages et jobs au lieu d'étapes).
Le runner auto-hébergé n'était pas un choix esthétique : le serveur n'est pas joignable depuis Internet. Un runner hébergé par la forge ne peut tout simplement pas l'atteindre. C'est le runner qui sort vers la forge, récupère le travail, et l'exécute localement.
Refaire trois fois la même chose a un effet que je n'attendais pas : à la troisième, on ne lit plus la documentation de la même façon. On cherche où l'outil range les concepts qu'on connaît déjà - où il déclare les étapes, où il stocke les secrets, comment il désigne un runner - au lieu de réapprendre depuis zéro.
Taguer par le hash de commit
Chaque image est publiée avec deux étiquettes : latest et le hash court du commit. La seconde est celle qui compte. latest est mouvante - elle pointe vers le dernier build, et si deux personnes publient à quelques minutes d'intervalle, plus personne ne sait quelle version tourne réellement. Le hash, lui, est immuable et relie l'image au commit exact qui l'a produite.
C'est ce qui rend un retour arrière possible : on redéploie un tag connu comme fonctionnel plutôt que de reconstruire en espérant. En production, on déploie un tag explicite, jamais latest.
Le lien direct avec ma formation
Docker et Git faisaient déjà partie de mes projets Ynov - Ymmo tourne en Docker Compose avec un backend C# et un microservice FastAPI. Le stage a ajouté la dimension industrialisation : un pipeline utilisé par d'autres, la gestion des secrets, les caches de build, les temps d'exécution. En retour, ces pratiques ont été réinjectées dans mes projets scolaires.
Ce que le test de fumée a réellement bloqué
Le pipeline commence par un test volontairement minimal : construire l'image, la lancer, appeler son point de contrôle de santé. Rien de sophistiqué. Pour vérifier qu'il servait à quelque chose, j'ai poussé une faute de syntaxe délibérée.
Le pipeline est passé au rouge à cette première étape. L'image cassée n'a jamais atteint le registre - le tag correspondant est simplement absent - et la version en service n'a pas bougé. Le correctif poussé derrière a fait repasser la chaîne au vert. Une application qui ne démarre pas ne peut pas être publiée : c'est peu, mais c'est exactement ce qu'on lui demande.
Séparer la construction du déploiement
Sur deux des trois forges, j'ai volontairement arrêté le pipeline après la publication de l'image, sans étape de déploiement. La mise en service reste une action décidée par quelqu'un. Ce n'est pas de la prudence excessive : le maillon le plus fragile de toute la chaîne était justement le déploiement, où le runner pilotait sa propre machine via la socket Docker. Pratique en laboratoire, difficilement défendable ailleurs - qui détient cette socket détient la machine.
Le déploiement manuel a lui aussi eu sa surprise : après avoir recréé le conteneur applicatif, le site renvoyait des erreurs 502 alors que tout semblait sain. Nginx avait mis en cache l'adresse de l'application au démarrage, et le conteneur recréé en avait une nouvelle. Un redémarrage du proxy suffisait. Le genre de détail qu'aucun cours ne mentionne et qu'on n'oublie plus.
Les difficultés rencontrées sur ces missions font l'objet de l'article suivant.