01 Guide pas-a-pas - Atelier 1 - Semaine 2

Dockerisez la borne
d'accueil Next.js

Vous recevez une application Next.js du studio creatif et vous l'emballez dans une image Docker multi-stage. Sept etapes guidees, avec indices et verifications - les solutions sont ailleurs.

Ce qu'on vous fournit

Le dossier labos/semaine-2/demande-1-borne-accueil/ contient :

Vous y ajoutez : Dockerfile multi-stage, .dockerignore, construire.sh, demarrer.sh, arreter.sh, votre README.md.

Avant de commencer

Pre-requis

Vue d'ensemble

Le parcours en sept etapes

Cliquez sur une etape pour y acceder directement. Chaque etape a un objectif clair, des indications et une verification de reussite.

Etape 01

Inspecter le materiel Next.js

Etape 02

Comprendre le multi-stage

Etape 03

Ecrire le Dockerfile multi-stage

Etape 04

Injecter la banniere via ARG

Etape 05

Construire et demarrer

Etape 06

Encapsuler dans 3 scripts

Etape 07

Finaliser README et .dockerignore

01

Inspecter et tester le materiel sans Docker

Comprenez ce que fait l'application avant de l'emballer. Si vous avez Node.js >= 18 sur votre poste, lancez-la en local. Sinon, lisez le code, c'est court.

Indications

Si Node est installe :

Terminal
$ cd application/
$ npm install
$ npm run dev

Ouvrez http://localhost:3000/. Vous voyez la carte SVG du parc avec quatre pavillons cliquables, et l'onglet "Horaires" qui montre les activites de la journee.

Si Node n'est pas installe, ouvrez application/app/page.js dans votre editeur. C'est un seul composant React de quelques centaines de lignes : tableaux de pavillons, fonctions de dessin SVG, gestion de l'etat avec useState. Vous n'avez pas besoin de modifier ce code.

Vous savez decrire en une phrase ce que la borne montre au visiteur. Vous savez nommer les quatre pavillons (foret, marin, prairie, voyageurs).
02

Comprendre pourquoi un Dockerfile multi-stage

A la semaine 1, votre Dockerfile faisait une seule chose : copier des fichiers HTML statiques dans Apache. Ici, l'application doit d'abord etre compilee avant d'etre servie. Deux mondes : le monde du build et le monde de l'execution.

Le probleme du single-stage

Si vous mettiez tout dans une seule etape, l'image finale contiendrait :

  • Node.js (~150 Mo)
  • node_modules/ (~300 Mo)
  • Le source code (40 Mo)
  • Le resultat du build (10 Mo)

Votre image pese alors 500 Mo pour servir 10 Mo de contenu. Et chaque borne du parc telecharge ces 500 Mo a chaque deploiement.

La solution multi-stage

Un Dockerfile peut avoir plusieurs sections FROM. Chaque FROM demarre une nouvelle etape. La derniere etape est celle qu'on garde dans l'image finale. Les etapes precedentes existent juste pour produire des artefacts qu'on copie selectivement.

Pour cette borne, deux scenarios sont valides :

Scenario A - Next start (mode serveur Node)

  • Etape 1 (build) : node:20-slim, npm ci, npm run build.
  • Etape 2 (run) : node:20-slim aussi (Next.js a besoin du runtime Node), mais sans le code source ni les devDependances. On copie .next/, public/, node_modules/ (production seulement) et package.json.
  • Commande de demarrage : npm start qui execute next start.

