🐳 Module Docker Avancé

Maîtriser la persistance, les réseaux
et les multi-stage builds

Volumes · Réseaux · Images de production optimisées

Utilisez les flèches, cliquez ou glissez pour naviguer

Objectifs de la leçon

1. Comprendre les types de montage Docker

Volumes nommés, bind mounts, tmpfs

2. Gérer la persistance des données

Volumes nommés pour les bases de données

3. Comprendre les réseaux Docker

Bridge, host, none — isolation et communication

4. Écrire des multi-stage builds

Séparer le build du runtime

5. Comparer les tailles d'images

Avant/après optimisation multi-stage

Plan du cours

1

Rappel : données éphémères

Pourquoi les données disparaissent avec le conteneur

2

Les 3 types de montage

Volumes nommés, bind mounts, tmpfs

3

Réseaux Docker

Bridge (défaut), host, none — isolation et communication

4

Multi-stage builds

Build séparé du runtime pour des images minimales

5

Live coding

Transformer le Dockerfile en multi-stage build + comparer les tailles

1. Le problème des données éphémères

Un conteneur qui s'arrête = des données perdues ?

Données éphémères par défaut

Chaque conteneur démarre avec un filesystem vierge

# On crée un fichier dans le conteneur

docker run --name demo alpine sh -c "echo 'Données importantes' > /data/mon-fichier.txt"

# On supprime le conteneur

docker rm demo

# On relance un nouveau conteneur — les données ont disparu !

docker run alpine cat /data/mon-fichier.txt

cat: /data/mon-fichier.txt: No such file or directory

❌ Le filesystem du conteneur est éphémère

Quand le conteneur meurt, ses données meurent avec lui

Pourquoi c'est problématique ?

🔄 Redémarrage du conteneur

PostgreSQL redémarre → toutes les tables sont vides

📦 Mise à jour de l'image

Nouveau déploiement → nouvelle image → anciennes données perdues

☠️ Crash du conteneur

Le conteneur plante → impossible de récupérer les logs ou les données

✅ La solution : monter un volume externe dans le conteneur

Les données vivent en dehors du conteneur — il ne fait que les utiliser

2. Les 3 types de montage

Volumes nommés · Bind mounts · tmpfs

Les 3 types de montage Docker

📦 Volume nommé

Géré par Docker dans /var/lib/docker/volumes/

✅ Production (DB)

📁 Bind mount

Monte un dossier ou fichier de l'hôte

✅ Développement (hot-reload)

⚡ tmpfs

Stockage en mémoire RAM (temporaire)

✅ Secrets temporaires

# Syntaxe -v (ancienne) ou --mount (moderne)

docker run -v mon_volume:/chemin image

📦 Volume nommé

Le choix n°1 pour les données de production

docker volume create pgdata

docker run -d \

--name postgres \

-v pgdata:/var/lib/postgresql/data \

postgres:16

Géré par Docker

Création, sauvegarde, restauration via CLI

Persistant

Survit à la suppression du conteneur

Portable

Fonctionne sur tout OS (Linux, Mac, Windows)

💡 docker volume ls pour lister | docker volume inspect pgdata pour le chemin

📁 Bind mount

Monte un dossier ou fichier de l'hôte dans le conteneur

docker run -d \

--name mon-app \

-v $(pwd):/app \

node:20 node server.js

✅ Avantages

  • Hot-reload : modifiez le code sur l'hôte → conteneur mis à jour
  • Partage de fichiers entre hôte et conteneur
  • Idéal pour le développement

⚠️ Attention

  • Bidirectionnel : modifications des deux côtés
  • npm install dans le conteneur modifie node_modules sur l'hôte
  • Dépend du système de fichiers de l'hôte

⚡ tmpfs — Stockage en mémoire

Données temporaires, volatiles, ultra-rapides

docker run -d \

--name app \

--tmpfs /tmp:noexec,nosuid,size=64m \

