TP7 : Docker avancé et Docker Compose (multi-stage, non-root, PostgreSQL)
Vous savez déjà lancer un conteneur et écrire un Dockerfile simple (TP4, partie 4). Ce TP vous apprend à produire des images petites, reproductibles et sûres, puis à décrire une application complète (application + base de données + outil d’administration) avec Docker Compose.
Fichiers de départ :
app-starter.zip
1. Objectifs pédagogiques
À l’issue du TP, vous serez capable de :
- expliquer le rôle des couches et du cache dans un build Docker ;
- écrire un
Dockerfilemulti-stage avec un.dockerignore, un utilisateur non-root et unHEALTHCHECK; - mesurer et comparer la taille, le temps de build et le nombre de vulnérabilités de deux images ;
- décrire une pile Compose avec réseaux, volume nommé,
healthcheck,depends_onet fichier.env; - construire une image multi-architecture (
amd64/arm64) avec Buildx.
Les images de cette page sont des schémas explicatifs, pas des captures d’écran.
2. Environnement du TP
-
Une VM Ubuntu Server 26.04 (la VM de lab des TP4–TP6 convient) avec Docker Engine et le plugin Compose :
docker version docker compose version docker buildx versionSi
docker composeest absent :sudo apt install -y docker-compose-v2(ou installez Docker depuis le dépôt officieldownload.docker.com). -
Node.js 24 et npm sur la VM (uniquement pour générer
package-lock.json). -
Copier le dossier
app-starter/sur la VM. Il contient une petite API TypeScript qui compte les visites dans PostgreSQL.
| Fichier fourni | Rôle |
|---|---|
src/server.ts |
API Express : GET / (compteur) et GET /health |
Dockerfile.naive |
Dockerfile « naïf » à ne pas reproduire (point de comparaison) |
Dockerfile |
Squelette multi-stage avec des TODO |
compose.yaml |
Squelette Compose avec des TODO |
.env.example |
Variables attendues, sans vraie valeur |
3. Partie 1 : Couches et cache
| Notion | Explication |
|---|---|
| Couche | Chaque instruction RUN, COPY, ADD produit une couche en lecture seule, empilée sur la précédente. |
| Cache | Si une instruction et ses fichiers d’entrée n’ont pas changé, Docker réutilise la couche existante. |
| Invalidation | Dès qu’une couche change, toutes les suivantes sont reconstruites. |
| Contexte | Le dossier envoyé au démon au début du build. Sans .dockerignore, il inclut node_modules/, .git/, .env… |
-
Générer le fichier de verrouillage des dépendances (nécessaire à
npm ci) :cd app-starter npm install -
Construire l’image naïve en mesurant le temps :
time docker build -f Dockerfile.naive -t visits-app:naive . -
Modifier une ligne de
src/server.ts(le message par exemple) et reconstruire. Observer quenpm installest réexécuté : pourquoi ? -
Afficher les couches et leur taille :
docker history visits-app:naive docker image ls visits-app
4. Partie 2 : Mesurer l’image naïve
-
Relever les trois indicateurs de l’image naïve. Pour les vulnérabilités, utiliser Trivy sous forme de conteneur (aucune installation nécessaire) :
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:latest image --severity HIGH,CRITICAL --quiet visits-app:naive -
Vérifier sous quel utilisateur tourne le conteneur :
docker run --rm visits-app:naive idUn processus
rootdans un conteneur estrootsur le noyau de l’hôte si une faille permet de s’échapper du conteneur.
5. Partie 3 : Dockerfile multi-stage
-
Créer un fichier
.dockerignorequi exclut au minimum :node_modules,dist,.git,.env,Dockerfile*,compose.yaml,*.md. -
Compléter les
TODOduDockerfile:- étape
deps: copier uniquementpackage*.jsonpuisnpm ci; - étape
build: récupérernode_modules, copier les sources, compiler, puisnpm prune --omit=dev; - étape
runtime: copierdist/,node_modules/etpackage.jsonavec--chown=node:node, puisUSER node; - ajouter un
HEALTHCHECK(--interval=10s --timeout=3s --retries=3) qui appelle/health.
- étape
-
Construire et comparer :
time docker build -t visits-app:multistage . docker image ls visits-app docker run --rm visits-app:multistage id # doit afficher uid=1000(node) -
Modifier
src/server.tset reconstruire : l’étapenpm cidoit maintenant venir du cache (CACHED). -
Remplir le tableau comparatif (à reprendre dans le
README.mdde votre rendu) :
| Indicateur | naive |
multistage |
|---|---|---|
| Taille de l’image | ||
| Temps de build (à froid) | ||
| Temps de build (code modifié) | ||
| Vulnérabilités HIGH/CRITICAL | ||
| Utilisateur |
6. Partie 4 : Pile Docker Compose
-
Créer le fichier
.envà partir de.env.exampleet choisir un mot de passe. Vérifier que.envest bien ignoré par Git. -
Compléter les
TODOdecompose.yaml:db: volume nommépgdata,healthcheckavecpg_isready -U $${POSTGRES_USER}, aucun port publié, réseaubackend;app: variablesPGUSER,PGPASSWORD,PGDATABASEissues du.env,depends_onaveccondition: service_healthy, réseauxfrontendetbackend;adminer: réseaubackend.
-
Valider puis démarrer la pile :
docker compose config # affiche le fichier résolu (variables remplacées) docker compose up -d --build docker compose ps # db doit être "healthy" avant que app démarre curl http://localhost:3000/ curl http://localhost:3000/health -
Ouvrir Adminer (
http://<IP-VM>:8080, serveurdb) et consulter la tablevisits. -
Prouver la persistance :
docker compose downpuisdocker compose up -d. Le compteur doit reprendre là où il s’était arrêté. Que faitdocker compose down -v? -
Prouver l’isolation réseau : depuis la VM,
psql -h localhostdoit échouer (port non publié), alors que l’application jointdb. -
Mettre
appà l’échelle :docker compose up -d --scale app=2. Quel problème apparaît avecports: "3000:3000"? Proposez une solution (défi plus bas).
7. Partie 5 : Buildx et images multi-architecture
| Terme | Explication |
|---|---|
| BuildKit | Moteur de build par défaut de Docker : étapes parallèles, cache avancé, secrets de build. |
| Buildx | Interface CLI de BuildKit : builders, plateformes multiples, export du cache. |
| Manifest list | Index qui associe une même référence d’image à plusieurs architectures. |
-
Activer l’émulation des autres architectures et créer un builder :
docker run --privileged --rm tonistiigi/binfmt --install arm64 docker buildx create --name multiarch --use docker buildx inspect --bootstrap -
Construire et pousser une image
amd64+arm64sur votre compte Docker Hub :docker login docker buildx build --platform linux/amd64,linux/arm64 \ -t <utilisateur>/visits-app:1.0.0 --push . docker buildx imagetools inspect <utilisateur>/visits-app:1.0.0
Défi
- Ajouter un service Nginx reverse proxy devant
app(seul port publié : 80) et lancerappavec--scale app=3. Vérifier aveccurlque le champhostchange d’une requête à l’autre.- Utiliser un secret de build BuildKit (
RUN --mount=type=secret,id=npmrc …) et vérifier avecdocker historyque le secret n’apparaît dans aucune couche.
8. Livrables
Captures à déposer dans captures/ :
| Fichier | Contenu attendu |
|---|---|
image-comparison.png |
docker image ls visits-app montrant naive et multistage |
non-root.png |
docker run --rm visits-app:multistage id |
compose-ps.png |
docker compose ps avec db en état healthy |
persistence.png |
Compteur de visites conservé après down / up |
buildx-manifest.png |
docker buildx imagetools inspect listant amd64 et arm64 |
reverse-proxy.png |
(défi) réponses venant de trois conteneurs différents |
Ainsi que : Dockerfile, .dockerignore, compose.yaml, .env.example (pas .env) et le tableau comparatif rempli.