Scenario B - Next export + nginx (rendu statique)

  • Etape 1 (build) : node:20-slim, npm ci, npm run build. La page app/page.js est marquee "use client" et tout le rendu se fait dans le navigateur, donc l'export statique fonctionne.
  • Etape 2 (run) : nginx:1.27-alpine. On copie uniquement le dossier out/ dans /usr/share/nginx/html/.
  • Plus leger (~25 Mo final), pas de Node au runtime, mais perte des fonctionnalites Next dynamiques (que vous n'utilisez pas ici de toute facon).

Choisissez votre scenario maintenant et tenez-vous-y pour la suite. Le scenario B donne une image plus legere et illustre mieux le pattern multi-stage classique. Le scenario A est plus proche de ce qu'on ferait pour une vraie app Next avec API routes ou ISR.

Vous pouvez expliquer en deux phrases la difference entre l'etape de build et l'etape de run, et pourquoi on n'embarque pas Node + node_modules dans l'image finale.
03

Ecrire le Dockerfile multi-stage

Creer le fichier Dockerfile a la racine du dossier demande-1-borne-accueil/ (donc a cote de application/, pas dedans).

Squelette - scenario A (next start)

Dockerfile - scenario A
# --- Etape 1 : build ---
FROM node:20-slim AS builder
WORKDIR /app

# Cache npm install : copier package.json AVANT le reste
COPY application/package*.json ./
RUN npm ci

# Maintenant le code source
COPY application/ ./

RUN [commande de build Next]

# --- Etape 2 : run ---
FROM node:20-slim
WORKDIR /app

# Copier uniquement les artefacts utiles depuis builder
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules

EXPOSE 3000
CMD ["npm", "start"]

Squelette - scenario B (export statique + nginx)

Dockerfile - scenario B
# --- Etape 1 : build ---
FROM node:20-slim AS builder
WORKDIR /app
COPY application/package*.json ./
RUN npm ci
COPY application/ ./

# Activer l'export statique dans next.config.mjs : output: 'export'
RUN [npm run build]

# --- Etape 2 : run ---
FROM nginx:1.27-alpine
COPY --from=builder /app/out /usr/share/nginx/html
EXPOSE 80
Le scenario B exige d'ajouter output: 'export' dans next.config.mjs. Sans ca, le dossier out/ ne sera pas genere et votre COPY --from=builder /app/out echouera.
Pour Next.js 13+, l'export statique n'est pas compatible avec certaines fonctionnalites (API routes, middleware, getServerSideProps). Pour cette borne, l'app est entierement client-side, donc vous etes libres de choisir.
Le fichier Dockerfile existe, contient deux blocs FROM (le premier avec AS builder, le second sans), et au moins une instruction COPY --from=builder ....
04

Injecter la banniere d'identification (ARG -> ENV -> UI)

Faire apparaitre votre nom, votre matricule et la date de build dans la borne. Ce qui prouve que votre image et celle de votre voisin de classe ne sont pas identiques.

La chaine ARG -> ENV -> NEXT_PUBLIC -> UI

Next.js a une regle particuliere : seules les variables d'environnement qui commencent par NEXT_PUBLIC_ sont injectees dans le bundle JavaScript cote client. Donc votre chaine doit etre :

  1. ARG NOM_ETUDIANT dans le Dockerfile (etape de build)
  2. ENV NEXT_PUBLIC_NOM_ETUDIANT=$NOM_ETUDIANT dans le Dockerfile (avant le npm run build pour qu'elle soit lue par le bundler)
  3. Dans le code React : process.env.NEXT_PUBLIC_NOM_ETUDIANT

Modification du Dockerfile (etape builder)

Dockerfile - extrait builder
ARG NOM_ETUDIANT="Etudiant Inconnu"
ARG MATRICULE="0000000"
ARG BUILD_DATE="date-non-fournie"

ENV NEXT_PUBLIC_NOM_ETUDIANT=$NOM_ETUDIANT
ENV NEXT_PUBLIC_MATRICULE=$MATRICULE
ENV NEXT_PUBLIC_BUILD_DATE=$BUILD_DATE

# Le build doit avoir lieu APRES la definition des ENV
RUN npm run build

Modification du composant React

Dans application/app/page.js, ajoutez en haut du composant PageBorneAccueil une banniere :

page.js - extrait
// Au debut du JSX retourne
<div id="banniere-identification">
  Borne d'accueil - {process.env.NEXT_PUBLIC_NOM_ETUDIANT} ({process.env.NEXT_PUBLIC_MATRICULE}) - build {process.env.NEXT_PUBLIC_BUILD_DATE}
</div>

Stylez ensuite #banniere-identification dans globals.css pour qu'elle apparaisse en haut de l'ecran (vert zoo, texte blanc, hauteur 40px par exemple).

Si vous mettez ENV apres npm run build, le bundle est compile sans vos variables et la banniere affiche undefined. L'ordre dans le Dockerfile compte.
Pour le scenario B (export statique), il faut absolument que les ENV soient definies avant le npm run build car le HTML genere fige les valeurs en clair. Apres le build, c'est trop tard.
Apres rebuild avec --build-arg NOM_ETUDIANT="Votre Nom", vous voyez votre nom dans la banniere quand vous ouvrez la borne dans le navigateur.
05

Construire et demarrer

Lancer le build avec les --build-arg, demarrer un container, verifier dans le navigateur.

Terminal
$ docker build \
    --build-arg NOM_ETUDIANT="Prenom Nom" \
    --build-arg MATRICULE="1234567" \
    --build-arg BUILD_DATE="$(date -I)" \
    -t zoo-borne-accueil:1.0 .

$ docker run -d \
    --name borne-accueil-en-marche \
    -p 8090:[port-container] \
    zoo-borne-accueil:1.0

Le port cote container depend du scenario : 3000 pour scenario A (next start), 80 pour scenario B (nginx).

Ouvrez http://localhost:8090/. Vous devez voir la carte du parc avec votre banniere en haut.

Le premier npm ci dans l'etape de build prend 1 a 2 minutes. C'est normal. Tant que vous ne touchez pas a package.json, les builds suivants reutilisent le cache de cette couche et redeviennent rapides.
Si la page est blanche : ouvrez la console du navigateur (F12). Souvent une erreur "Cannot find module" ou un 404 sur un asset CSS. Comparez les COPY --from=builder de votre Dockerfile avec ce que produit reellement next build. Pour le scenario A : il faut copier .next, public, package.json ET node_modules.

La carte du parc s'affiche, les pavillons sont cliquables, l'onglet Horaires fonctionne, votre banniere est visible en haut.
06

Encapsuler dans trois scripts d'usage

Trois scripts bash en kebab-case, executables. Comme en semaine 1, mais avec deux nouveautes : construire.sh accepte des arguments pour la banniere, et il calcule automatiquement la date de build.

Squelette de construire.sh

construire.sh
#!/bin/bash
set -e

# Arguments optionnels (ordre : nom, matricule)
nomEtudiant="${1:-Etudiant Inconnu}"
matricule="${2:-0000000}"
dateBuild="$(date -I)"

nomImage="zoo-borne-accueil:1.0"

echo "[info] Build de $nomImage avec banniere $nomEtudiant ($matricule)"

docker build \
    --build-arg NOM_ETUDIANT="$nomEtudiant" \
    --build-arg MATRICULE="$matricule" \
    --build-arg BUILD_DATE="$dateBuild" \
    -t "$nomImage" .

echo "[ok] Image $nomImage prete."

Squelette de demarrer.sh

Identique a la semaine 1, avec gestion du conflit de nom de container. Variables en mixedCase : nomImage, nomContainer, portHote, portContainer (3000 ou 80 selon scenario).

Squelette de arreter.sh

Identique a la semaine 1 : docker stop puis docker rm du container nomme. Pas de volume a nettoyer ici (la borne d'accueil ne stocke rien).

Une fois ecrits, rendez-les executables : chmod +x *.sh.

La sequence ./construire.sh "Mon Nom" 1234567 ; ./demarrer.sh demarre la borne en moins d'une minute (apres premier build) et affiche votre banniere. ./arreter.sh la stoppe proprement.
07

Finaliser le README et le .dockerignore

Documenter votre travail et exclure du contexte de build ce qui n'a rien a y faire.

.dockerignore - critique pour Next.js

Sans .dockerignore, votre docker build envoie au daemon Docker tout votre dossier, y compris application/node_modules/ qui peut peser plusieurs centaines de Mo. A exclure imperativement :

  • application/node_modules/
  • application/.next/
  • application/out/
  • Les scripts .sh
  • Le README.md
  • Les fichiers caches OS (.DS_Store, Thumbs.db)
  • Le .dockerignore lui-meme

Test de validation : avant et apres ajout du .dockerignore, comparez la taille du contexte avec docker build --progress=plain .... Vous devriez voir le contexte passer de plusieurs centaines de Mo a quelques Mo.

README.md - sections obligatoires

  • Ce que c'est : un paragraphe sur la borne d'accueil et sur votre choix de scenario (A ou B).
  • Comment lancer : la sequence des trois scripts avec un exemple complet (vos vrais nom et matricule).
  • Ce qu'on voit : URL d'acces, capture d'ecran de la carte et de la vue Horaires, votre banniere bien visible.
  • Choix techniques : pourquoi multi-stage, pourquoi votre scenario, pourquoi votre port.

Bonus pedagogique : section Difficultes rencontrees qui raconte une difficulte concrete (banniere undefined, port deja pris, build qui echoue) et comment vous l'avez resolue.

Quelqu'un qui n'a pas vu votre travail peut, en suivant uniquement votre README, faire tourner la borne sur son poste. La taille du contexte de build est inferieure a 5 Mo.

Trois defis optionnels

Defi A - Reduire l'image en dessous de 40 Mo

Si vous avez choisi le scenario A (Node), basculez sur le scenario B (nginx + export statique) et mesurez. Si vous avez deja le scenario B, essayez nginx:alpine-slim. Documentez la taille avant et apres dans votre README.

Defi B - Healthcheck Docker

Ajoutez une instruction HEALTHCHECK qui appelle wget --quiet --tries=1 --spider http://localhost:PORT/ || exit 1 toutes les 30 secondes. Verifiez avec docker ps que la colonne STATUS affiche "healthy".

Defi C - Quatrieme onglet "Equipe du jour"

Ajoutez un troisieme bouton dans la bascule (a cote de Carte et Horaires) qui affiche une liste statique de 5 employes du zoo (nom, role, pavillon). C'est l'occasion de prouver que vous avez compris la structure du composant React et que votre Dockerfile rebuild proprement quand vous modifiez le code source.