nginx

Cas d'usage

  • Fichiers temporaires (sessions, caches)
  • Secrets volatils
  • Fichiers de lock

Limitations

  • Perdu quand le conteneur s'arrête
  • Limité par la RAM disponible
  • Pas de partage entre conteneurs

💡 tmpfs = RAM = ultra-rapide, mais volatil. Parfait pour les caches Redis qui peuvent être reconstruits

Comparaison des 3 types

Critère Volume nommé Bind mount tmpfs
Localisation /var/lib/docker/volumes/ Dossier hôte (choisi par l'utilisateur) RAM (mémoire vive)
Persistance ✅ Oui ✅ Oui ❌ Non
Performance ⭐⭐⭐ ⭐⭐⭐ (selon OS hôte) ⭐⭐⭐⭐⭐
Usage idéal Production (DB, fichiers) Développement (hot-reload) Caches, secrets temporaires

3. Les réseaux Docker

Isolation et communication entre conteneurs

Les 3 réseaux par défaut

🌉 bridge

Réseau privé par défaut

Les conteneurs communiquent entre eux via IP

docker run (par défaut)

🖥️ host

Partage le réseau de l'hôte

Pas d'isolation réseau — performances max

docker run --network host

🚫 none

Pas de réseau

Isolation totale — pas d'accès entrant/sortant

docker run --network none

🔗 Les conteneurs sur le même bridge peuvent communiquer

Ceux sur des réseaux différents sont isolés

🌉 Réseau bridge — Communication entre conteneurs

# Créer un réseau personnalisé

docker network create --driver bridge mon-reseau

# Lancer des conteneurs sur ce réseau

docker run -d --name db --network mon-reseau postgres:16

docker run -d --name app --network mon-reseau -p 3000:3000 mon-app

# Le backend peut accéder à la base via "db:5432"

// Docker DNS résout automatiquement le nom du conteneur

DNS intégré

Les conteneurs se découvrent par leur nom

Isolation

Un réseau = un groupe isolé d'autres réseaux

⚠️ Les conteneurs sur des réseaux différents ne peuvent PAS communiquer — ils sont isolés par défaut !

Docker Compose crée son propre réseau

Chaque docker compose up crée un réseau isolé pour la stack

🔗 Les services communiquent par nom de service

Dans votre code, utilisez db:5432 et non localhost:5432

# docker-compose.yml — réseau automatique

services:

backend:

build: .

ports: - "3000:3000"

db:

image: postgres:16

volumes: - pgdata:/var/lib/postgresql/data

# Dans le .env : DATABASE_URL=postgres://user:pass@db:5432/mydb

host et none — Cas particuliers

🖥️ host

Le conteneur partage la pile réseau de l'hôte

docker run --network host nginx

# accessible sur localhost:80 directement

⚠️ Pas d'isolation — conflit de ports possible

🚫 none

Aucun accès réseau (loopback uniquement)

docker run --network none alpine

# Pas d'internet, pas de communication

🔒 Idéal pour les traitements sensibles isolés

💡 99% du temps, vous utilisez le bridge par défaut ou un réseau personnalisé

4. Multi-stage builds

Séparer le build du runtime
pour des images de production minimales

Pourquoi multi-stage ?

❌ Problème

Les outils de build (compilateurs, devDependencies) gonflent l'image de production

# Dockerfile simple — tout dans une image

FROM node:20

RUN npm install

RUN npm run build

# Image finale = 1 Go+ (TypeScript, node_modules, devDeps...)

✅ Solution

Compilez dans une image "builder", puis copiez SEULEMENT le résultat dans l'image finale

# Multi-stage — image finale = 150 Mo

# Stage 1 : builder avec outils de build

# Stage 2 : runtime minimal + artefact

📉 Objectif : réduire la taille de l'image de 1 Go → ~150 Mo

Syntaxe d'un multi-stage build

# Stage 1 : BUILD — on compile avec tous les outils

FROM node:20 AS builder

WORKDIR /app

COPY package.json package-lock.json .

RUN npm ci

COPY . .

RUN npm run build

# Stage 2 : RUNTIME — image légère, juste le nécessaire

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/dist /app

COPY --from=builder /app/node_modules /app/node_modules

EXPOSE 3000

CMD ["node", "server.js"]

📌 AS builder nomme le stage | --from=builder copie depuis un stage précédent

Comment ça marche ?

$ docker build -t mon-app:multi-stage .

✔ [builder 1/5] FROM node:20

✔ [builder 2/5] COPY package.json .

✔ [builder 3/5] RUN npm ci

✔ [builder 4/5] COPY . .

✔ [builder 5/5] RUN npm run build

► [runner 1/3] FROM node:20-alpine

► [runner 2/3] COPY --from=builder /app/dist /app

► [runner 3/3] COPY --from=builder /app/node_modules /app

✔ Image finale : 152 Mo

Les étapes clés :

  • Stage 1 (builder) : utilise node:20 complet (1 Go) pour compiler
  • Stage 2 (runner) : utilise node:20-alpine (135 Mo) comme base
  • Seul le dernier stage constitue l'image finale
  • Les stages intermédiaires sont jetés après le build

COPY --from — Copier entre stages

L'instruction magique qui rend le multi-stage possible

COPY --from=builder /app/dist /app

# Copie depuis un stage nommé "builder"

# Source = /app/dist dans le builder

# Destination = /app dans le runner

✅ Ce qu'on copie

  • Le code compilé (dist/, build/)
  • Les dépendances de production (npm ci --only=production)
  • Les assets statiques

⚠️ Ce qu'on NE copie PAS

  • Les devDependencies (TypeScript, ESLint, tests)
  • Les outils de build (webpack, tsc, vite)
  • Les caches npm, .git, fichiers sources .ts

⚠️ Oublier de copier un fichier nécessaire → l'image finale plante au runtime !

5. 🔴 Live Coding

Transformer le Dockerfile en multi-stage build

Avant : Dockerfile naïf

FROM node:20

WORKDIR /app

COPY package.json package-lock.json .

RUN npm ci

COPY . .

RUN npm run build

ENV NODE_ENV=production

EXPOSE 3000

CMD ["node", "dist/server.js"]

❌ Problèmes :

  • Image node:20 = 1 Go
  • Les devDependencies (TypeScript, etc.) restent dans l'image
  • Tous les fichiers sources .ts sont dans l'image finale

Après : Multi-stage build

# === STAGE 1 : BUILD ===

FROM node:20 AS builder

WORKDIR /app

COPY package.json package-lock.json .

RUN npm ci

COPY tsconfig.json .ts .

RUN npm run build

# === STAGE 2 : PRODUCTION ===

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/dist ./dist

COPY --from=builder /app/node_modules ./node_modules

COPY package.json .

ENV NODE_ENV=production

EXPOSE 3000

USER node

CMD ["node", "dist/server.js"]

✅ Image finale basée sur node:20-alpine (~135 Mo) + uniquement dist/ et node_modules/

Comparaison : avant vs après

❌ Avant

1.02 Go

📦 node:20 (Debian full)

📦 node_modules (dev + prod)

📦 Fichiers sources .ts

📦 Caches npm

✅ Après

152 Mo

📦 node:20-alpine (minimal)

📦 node_modules (production only)

📦 dist/ (JS compilé)

📦 package.json

📉 Réduction de ~85 % de la taille de l'image !

Déploiements plus rapides, consommation disque réduite, surface d'attaque diminuée

Visualisation du build multi-stage

$ docker build -t mon-app:optimise .

✔ CACHED [builder 1/5] FROM node:20

✔ CACHED [builder 2/5] COPY package.json .

► BUILD [builder 3/5] RUN npm ci (45s)

► BUILD [builder 4/5] COPY . .

► BUILD [builder 5/5] RUN npm run build (12s)

✔ CACHED [runner 1/3] FROM node:20-alpine

► BUILD [runner 2/3] COPY --from=builder /app/dist /app

► BUILD [runner 3/3] COPY --from=builder /app/node_modules /app

✔ Successfully built 8c3a1b2d9e0f

✔ Successfully tagged mon-app:optimise

💡 Les layers du builder sont en cache tant que les sources ne changent pas

Vérification avec docker images

$ docker images mon-app

REPOSITORY TAG SIZE

mon-app naif 1.02GB

mon-app optimise 152MB

1.02 Go — Image naïve

Contient le compilateur TypeScript, tous les .ts, les devDependencies, npm cache...

152 Mo — Image optimisée

Contient uniquement le JS compilé, les dépendances de production, et le runtime Alpine

🎯 docker images pour constater la différence 🎯

⚠️ Pièges courants

Les erreurs à éviter absolument

Piège n°1 : Confondre volume nommé et bind mount

❌ Bind mount

-v /chemin/hote:/chemin/conteneur

Monte un dossier ou chemin existant de l'hôte

Dépend du système de fichiers de l'hôte

✅ Volume nommé

-v mon_volume:/chemin/conteneur

Géré par Docker dans /var/lib/docker/volumes/

Indépendant de l'OS hôte — portable

💡 Règle : chemin absolu /home/... = bind mount | nom simple pgdata = volume nommé

Piège n°2 : Bind mounts bidirectionnels

❌ Le piège

# docker-compose.yml

- ./app:/app

# Dans le conteneur : npm install

// Sur l'hôte : node_modules/ modifié !

npm install dans le conteneur modifie node_modules sur l'hôte

✅ La solution

# Solution 1 : volume anonyme pour node_modules

- /app/node_modules

# Solution 2 : .dockerignore + npm ci dans le Dockerfile

Empêcher l'écrasement du node_modules de l'hôte

💡 Astuce : ajoutez node_modules dans votre .gitignore pour éviter les commits accidentels

Piège n°3 : Oublier de copier les fichiers nécessaires

❌ L'erreur

# Stage runner

FROM node:20-alpine

COPY --from=builder /app/dist ./dist

CMD ["node", "dist/server.js"]

// 💥 Error: Cannot find module 'express'

❌ node_modules n'a pas été copié !

✅ La solution

# Stage runner

FROM node:20-alpine

COPY --from=builder /app/dist ./dist

COPY --from=builder /app/node_modules ./node_modules

COPY package.json .

CMD ["node", "dist/server.js"]

✅ Tous les fichiers nécessaires copiés

💡 Testez toujours l'image finale avec docker run mon-app avant de déployer

Piège n°4 : Conteneurs sur des réseaux différents

❌ L'erreur

# Conteneur A sur réseau "front"

docker run --network front nginx

# Conteneur B sur réseau "back"

docker run --network back postgres

// 💥 A ne peut pas joindre B !

Deux réseaux différents = pas de communication

✅ La solution

# Même réseau pour les deux

docker network create app-net

docker run --network app-net nginx

docker run --network app-net postgres

// ✅ Communication possible

Ou utilisez Docker Compose qui crée un réseau unique pour tous les services

🎯 À retenir !

📦 Volumes

Volumes nommés pour la production

Bind mounts pour le développement

tmpfs pour les données temporaires

🌉 Réseaux

Bridge par défaut — DNS par nom de service

Chaque compose crée son propre réseau

Des réseaux différents = isolation totale

🔨 Multi-stage

Build dans node:20, run dans alpine

COPY --from pour ne garder que le strict nécessaire

~85% de réduction de taille d'image

⚠️ Pièges

Bind mount ≠ volume nommé

Oublier de copier un fichier en multi-stage

Conteneurs isolés sur des réseaux différents

🐳 Des questions ?