Objectif de la journée : transformer l'application en un artefact unique (une image Docker) que le pipeline pourra construire à chaque commit, puis la faire tourner avec sa base de données derrière un reverse proxy Nginx, en une seule commande.
Aujourd'hui, pour lancer l'API il faut « le bon Node.js, les bonnes dépendances, la bonne commande ». Une image Docker emballe tout cela une fois pour toutes. Demain, le pipeline construira cette image à chaque commit ; après-demain, c'est elle qui sera déployée. Tout ce que vous écrivez aujourd'hui sera réutilisé.
Créez un Dockerfile à la racine qui respecte ces exigences :
| Exigence | Pourquoi |
|---|---|
Une première étape nommée deps qui copie package.json et package-lock.json puis lance npm ci --omit=dev |
installer uniquement les dépendances de production, et profiter du cache tant que ces deux fichiers ne changent pas |
Une étape finale basée sur node:24-alpine |
image de base légère |
L'étape finale copie node_modules depuis deps, puis package.json et le dossier src |
ni les tests, ni les outils de développement en production |
USER node |
ne pas exécuter l'application en root |
EXPOSE 3000 |
documenter le port d'écoute |
Un HEALTHCHECK qui appelle /health |
Docker sait si l'application répond, pas seulement si le processus tourne |
CMD ["node", "src/server.js"] |
la commande de démarrage |
Créez aussi un .dockerignore qui exclut au minimum node_modules, coverage, .git, .github, tests, docs, ainsi que .env et .env.* : sans cette exclusion, un fichier de secrets présent dans le dossier serait copié dans l'image.
docker build -t devops-tasktracker:local .
docker image ls devops-tasktrackerRésultat attendu : l'image se construit. Dans la sortie de docker image ls, elle ne pèse pas plus de 20 Mo de plus que node:24-alpine (lancez docker image ls node:24-alpine pour comparer) : votre application et ses dépendances de production ne doivent presque rien ajouter à l'image de base. Modifiez une ligne de src/app.js et reconstruisez : les étapes npm ci doivent apparaître comme CACHED. Si ce n'est pas le cas, revoyez l'ordre des instructions.
Créez compose.yml avec trois services :
| Service | Image | Rôle | Port publié sur l'hôte |
|---|---|---|---|
nginx |
nginx:1.28-alpine |
point d'entrée unique, reverse proxy | 80:80 |
app |
construite depuis . |
l'API | aucun |
db |
postgres:16-alpine |
stockage des tâches | aucun |
Contraintes :
dbreçoit les variablesPOSTGRES_USER=tasks,POSTGRES_PASSWORD=tasksetPOSTGRES_DB=tasks: sansPOSTGRES_PASSWORD, l'image officielle PostgreSQL refuse de démarrer ;appreçoitDATABASE_URL=postgres://tasks:tasks@db:5432/tasks(dans un réseau Compose, un service se joint par son nom, icidb) ;dba un volume nommé pour/var/lib/postgresql/dataet unhealthcheckbasé surpg_isready -U tasks -d tasks;appattend quedbsoit en bonne santé, etnginxattendapp(depends_onaveccondition: service_healthy).
Créez nginx/default.conf : un bloc server qui écoute sur le port 80 et relaie toutes les requêtes vers app:3000 avec proxy_pass, en transmettant les en-têtes Host, X-Real-IP, X-Forwarded-For et X-Forwarded-Proto. C'est ce que vous avez fait ce matin dans le lab configurer-reverse-proxy-nginx. Pour X-Forwarded-For, utilisez la variable $proxy_add_x_forwarded_for (elle ajoute l'adresse du client à l'en-tête existant) et non $http_x_forwarded_for (vide si le client n'en envoie pas). Montez ce fichier en lecture seule dans le conteneur Nginx, à la place de /etc/nginx/conf.d/default.conf.
Dans l'Atelier, la machine est neuve chaque après-midi : après atelier-init, relancez docker compose up -d --build --wait avant de vérifier ou d'évaluer.
docker compose up -d --build --wait
docker compose ps
curl -i localhost/health
curl -X POST localhost/tasks -H 'content-type: application/json' -d '{"title":"survivre au redemarrage"}'
docker compose restart app
docker compose up -d --wait
curl localhost/tasks
docker compose logs nginx | tail -5--wait attend que les services soient en bonne santé (healthcheck) avant de rendre la main : sans lui, un curl lancé trop tôt répond « connection refused » alors que tout va bien.
Si un téléchargement d'image échoue avec toomanyrequests, c'est la limite de téléchargements anonymes de Docker Hub : attendez quelques minutes et relancez, ou connectez-vous avec un compte Docker Hub gratuit (docker login).
Résultat attendu : curl -i montre Server: nginx ; la tâche est toujours là après le redémarrage de app (elle est dans PostgreSQL) ; seul nginx publie un port dans docker compose ps ; vos requêtes apparaissent dans les journaux Nginx.
Complétez ce tableau avec docker image ls :
| Image | Taille |
|---|---|
node:24 |
|
node:24-alpine |
|
devops-tasktracker:local |
Chaque déploiement transfère l'image sur un serveur. Multipliez l'écart de taille par le nombre de déploiements d'une équipe qui livre dix fois par jour : c'est de la bande passante, du stockage et de l'énergie en moins.
-
Dockerfilemulti-stage et.dockerignorecommités - Image à moins de 20 Mo au-dessus de
node:24-alpine -
compose.ymlavecnginx,app,db, seulnginxpublie un port - Les données survivent à
docker compose restart app - Le CI du jour 1 est toujours au vert après votre
git push -
git statuspropre (rien à committer) etgit pushfait, puis Évaluer et Terminer l'Atelier : ce qui n'est pas poussé est perdu
Si vous n'avez pas terminé, récupérez la correction au début de l'Atelier du jour 3, après atelier-init, dans ~/devops-tasktracker. La branche start-jN n'existe dans le dépôt du cours qu'à partir du matin du jour N. Commencez par enregistrer votre travail, puis fusionnez-la en gardant sa version en cas de conflit, et créez un commit à vous :
git add -A
git commit -m "wip: avant rattrapage" # ignorez le message si rien n'est à committer
git fetch upstream
git merge -X theirs --no-edit upstream/start-j3
git commit --allow-empty -m "rattrapage J3"
npm ci
npm test
git push origin main-X theirs garde vos modifications qui n'entrent pas en conflit, et prend la version du cours là où vous aviez écrit la même chose différemment. Le commit vide rattrapage J3 garantit que votre fork contient au moins un commit à vous, même quand la fusion n'en crée aucun.