feat: docs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Cas pratique",
|
||||
"description": "Relier React, NestJS, PostgreSQL et Nginx",
|
||||
"pages": ["react-nest-postgresql"]
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: React, NestJS et PostgreSQL
|
||||
description: Mettre en pratique les images, volumes, réseaux, ports et dépendances.
|
||||
---
|
||||
|
||||
Cet exemple rassemble un frontend React/Vite, un backend NestJS, une base PostgreSQL et Nginx.
|
||||
|
||||
## Dockerfile du backend NestJS
|
||||
|
||||
```dockerfile
|
||||
# Stage 1 - build
|
||||
FROM node:25-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2 - production
|
||||
FROM node:25-slim
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/scripts ./scripts
|
||||
|
||||
CMD ["node", "dist/src/main"]
|
||||
```
|
||||
|
||||
Le premier stage compile le TypeScript. Le second ne récupère que les fichiers nécessaires au runtime.
|
||||
|
||||
## Dockerfile du frontend React/Vite
|
||||
|
||||
```dockerfile
|
||||
# Stage 1 - build
|
||||
FROM node:25-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2 - production
|
||||
FROM node:25-slim
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install --global serve
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
CMD ["sh", "-c", "serve -s dist -l ${FRONTEND_CONTAINER_PORT}"]
|
||||
```
|
||||
|
||||
Un build Vite produit des fichiers statiques. Le support propose de générer un `config.json` au démarrage à partir des variables d'environnement, avant de servir `dist/`. Une même image peut ainsi recevoir plusieurs configurations runtime sans être reconstruite.
|
||||
|
||||
Pour PostgreSQL, aucun Dockerfile n'est nécessaire : l'image officielle `postgres:16-alpine` suffit.
|
||||
|
||||
## Fichier Compose de développement
|
||||
|
||||
```yaml
|
||||
name: mon-app-dev
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
networks:
|
||||
- app-network
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
ports:
|
||||
- "${NGINX_SECURE_HOST_PORT}:443"
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
## Ce que l'exemple met en œuvre
|
||||
|
||||
| Concept | Emplacement |
|
||||
| --- | --- |
|
||||
| Image existante | `postgres:16-alpine` et `nginx:1.27-alpine` |
|
||||
| Image custom | `build:` pour le backend et le frontend |
|
||||
| Build puis run | Compose construit les images et instancie les conteneurs |
|
||||
| Persistance | `postgres_data` conserve la base |
|
||||
| Hot-reload | `./backend:/app` et `./frontend:/app` |
|
||||
| Réseau | Tous les services partagent `app-network` |
|
||||
| DNS interne | Le backend utilise `POSTGRES_HOST=postgres` |
|
||||
| Ports | Seul Nginx publie un port |
|
||||
| Démarrage ordonné | Le backend attend le healthcheck PostgreSQL |
|
||||
|
||||
## Flux d'une requête
|
||||
|
||||
```text
|
||||
navigateur
|
||||
│
|
||||
▼
|
||||
port HTTPS publié par Nginx
|
||||
│
|
||||
├──► frontend React
|
||||
│
|
||||
└──► backend NestJS ──► PostgreSQL
|
||||
```
|
||||
|
||||
Un seul port est exposé. Tout le trafic externe entre par Nginx ; les autres communications restent dans le réseau privé Docker.
|
||||
|
||||
## Lancer la stack
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Conteneur vs machine virtuelle
|
||||
description: Deux formes d'isolation qui ne travaillent pas au même niveau.
|
||||
---
|
||||
|
||||
C'est la distinction fondamentale : une machine virtuelle et un conteneur isolent tous les deux une application, mais pas de la même manière.
|
||||
|
||||
## Machine virtuelle
|
||||
|
||||
Une VM virtualise le **matériel**. Un hyperviseur simule une machine physique complète sur laquelle on installe un système d'exploitation invité, noyau compris.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────┐
|
||||
│ App A │ App B │ App C │
|
||||
├─────────────┼─────────────┼──────────────┤
|
||||
│ Bins / libs │ Bins / libs │ Bins / libs │
|
||||
├─────────────┼─────────────┼──────────────┤
|
||||
│ OS invité │ OS invité │ OS invité │
|
||||
│ + noyau │ + noyau │ + noyau │
|
||||
├──────────────────────────────────────────┤
|
||||
│ Hyperviseur │
|
||||
├──────────────────────────────────────────┤
|
||||
│ OS hôte + matériel │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Chaque VM doit démarrer un OS complet. Cela demande plusieurs gigaoctets de stockage ou de mémoire et des dizaines de secondes de démarrage.
|
||||
|
||||
## Conteneur
|
||||
|
||||
Un conteneur ne virtualise ni le matériel ni le noyau. Il partage le noyau de l'hôte et isole la vue qu'a le processus du système.
|
||||
|
||||
Deux mécanismes du noyau Linux rendent cela possible :
|
||||
|
||||
- **namespaces** : chaque conteneur voit ses propres processus, son réseau et son système de fichiers ;
|
||||
- **cgroups** : le système peut limiter sa consommation de CPU et de mémoire.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────┐
|
||||
│ App A │ App B │ App C │
|
||||
├─────────────┼─────────────┼──────────────┤
|
||||
│ Bins / libs │ Bins / libs │ Bins / libs │
|
||||
├──────────────────────────────────────────┤
|
||||
│ Docker Engine │
|
||||
├──────────────────────────────────────────┤
|
||||
│ OS hôte - noyau partagé │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Le conteneur contient uniquement le userspace minimal nécessaire : binaires, bibliothèques et application. Il emprunte le noyau à l'hôte.
|
||||
|
||||
## Comparaison
|
||||
|
||||
| Critère | Machine virtuelle | Conteneur |
|
||||
| --- | --- | --- |
|
||||
| Niveau d'isolation | Matériel, avec noyau invité dédié | Processus, avec noyau partagé |
|
||||
| Poids | Plusieurs Go | Quelques Mo à quelques centaines de Mo |
|
||||
| Démarrage | Dizaines de secondes | Secondes ou moins |
|
||||
| Densité | Quelques VM par machine | Des dizaines ou centaines de conteneurs |
|
||||
| Frontière de sécurité | Plus forte | Plus légère, mais suffisante pour beaucoup d'usages |
|
||||
|
||||
<Callout title="Un conteneur n'est pas une mini-VM" type="warning">
|
||||
Dans l'usage, le résultat peut sembler proche. Techniquement, un conteneur reste un processus isolé. Sur Mac et Windows, tous les conteneurs partagent le noyau de la même VM Docker Desktop, au lieu d'utiliser une VM par service.
|
||||
</Callout>
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Docker Engine et Docker Desktop
|
||||
description: Distinguer le moteur Docker de l'application qui l'exécute sur Mac et Windows.
|
||||
---
|
||||
|
||||
## Docker Engine
|
||||
|
||||
Docker Engine est le cœur de Docker. Il comprend :
|
||||
|
||||
- le daemon `dockerd`, qui construit les images et fait tourner les conteneurs ;
|
||||
- la CLI `docker`, utilisée depuis le terminal pour parler au daemon.
|
||||
|
||||
Docker Engine est nativement Linux. Il s'appuie sur des fonctionnalités du noyau Linux, notamment les namespaces et les cgroups.
|
||||
|
||||
## Sur Mac et Windows
|
||||
|
||||
Le noyau requis étant un noyau Linux, Docker fait tourner une petite VM Linux en arrière-plan. Docker Engine vit dans cette VM et les conteneurs tournent dans ce Linux, pas directement dans macOS ou Windows.
|
||||
|
||||
Docker Desktop orchestre cet ensemble :
|
||||
|
||||
- il crée et maintient la VM Linux ;
|
||||
- il expose la commande `docker` dans le terminal hôte ;
|
||||
- il fournit une interface graphique ;
|
||||
- il gère le partage des fichiers et des ports entre l'hôte et la VM.
|
||||
|
||||
| Plateforme | Où tourne Docker Engine ? |
|
||||
| --- | --- |
|
||||
| Linux | Directement sur le système hôte |
|
||||
| macOS | Dans la VM Linux gérée par Docker Desktop |
|
||||
| Windows | Dans la VM Linux gérée par Docker Desktop |
|
||||
|
||||
## Conséquences pratiques
|
||||
|
||||
### Bind mounts sur Mac et Windows
|
||||
|
||||
Quand un dossier de l'hôte est monté dans un conteneur, les fichiers traversent la frontière entre l'hôte et la VM Linux. Les entrées-sorties peuvent être moins rapides qu'avec un accès Linux natif, notamment sur de gros dossiers comme `node_modules`.
|
||||
|
||||
### Volumes Docker
|
||||
|
||||
Un volume Docker vit dans le système de fichiers du Docker Engine. Sur Mac et Windows, il se trouve donc dans la VM Linux. Il reste rapide et persiste indépendamment du dossier du projet.
|
||||
|
||||
### Serveur Linux
|
||||
|
||||
Sur un VPS Linux de production, il n'y a pas cette VM intermédiaire : Docker Engine tourne directement sur l'OS. C'est le cas le plus simple et le plus efficace.
|
||||
|
||||
<Callout title="À retenir" type="info">
|
||||
Docker Desktop n'est pas le moteur lui-même. C'est l'application qui rend Docker Engine utilisable sur Mac et Windows en gérant une VM Linux discrète.
|
||||
</Callout>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Fondamentaux",
|
||||
"description": "Comprendre le rôle et le fonctionnement de Docker",
|
||||
"pages": ["pourquoi-docker", "engine-et-desktop", "conteneur-vs-vm"]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Pourquoi Docker ?
|
||||
description: Résoudre le problème « ça marche sur ma machine » avec un environnement reproductible.
|
||||
---
|
||||
|
||||
## Le problème : « ça marche sur ma machine »
|
||||
|
||||
Une application ne tourne jamais dans le vide. Elle dépend de tout un environnement :
|
||||
|
||||
- une version de runtime, par exemple Node.js 25 ou Python 3.12 ;
|
||||
- des binaires système, comme OpenSSL ou un client PostgreSQL ;
|
||||
- des services externes : PostgreSQL 16, un broker, un stockage S3 ;
|
||||
- une configuration : variables d'environnement, chemins et ports.
|
||||
|
||||
Sur une machine de développement, ce contexte existe parce qu'il a été installé au fil du temps. Sur la machine d'un collègue ou sur le serveur de production, il peut être différent ou absent. Le code identique fonctionne alors ici et casse ailleurs : le bug est dans l'environnement.
|
||||
|
||||
## La réponse apportée par Docker
|
||||
|
||||
Docker empaquette **l'application et son environnement** dans une unité reproductible : l'image.
|
||||
|
||||
La même image peut tourner sur le poste de développement, dans la CI et sur le serveur de production. L'environnement n'est plus installé manuellement sur chaque machine : il est livré avec le code.
|
||||
|
||||
## Les gains concrets
|
||||
|
||||
| Gain | Ce que cela apporte |
|
||||
| --- | --- |
|
||||
| Iso dev / prod | La stack locale est construite avec les mêmes briques que la production. |
|
||||
| Isolation | Chaque service possède ses dépendances. PostgreSQL 16 d'un projet ne gêne pas PostgreSQL 14 d'un autre. |
|
||||
| Agnostique OS | L'image embarque son userspace Linux ; l'application voit le même environnement sur Mac, Windows ou Linux. |
|
||||
| Reproductibilité | Un Dockerfile versionné constitue une recette de construction exacte et partageable. |
|
||||
| Légèreté | Un conteneur partage le noyau de l'hôte, démarre rapidement et consomme moins de mémoire qu'une VM complète. |
|
||||
| Onboarding | Un nouveau développeur clone le dépôt et lance toute la stack sans installer chaque service à la main. |
|
||||
|
||||
## Exemple d'onboarding
|
||||
|
||||
Une stack de développement complète peut regrouper PostgreSQL, MinIO, Mailpit, un frontend, un backend et Nginx dans un seul fichier Compose.
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up
|
||||
```
|
||||
|
||||
Cette commande remplace une longue liste d'installations manuelles et donne à toute l'équipe le même environnement.
|
||||
|
||||
<Callout title="Idée centrale" type="success">
|
||||
Docker déplace la complexité de l'installation de la machine vers une description versionnée : le Dockerfile pour une image, puis le fichier Compose pour toute la stack.
|
||||
</Callout>
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Du code au conteneur
|
||||
description: Le cycle code, Dockerfile, build, image, run et conteneur.
|
||||
---
|
||||
|
||||
Le cycle de vie complet suit toujours le même enchaînement :
|
||||
|
||||
```text
|
||||
code de l'application + Dockerfile
|
||||
│
|
||||
│ docker build
|
||||
▼
|
||||
image
|
||||
│
|
||||
│ docker run
|
||||
▼
|
||||
conteneur
|
||||
```
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Versionner le code et le Dockerfile
|
||||
|
||||
Le code et sa recette de construction vivent ensemble dans le dépôt.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Construire l'image
|
||||
|
||||
`docker build` lit le Dockerfile et exécute chaque instruction. Le résultat est une image figée et taguée.
|
||||
|
||||
```bash
|
||||
docker build -t mon-app/backend:1.4.0 .
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Instancier l'image
|
||||
|
||||
`docker run`, ou `docker compose up`, lance un conteneur qui exécute la commande de démarrage.
|
||||
|
||||
```bash
|
||||
docker run --rm mon-app/backend:1.4.0
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Build et run sont deux opérations différentes
|
||||
|
||||
Le **build** produit une image, normalement une fois par version. Le **run** lance autant de conteneurs que nécessaire à partir de cette image.
|
||||
|
||||
## L'image reste immuable
|
||||
|
||||
Un conteneur peut écrire dans son propre système de fichiers. Ces écritures disparaissent lorsque le conteneur est détruit, sauf lorsqu'elles sont stockées dans un volume.
|
||||
|
||||
L'image d'origine, elle, ne change jamais.
|
||||
|
||||
## Le rôle de CMD
|
||||
|
||||
`CMD` indique le processus principal lancé au démarrage :
|
||||
|
||||
```dockerfile
|
||||
CMD ["node", "dist/src/main"]
|
||||
```
|
||||
|
||||
Dans cet exemple, le conteneur est le processus Node. Si ce processus s'arrête, le conteneur s'arrête.
|
||||
|
||||
<Callout title="Conséquence" type="info">
|
||||
Une image constitue l'artefact à livrer. Le conteneur n'est que son instance en cours d'exécution, avec une configuration et un état temporaires.
|
||||
</Callout>
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Images Docker
|
||||
description: Utiliser une image existante ou construire une image adaptée à son application.
|
||||
---
|
||||
|
||||
## Une image est un template en lecture seule
|
||||
|
||||
Une image regroupe le userspace, l'application et sa configuration de démarrage. Elle est figée : on ne la modifie pas après sa construction, on l'instancie sous la forme d'un conteneur.
|
||||
|
||||
Une même image peut servir à lancer plusieurs conteneurs.
|
||||
|
||||
## Des couches partagées
|
||||
|
||||
Une image est composée de couches, ou *layers*. Chaque instruction de construction ajoute une couche. Docker peut mettre ces couches en cache et les partager entre plusieurs images.
|
||||
|
||||
Si dix images utilisent `node:25-slim` comme base, cette couche n'est téléchargée et stockée qu'une seule fois.
|
||||
|
||||
## Utiliser une image existante
|
||||
|
||||
Docker Hub et d'autres registries, comme GHCR, proposent des images prêtes à l'emploi. Pour un service standard, il suffit généralement d'utiliser l'image officielle :
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
|
||||
mailpit:
|
||||
image: axllent/mailpit:latest
|
||||
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
```
|
||||
|
||||
Le suffixe après les deux-points est le **tag**. Par exemple, `postgres:16-alpine` désigne PostgreSQL 16 sur une base Alpine Linux minimale.
|
||||
|
||||
<Callout title="Épingler les versions" type="warning">
|
||||
Préfère un tag précis comme 16-alpine ou 1.27-alpine à latest. Le résultat sera plus prévisible et les builds plus reproductibles.
|
||||
</Callout>
|
||||
|
||||
## Construire une image custom
|
||||
|
||||
Le code de ton frontend ou de ton backend est unique. Il faut donc écrire un Dockerfile qui part d'une image existante et ajoute l'application :
|
||||
|
||||
```dockerfile
|
||||
FROM node:25-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
CMD ["node", "dist/src/main"]
|
||||
```
|
||||
|
||||
L'instruction `FROM` exprime l'héritage : l'image de l'application réutilise `node:25-slim`, puis ajoute ses propres couches.
|
||||
|
||||
## Choisir entre image et build
|
||||
|
||||
| Besoin | Choix |
|
||||
| --- | --- |
|
||||
| Base de données ou proxy standard | Image officielle via `image:` |
|
||||
| Application développée par l'équipe | Image custom via un Dockerfile |
|
||||
| Outil tiers avec configuration légère | Image officielle + variables et volumes |
|
||||
| Runtime spécifique et code métier | Dockerfile custom |
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Images et builds",
|
||||
"description": "Construire des images reproductibles et efficaces",
|
||||
"pages": ["images-docker", "cycle-de-vie", "multi-stage-et-cache"]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Multi-stage et cache de build
|
||||
description: Accélérer les builds et réduire la taille de l'image finale.
|
||||
---
|
||||
|
||||
## Le modèle de couches
|
||||
|
||||
Chaque instruction d'un Dockerfile produit une couche. Au prochain build, Docker réutilise une couche mise en cache tant que ses entrées n'ont pas changé.
|
||||
|
||||
<Callout title="Règle du cache" type="warning">
|
||||
Le cache fonctionne comme un préfixe : dès qu'une couche est invalidée, toutes les couches suivantes doivent être reconstruites.
|
||||
</Callout>
|
||||
|
||||
## Ce qui invalide une couche
|
||||
|
||||
| Instruction | Le cache saute si… |
|
||||
| --- | --- |
|
||||
| `FROM node:25-slim` | Le digest de l'image de base change |
|
||||
| `COPY package.json ./` | Le contenu du fichier copié change |
|
||||
| `RUN npm ci` | La commande ou une couche précédente change |
|
||||
| `COPY . .` | N'importe quel fichier du contexte change |
|
||||
|
||||
`COPY` compare le contenu, pas seulement la date. Avec `COPY . .`, le moindre changement dans le contexte invalide cette couche.
|
||||
|
||||
## Ordonner du plus stable au plus volatil
|
||||
|
||||
```dockerfile
|
||||
FROM node:25-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Les manifestes changent rarement
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Le code change souvent
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
```
|
||||
|
||||
Si un fichier TypeScript change, les couches `COPY package.json` et `RUN npm ci` restent en cache. Seules la copie du code et la compilation sont rejouées.
|
||||
|
||||
À l'inverse, placer `COPY . .` avant `npm ci` provoquerait une réinstallation complète des dépendances à chaque changement de code.
|
||||
|
||||
## Stabiliser le contexte avec .dockerignore
|
||||
|
||||
```text
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
*.log
|
||||
```
|
||||
|
||||
Sans ce fichier, des dossiers locaux volumineux peuvent être envoyés au daemon et invalider le cache inutilement.
|
||||
|
||||
## Séparer la compilation du runtime
|
||||
|
||||
Un build multi-stage utilise plusieurs instructions `FROM`. Le premier stage contient les outils de compilation ; le second repart d'une image propre et ne récupère que le résultat.
|
||||
|
||||
```dockerfile
|
||||
# Stage 1 - builder
|
||||
FROM node:25-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2 - runtime
|
||||
FROM node:25-slim
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./
|
||||
|
||||
CMD ["node", "dist/src/main"]
|
||||
```
|
||||
|
||||
## Deux bénéfices distincts
|
||||
|
||||
| Bénéfice | Explication |
|
||||
| --- | --- |
|
||||
| Image finale plus petite | Les sources, les caches et l'outillage du builder ne sont pas copiés dans le runtime. |
|
||||
| Caches séparés | Le stage de build conserve son cache de dépendances ; le stage final ne change que si les artefacts changent. |
|
||||
|
||||
<Callout title="Aller plus loin" type="idea">
|
||||
Copier node_modules depuis le builder embarque aussi les devDependencies. Pour une image plus légère, le stage final peut exécuter un npm ci --omit=dev dédié.
|
||||
</Callout>
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Docker - Support de cours
|
||||
description: Comprendre les images, les conteneurs, Compose, le réseau, le déploiement et les tests.
|
||||
---
|
||||
|
||||
Ce cours explique pourquoi Docker est utile, comment il fonctionne et comment l'utiliser sur une application complète. Le contenu du support PDF a été réorganisé en chapitres courts pour faciliter la lecture et la recherche.
|
||||
|
||||
<Callout title="Parcours conseillé" type="info">
|
||||
Lis les chapitres dans l'ordre lors d'une première découverte. Les liens « Page précédente » et « Page suivante » permettent de suivre le cours sans revenir au sommaire.
|
||||
</Callout>
|
||||
|
||||
## Objectifs du cours
|
||||
|
||||
À la fin du parcours, tu sauras :
|
||||
|
||||
- expliquer la différence entre une image, un conteneur et une machine virtuelle ;
|
||||
- construire une image reproductible avec un Dockerfile ;
|
||||
- optimiser le cache de build et utiliser un build multi-stage ;
|
||||
- orchestrer plusieurs services avec Docker Compose ;
|
||||
- gérer la persistance, les réseaux et les ports ;
|
||||
- déployer une image construite par une CI/CD ;
|
||||
- lancer une infrastructure de test isolée avec Testcontainers.
|
||||
|
||||
## Plan
|
||||
|
||||
<Cards>
|
||||
<Card title="Fondamentaux" href="/docs/fondamentaux/pourquoi-docker">
|
||||
Pourquoi Docker, Engine et Desktop, conteneurs et machines virtuelles.
|
||||
</Card>
|
||||
<Card title="Images et builds" href="/docs/images-et-builds/images-docker">
|
||||
Images existantes ou custom, cycle de vie, cache et multi-stage.
|
||||
</Card>
|
||||
<Card title="Orchestration" href="/docs/orchestration/docker-compose">
|
||||
Compose, volumes, réseaux Docker et mapping de ports.
|
||||
</Card>
|
||||
<Card title="Cas pratique" href="/docs/cas-pratique/react-nest-postgresql">
|
||||
Une stack React, NestJS, PostgreSQL et Nginx de bout en bout.
|
||||
</Card>
|
||||
<Card title="Production" href="/docs/production/deploiement">
|
||||
Construire une fois, publier dans un registry et exécuter partout.
|
||||
</Card>
|
||||
<Card title="Tests" href="/docs/tests/testcontainers">
|
||||
Infrastructure réelle, isolée et jetable pour les tests e2e.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## Le fil conducteur
|
||||
|
||||
**Code et Dockerfile** → docker build → **image** → docker run ou docker compose up → **conteneur**
|
||||
|
||||
Une image est un artefact figé et reproductible. Un conteneur est une instance vivante de cette image. Docker Compose permet ensuite de lancer et relier plusieurs conteneurs comme une seule application.
|
||||
|
||||
## Accès rapide
|
||||
|
||||
Si tu connais déjà les bases, tu peux aller directement au [cas pratique complet](/docs/cas-pratique/react-nest-postgresql) ou au [récapitulatif](/docs/recapitulatif).
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Cours Docker",
|
||||
"pages": [
|
||||
"index",
|
||||
"fondamentaux",
|
||||
"images-et-builds",
|
||||
"orchestration",
|
||||
"cas-pratique",
|
||||
"production",
|
||||
"tests",
|
||||
"recapitulatif"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Docker Compose
|
||||
description: Décrire et lancer une application composée de plusieurs services.
|
||||
---
|
||||
|
||||
## Le problème résolu par Compose
|
||||
|
||||
`docker run` lance un conteneur. Une application réelle utilise souvent plusieurs services : frontend, backend, base de données, stockage, serveur de mail et reverse proxy.
|
||||
|
||||
Lancer chaque conteneur à la main, dans le bon ordre et avec les bons réseaux, ports et variables, devient vite ingérable.
|
||||
|
||||
Docker Compose décrit toute la stack dans un fichier YAML déclaratif et la pilote avec une seule commande :
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
```
|
||||
|
||||
## Anatomie d'un fichier Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.dev
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
POSTGRES_HOST: postgres
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
## Les notions principales
|
||||
|
||||
### services
|
||||
|
||||
Chaque clé représente un service. Il utilise soit `image:` pour récupérer une image existante, soit `build:` pour construire une image depuis un Dockerfile.
|
||||
|
||||
### depends_on et healthcheck
|
||||
|
||||
`depends_on` organise le démarrage. Avec `condition: service_healthy`, le backend n'attend pas seulement que le conteneur PostgreSQL existe : il attend que la base soit prête à accepter des connexions.
|
||||
|
||||
### environment
|
||||
|
||||
Les variables configurent le conteneur. La syntaxe `${VAR}` lit généralement la valeur depuis un fichier `.env`.
|
||||
|
||||
### networks et volumes
|
||||
|
||||
Les réseaux et volumes sont déclarés une fois, puis associés aux services qui en ont besoin.
|
||||
|
||||
## Superposer plusieurs fichiers
|
||||
|
||||
Un projet peut séparer les usages :
|
||||
|
||||
| Fichier | Usage |
|
||||
| --- | --- |
|
||||
| `docker-compose.dev.yml` | Développement avec hot-reload et bind mounts |
|
||||
| `docker-compose.prod.yml` | Production avec images préconstruites et politique de redémarrage |
|
||||
| `docker-compose.local-e2e.yml` | Surcharge pour les tests e2e |
|
||||
|
||||
Le second fichier surcharge le premier :
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
-f docker-compose.prod.yml \
|
||||
-f docker-compose.local-e2e.yml \
|
||||
up
|
||||
```
|
||||
|
||||
<Callout title="Compose orchestre, il ne remplace pas les images" type="info">
|
||||
Le Dockerfile décrit comment construire une image. Le fichier Compose décrit comment lancer, configurer et relier plusieurs instances d'images.
|
||||
</Callout>
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Mapping de ports
|
||||
description: Publier un port du conteneur vers l'hôte sans exposer toute la stack.
|
||||
---
|
||||
|
||||
## Réseau interne et monde extérieur
|
||||
|
||||
Le réseau bridge est privé. Les services communiquent entre eux, mais le navigateur ou le terminal de l'hôte ne peut pas les joindre tant qu'aucun port n'est publié.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
ports:
|
||||
- "8080:80"
|
||||
- "8443:443"
|
||||
```
|
||||
|
||||
La syntaxe est :
|
||||
|
||||
```text
|
||||
PORT_HÔTE:PORT_CONTENEUR
|
||||
```
|
||||
|
||||
`8443:443` redirige le port 8443 de la machine vers le port 443 écouté par Nginx dans le conteneur.
|
||||
|
||||
## Exposer uniquement le point d'entrée
|
||||
|
||||
Dans une stack classique, PostgreSQL, le backend et le frontend n'ont pas forcément de section `ports:`. Ils restent accessibles depuis le réseau Docker, mais pas depuis l'extérieur.
|
||||
|
||||
Seul Nginx publie un port. Il devient le point d'entrée et route ensuite les requêtes vers les services internes.
|
||||
|
||||
```text
|
||||
navigateur
|
||||
│
|
||||
▼
|
||||
port publié de Nginx
|
||||
│
|
||||
├──► frontend
|
||||
└──► backend ──► postgres
|
||||
```
|
||||
|
||||
## Éviter les collisions
|
||||
|
||||
Le port interne d'un service reste le même, tandis que le port hôte peut changer :
|
||||
|
||||
```yaml
|
||||
# Stack A
|
||||
ports:
|
||||
- "8443:443"
|
||||
|
||||
# Stack B
|
||||
ports:
|
||||
- "8444:443"
|
||||
```
|
||||
|
||||
Ce principe permet à plusieurs workers de test de lancer la même stack sur des plages de ports différentes.
|
||||
|
||||
## Restreindre une publication à la machine
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
ports:
|
||||
- "127.0.0.1:15432:5432"
|
||||
```
|
||||
|
||||
Le préfixe `127.0.0.1` rend le port accessible depuis le serveur pour la maintenance, sans l'ouvrir sur l'adresse publique.
|
||||
|
||||
<Callout title="Principe de sécurité" type="warning">
|
||||
Ne publie pas une base de données sur toutes les interfaces sans nécessité. Sans le préfixe 127.0.0.1, un mapping comme 5432:5432 peut exposer PostgreSQL sur Internet si le pare-feu le permet.
|
||||
</Callout>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Orchestration",
|
||||
"description": "Faire fonctionner plusieurs services ensemble",
|
||||
"pages": ["docker-compose", "volumes", "reseaux", "mapping-de-ports"]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Réseaux Docker
|
||||
description: Relier les services avec un réseau privé et la résolution DNS interne.
|
||||
---
|
||||
|
||||
## Des conteneurs isolés
|
||||
|
||||
Chaque conteneur possède sa propre pile réseau. Pour que le backend puisse joindre PostgreSQL et que Nginx puisse joindre le frontend, leurs conteneurs doivent partager un réseau.
|
||||
|
||||
## Réseau bridge et noms de services
|
||||
|
||||
Compose crée un réseau et y attache les services. Sur ce réseau, Docker fournit un DNS interne : chaque service est joignable avec son nom.
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
postgres:
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
networks:
|
||||
- app-network
|
||||
environment:
|
||||
POSTGRES_HOST: postgres
|
||||
MINIO_HOST: minio
|
||||
SMTP_HOST: mailpit
|
||||
```
|
||||
|
||||
Le backend se connecte à la base avec l'hôte `postgres`. Docker résout ce nom vers l'adresse interne du conteneur correspondant.
|
||||
|
||||
Les adresses IP peuvent changer à chaque redémarrage ; les noms de services restent stables.
|
||||
|
||||
<Callout title="localhost reste local" type="warning">
|
||||
À l'intérieur d'un conteneur, localhost désigne ce même conteneur. Pour joindre un autre service, utilise son nom Compose, jamais localhost.
|
||||
</Callout>
|
||||
|
||||
## Isolation entre les stacks
|
||||
|
||||
Deux projets Compose distincts créent chacun leur réseau bridge. Leurs services ne se voient pas par défaut.
|
||||
|
||||
Cette isolation permet notamment de lancer plusieurs stacks de tests en parallèle sans qu'elles communiquent entre elles.
|
||||
|
||||
## Segmenter l'application
|
||||
|
||||
Un service peut appartenir à plusieurs réseaux. On peut ainsi exposer Nginx au réseau frontal tout en gardant PostgreSQL uniquement sur un réseau interne :
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
public:
|
||||
private:
|
||||
|
||||
services:
|
||||
nginx:
|
||||
networks:
|
||||
- public
|
||||
- private
|
||||
|
||||
backend:
|
||||
networks:
|
||||
- private
|
||||
|
||||
postgres:
|
||||
networks:
|
||||
- private
|
||||
```
|
||||
|
||||
Seuls les services partageant un réseau peuvent communiquer directement.
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Volumes
|
||||
description: Conserver les données indépendamment du cycle de vie des conteneurs.
|
||||
---
|
||||
|
||||
## Un conteneur est éphémère
|
||||
|
||||
Le système de fichiers propre à un conteneur est jetable. Les écritures disparaissent lorsque le conteneur est détruit ou recréé. Pour une base de données, ce comportement serait inacceptable.
|
||||
|
||||
Un volume est un stockage persistant géré par Docker et découplé du conteneur : le conteneur va et vient, le volume reste.
|
||||
|
||||
## Volume nommé
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
Cette configuration monte le volume `postgres_data` sur `/var/lib/postgresql/data`, l'emplacement où PostgreSQL écrit ses données.
|
||||
|
||||
Les données survivent à un `down` puis un `up`, tant que le volume n'est pas supprimé.
|
||||
|
||||
<Callout title="Attention à down -v" type="warning">
|
||||
docker compose down -v supprime aussi les volumes de la stack. Sur une base contenant des données importantes, cette action est destructive.
|
||||
</Callout>
|
||||
|
||||
## Où vit un volume ?
|
||||
|
||||
Le volume est stocké dans le système de fichiers du Docker Engine :
|
||||
|
||||
- directement sur l'hôte avec Linux ;
|
||||
- dans la VM Linux Docker Desktop avec Mac ou Windows.
|
||||
|
||||
Ce n'est pas un dossier du dépôt. Docker en gère l'emplacement et le cycle de vie.
|
||||
|
||||
## Bind mount : monter un dossier de l'hôte
|
||||
|
||||
En développement, on veut voir les modifications de code immédiatement dans le conteneur :
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- /app/dist
|
||||
```
|
||||
|
||||
### Le code local
|
||||
|
||||
`./backend:/app` monte le dossier du projet dans le conteneur. Quand un fichier est modifié sur l'hôte, le serveur dans le conteneur le voit et peut redémarrer en hot-reload.
|
||||
|
||||
### Les dépendances du conteneur
|
||||
|
||||
Le bind mount masquerait le dossier `node_modules` installé dans l'image. Le volume anonyme `/app/node_modules` le protège des fichiers absents ou incompatibles de l'hôte.
|
||||
|
||||
## Développement ou production
|
||||
|
||||
| Contexte | Stockage recommandé |
|
||||
| --- | --- |
|
||||
| Code en développement | Bind mount pour le hot-reload |
|
||||
| Dépendances internes | Volume anonyme si le bind mount les masque |
|
||||
| Données PostgreSQL | Volume nommé |
|
||||
| Code en production | Dans l'image, sans bind mount |
|
||||
| Données persistantes en production | Volume nommé et sauvegardé |
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Déploiement avec Docker
|
||||
description: Construire les images, les publier puis les exécuter sur le serveur.
|
||||
---
|
||||
|
||||
Une fois l'application dockerisée, le déploiement suit une chaîne simple :
|
||||
|
||||
```text
|
||||
build des images
|
||||
↓
|
||||
push vers un registry
|
||||
↓
|
||||
pull sur le serveur
|
||||
↓
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Une CI/CD, par exemple GitHub Actions, automatise cette chaîne.
|
||||
|
||||
## CI : garder la branche saine
|
||||
|
||||
Sur chaque push vers la branche principale :
|
||||
|
||||
| Job | Contenu |
|
||||
| --- | --- |
|
||||
| `backend-checks` | lint, build, tests unitaires et tests e2e |
|
||||
| `frontend-checks` | lint, build et tests |
|
||||
| `build-images` | construction des images avec le fichier Compose de production |
|
||||
|
||||
Le job de build vérifie que les Dockerfiles continuent de produire des images valides.
|
||||
|
||||
## CD : publier une version
|
||||
|
||||
Lorsqu'un tag de version est poussé :
|
||||
|
||||
| Job | Contenu |
|
||||
| --- | --- |
|
||||
| Vérifications | Les mêmes contrôles que la CI |
|
||||
| `build-and-push` | Build des images et push vers Docker Hub, GHCR ou un autre registry |
|
||||
| `deploy` | Copie de la configuration puis connexion SSH au serveur |
|
||||
|
||||
## Build une fois, run partout
|
||||
|
||||
Le serveur ne recompile rien. Il récupère exactement l'image construite et testée par la CI :
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d --no-build
|
||||
docker image prune -f
|
||||
```
|
||||
|
||||
`--no-build` garantit que le serveur lance les images récupérées sans reconstruire depuis le code source.
|
||||
|
||||
## image et build dans le Compose de production
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
image: registry.example.com/mon-app/backend:${IMAGE_TAG:-latest}
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
```
|
||||
|
||||
Dans la CI, `build:` sert à construire puis pousser l'image. En production, le serveur utilise `image:` et `--no-build` pour exécuter le tag déjà publié.
|
||||
|
||||
Le serveur n'a besoin ni du code source, ni de Node.js, ni du toolchain de compilation. Il lui faut seulement Docker, le fichier Compose et un accès au registry.
|
||||
|
||||
## Ce que Docker apporte
|
||||
|
||||
- **Reproductibilité** : l'image testée par la CI est celle qui tourne en production.
|
||||
- **Rollback simple** : revenir en arrière revient à redéployer le tag précédent.
|
||||
- **Serveur minimal** : toutes les versions et dépendances vivent dans les images.
|
||||
- **Immutabilité** : on remplace une image complète au lieu de modifier le serveur en place.
|
||||
|
||||
<Callout title="Principe de livraison" type="success">
|
||||
Construis une fois et exécute partout. Une image versionnée est l'unité de livraison ; le tag identifie précisément la version déployée.
|
||||
</Callout>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Production",
|
||||
"description": "Livrer et exécuter les images",
|
||||
"pages": ["deploiement"]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Récapitulatif
|
||||
description: Toutes les notions du cours Docker en une page.
|
||||
---
|
||||
|
||||
| Notion | En une phrase |
|
||||
| --- | --- |
|
||||
| Docker | Empaquette l'application et son environnement dans une image reproductible. |
|
||||
| Engine / Desktop | Engine est le daemon Linux ; Desktop le fait tourner dans une VM Linux sur Mac et Windows. |
|
||||
| Conteneur / VM | Une VM possède un noyau invité ; un conteneur partage le noyau de l'hôte. |
|
||||
| Image | Template figé en lecture seule, composé de couches, existant ou construit par un Dockerfile. |
|
||||
| Cycle | Code + Dockerfile → build → image → run → conteneur. |
|
||||
| Compose | Décrit et orchestre une stack multi-services dans un YAML déclaratif. |
|
||||
| Volume | Stockage persistant découplé du conteneur et géré par Docker. |
|
||||
| Réseau | Réseau bridge privé où les services se joignent par leur nom. |
|
||||
| Mapping de ports | Publie un port du conteneur vers l'hôte ; le reste demeure interne. |
|
||||
| Déploiement | Build une fois, push dans un registry, puis pull et run sur le serveur. |
|
||||
| Testcontainers | Infrastructure réelle, isolée et jetable, démarrée par les tests. |
|
||||
|
||||
## Les commandes essentielles
|
||||
|
||||
```bash
|
||||
# Construire une image
|
||||
docker build -t mon-app:1.0.0 .
|
||||
|
||||
# Lancer un conteneur
|
||||
docker run --rm mon-app:1.0.0
|
||||
|
||||
# Démarrer une stack Compose
|
||||
docker compose up -d --build
|
||||
|
||||
# Voir les conteneurs
|
||||
docker compose ps
|
||||
|
||||
# Suivre les journaux
|
||||
docker compose logs --follow
|
||||
|
||||
# Arrêter la stack sans supprimer les volumes
|
||||
docker compose down
|
||||
|
||||
# Récupérer et lancer les images de production
|
||||
docker compose pull
|
||||
docker compose up -d --no-build
|
||||
```
|
||||
|
||||
## Les règles à retenir
|
||||
|
||||
1. Épingle les versions des images au lieu de dépendre de `latest`.
|
||||
2. Ordonne le Dockerfile du plus stable au plus volatil.
|
||||
3. Utilise un `.dockerignore` pour garder un contexte de build propre.
|
||||
4. Sépare le builder et le runtime avec un build multi-stage.
|
||||
5. Stocke les données persistantes dans des volumes sauvegardés.
|
||||
6. Utilise les noms de services, pas `localhost`, entre conteneurs.
|
||||
7. Ne publie que les ports réellement nécessaires.
|
||||
8. Construis l'image dans la CI et exécute cette même image en production.
|
||||
9. Préfère une infrastructure de test réelle et isolée lorsque l'intégration compte.
|
||||
|
||||
<Callout title="Fin du cours" type="success">
|
||||
Tu peux maintenant revenir au [cas pratique](/docs/cas-pratique/react-nest-postgresql) pour relire l'ensemble des concepts dans une seule stack.
|
||||
</Callout>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Tests",
|
||||
"description": "Lancer une infrastructure de test réelle et jetable",
|
||||
"pages": ["testcontainers"]
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: Tests e2e avec Testcontainers
|
||||
description: Tester contre une infrastructure réelle, isolée et détruite après chaque run.
|
||||
---
|
||||
|
||||
Docker permet aux tests de démarrer une infrastructure proche de la production, de l'utiliser, puis de la supprimer.
|
||||
|
||||
## Le problème des tests d'intégration
|
||||
|
||||
Un backend qui utilise PostgreSQL, un stockage S3 et un serveur SMTP a besoin de ces dépendances pendant ses tests.
|
||||
|
||||
Deux solutions classiques ont des limites :
|
||||
|
||||
- **mocker les services** : rapide, mais ne teste pas le vrai SQL ni les vraies contraintes ;
|
||||
- **partager une base de test** : réaliste, mais l'état fuit entre les runs et les exécutions parallèles entrent en collision.
|
||||
|
||||
Testcontainers donne à chaque suite ses propres conteneurs : une vraie base PostgreSQL, un vrai MinIO et un vrai Mailpit, isolés puis supprimés automatiquement.
|
||||
|
||||
## Niveau backend : Testcontainers en code
|
||||
|
||||
```ts
|
||||
const [pgContainer, mailpitContainer, minioContainer] =
|
||||
await Promise.all([
|
||||
new PostgreSqlContainer("postgres:16-alpine")
|
||||
.withStartupTimeout(30_000)
|
||||
.start(),
|
||||
|
||||
new GenericContainer("axllent/mailpit:latest")
|
||||
.withExposedPorts(1025, 8025)
|
||||
.start(),
|
||||
|
||||
new GenericContainer("minio/minio:latest")
|
||||
.withCommand(["server", "/data"])
|
||||
.withEnvironment({
|
||||
MINIO_ROOT_USER: "minioadmin",
|
||||
MINIO_ROOT_PASSWORD: "minioadmin123",
|
||||
})
|
||||
.withExposedPorts(9000)
|
||||
.start(),
|
||||
]);
|
||||
|
||||
process.env.POSTGRES_HOST = pgContainer.getHost();
|
||||
process.env.POSTGRES_PORT = pgContainer.getPort().toString();
|
||||
process.env.POSTGRES_USER = pgContainer.getUsername();
|
||||
```
|
||||
|
||||
L'application NestJS démarre ensuite contre ces services réels.
|
||||
|
||||
### Même image que la production
|
||||
|
||||
Le test utilise `postgres:16-alpine`, comme la production. Il teste le vrai moteur et ses contraintes, pas une base en mémoire approximative.
|
||||
|
||||
### Ports dynamiques
|
||||
|
||||
Testcontainers publie les ports internes sur des ports libres choisis automatiquement. `getPort()` renvoie le port attribué. Deux runs simultanés ne se gênent pas.
|
||||
|
||||
### Isolation par run
|
||||
|
||||
Les données peuvent être nettoyées entre les tests, puis les conteneurs sont arrêtés à la fin. Chaque suite repart d'une infrastructure vierge.
|
||||
|
||||
## Le test reste orienté métier
|
||||
|
||||
```ts
|
||||
test("mes notifications sont triées par date décroissante", async () => {
|
||||
await driver.givenImAuthenticated();
|
||||
await driver.givenIHaveNotifications();
|
||||
await driver.whenIListMyNotifications();
|
||||
await driver.thenMyNotificationsAreReturnedSortedDesc();
|
||||
});
|
||||
```
|
||||
|
||||
Le test pilote l'application par HTTP comme un vrai client. L'infrastructure Docker reste cachée dans le setup.
|
||||
|
||||
## Niveau navigateur : une stack Compose par worker
|
||||
|
||||
Les tests Playwright peuvent lancer la stack de production complète, accompagnée d'un overlay e2e :
|
||||
|
||||
```ts
|
||||
const ports = getWorkerPorts(workerInfo.workerIndex);
|
||||
|
||||
execSync(
|
||||
"docker compose " +
|
||||
"--project-name " + ports.projectName + " " +
|
||||
"--env-file .env.prod " +
|
||||
"-f docker-compose.prod.yml " +
|
||||
"-f docker-compose.local-e2e.yml " +
|
||||
"up -d --build --force-recreate",
|
||||
{ env: envOverride },
|
||||
);
|
||||
|
||||
await use(ports);
|
||||
|
||||
execSync(
|
||||
"docker compose --project-name " +
|
||||
ports.projectName +
|
||||
" down -v",
|
||||
);
|
||||
```
|
||||
|
||||
Trois notions rendent le parallélisme possible :
|
||||
|
||||
1. un `--project-name` unique donne à chaque worker son propre réseau ;
|
||||
2. des ports hôte décalés évitent les collisions ;
|
||||
3. l'overlay Compose réutilise la stack de production et ne modifie que la configuration de test.
|
||||
|
||||
Plusieurs copies de la production peuvent ainsi tourner côte à côte dans la CI, puis être détruites avec leurs volumes.
|
||||
|
||||
## Comparaison
|
||||
|
||||
| Critère | Mocks | Base partagée | Testcontainers |
|
||||
| --- | --- | --- | --- |
|
||||
| Vrai SQL et vraies contraintes | Non | Oui | Oui |
|
||||
| Proche de la production | Non | Partiellement | Oui, mêmes images |
|
||||
| Isolation entre les runs | Oui | Non | Oui |
|
||||
| Setup manuel | Non | Oui | Non, décrit en code ou Compose |
|
||||
| Parallélisable | Oui | Difficile | Oui, grâce aux ports et réseaux |
|
||||
|
||||
<Callout title="Pourquoi le compromis fonctionne" type="info">
|
||||
La légèreté des conteneurs permet de démarrer l'infrastructure en quelques secondes. Les réseaux l'isolent, les ports dynamiques évitent les collisions et down -v la détruit proprement.
|
||||
</Callout>
|
||||
Reference in New Issue
Block a user