Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

4.4 — Conteneurisation (Docker)

🎯 Objectif : pouvoir conteneuriser n’importe quelle stack web et lancer un environnement complet (front + back + DB + cache) avec une seule commande.

À l'issue de cet axe, tu sauras :

  • Distinguer image, conteneur, volume, réseau
  • Écrire un Dockerfile multi-stage propre
  • Composer une stack avec Docker Compose
  • Optimiser la taille d'une image (de 1 Go à 200 Mo)
  • Comprendre le rôle de Kubernetes (sans encore l'utiliser)

Débutant 10 min prérequis : axe 1 lu

Avant Docker, tu disais : « ça marche sur ma machine ». Le serveur de prod te répondait : « mais pas sur la mienne ». Mille différences subtiles : version de Node, libssl, locale, packages OS, variables d’env.

Docker te donne un environnement reproductible : tu décris dans un fichier (Dockerfile) tout ce dont ton app a besoin pour tourner, et n’importe quelle machine avec Docker peut l’exécuter à l’identique.

Avant d’aller plus loin, il faut Docker sur ta machine. Choisis ta plateforme :

Recommandé : Docker Desktop avec WSL2 (déjà installé si tu as suivi 1.1).

  1. Télécharge Docker Desktop for Windows : docker.com/products/docker-desktop
  2. Lance l’installeur, coche « Use WSL 2 instead of Hyper-V » quand on te le demande.
  3. Redémarre.
  4. Au premier lancement, Docker Desktop te demandera d’activer l’intégration WSL — accepte.

Pré-requis : la virtualisation doit être activée dans le BIOS (presque toujours le cas sur les machines récentes). Si Docker refuse de démarrer avec un message « Virtualization disabled », redémarre dans le BIOS et active Intel VT-x / AMD-V.

Recommandé : Docker Desktop.

Fenêtre de terminal
brew install --cask docker
# ou téléchargement direct sur docker.com

Lance Docker Desktop depuis Applications. Le 1er démarrage prend ~30 secondes.

Alternative plus légère : OrbStack (commercial, mais ~10× plus léger en RAM que Docker Desktop, démarre en 2 secondes).

Docker Engine + Compose plugin sans Docker Desktop (plus léger en CLI pure) :

Fenêtre de terminal
# Suivre la doc officielle :
# https://docs.docker.com/engine/install/ubuntu/
# En résumé :
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Pour utiliser docker sans sudo :
sudo usermod -aG docker $USER
# Déconnecte/reconnecte ta session

Quelle que soit la plateforme, vérifie que tout marche :

Fenêtre de terminal
docker --version
# Docker version 27.x.x
docker compose version
# Docker Compose version v2.x.x
docker run hello-world
# Hello from Docker!
# (si tu vois ce message, c'est gagné)
flowchart LR
    Dockerfile[Dockerfile<br/>recette] -->|docker build| Image
    Image -->|docker run| Container1[Conteneur 1]
    Image -->|docker run| Container2[Conteneur 2]
    Container1 -->|écrit dans| Volume[Volume<br/>persistant]
    Container1 <-->|réseau| Container2
Les 4 concepts de base

Un modèle figé, en lecture seule. Contient OS minimal, ton code, ses dépendances.

Fenêtre de terminal
docker pull node:22-alpine # télécharger une image officielle
docker images # lister les images locales

Une instance d’une image, en cours d’exécution (ou arrêtée).

Fenêtre de terminal
docker run hello-world
docker ps # conteneurs en cours
docker ps -a # tous, y compris arrêtés
docker stop <id>
docker rm <id>

Espace de stockage persistant (les conteneurs sont éphémères, les volumes survivent).

Fenêtre de terminal
docker volume create mes-données
docker run -v mes-données:/app/data ...

Les conteneurs d’un même réseau peuvent se parler par nom.

Fenêtre de terminal
docker network create mon-réseau
docker run --network=mon-réseau --name=db postgres
docker run --network=mon-réseau --name=api node:22-alpine
# api peut joindre db via le hostname "db"
# Image de base
FROM node:22-alpine
# Dossier de travail
WORKDIR /app
# Copier les fichiers de dépendances en premier (bénéficie du cache)
COPY package.json package-lock.json ./
# Installer
RUN npm ci
# Copier le reste du code
COPY . .
# Build
RUN npm run build
# Exposer le port (documentation, pas effectif)
EXPOSE 3000
# Commande de démarrage
CMD ["node", "dist/server.js"]
Fenêtre de terminal
# Construire
docker build -t mon-app:1.0 .
# Lancer
docker run -p 3000:3000 mon-app:1.0

Chaque instruction du Dockerfile crée une couche. Docker met en cache chaque couche. Si une couche change, toutes celles d’après sont reconstruites.

Mauvais ordre :

COPY . . # ← change tout le temps
RUN npm ci # ← invalidé à chaque commit !

Bon ordre :

COPY package*.json ./ # ← rarement change
RUN npm ci # ← cache si package.json identique
COPY . . # ← copie le code après
RUN npm run build

Résultat : ton build passe de 2 minutes à 5 secondes quand tu modifies juste un fichier source.

Le Dockerfile naïf produit une image énorme : Node, sources, devDependencies, fichiers temporaires.

Solution : multi-stage. Tu construis dans une image lourde, tu copies l’essentiel dans une image finale légère.

# Stage 1 — Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2 — Production
FROM node:22-alpine AS runtime
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]

Bénéfice : la prod n’a ni TypeScript, ni Vite, ni node_modules de dev. Image passée de 1.2 Go à ~200 Mo.

Pareil que .gitignore, mais pour Docker — évite de copier dans l’image ce qui ne sert pas (et accélère le build) :

node_modules
dist
.git
.env
.env.*
*.log
coverage
.DS_Store
README.md
.vscode

Pour une stack complète (front + back + DB), tu ne veux pas lancer 4 commandes docker run. Docker Compose orchestre tout en un fichier YAML.

# compose.yml (anciennement docker-compose.yml)
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app_dev
volumes:
- db-data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
api:
build:
context: ./api
target: builder # multi-stage : on s'arrête au stage builder pour le dev
environment:
DATABASE_URL: postgres://app:secret@db:5432/app_dev
REDIS_URL: redis://redis:6379
ports:
- "3000:3000"
volumes:
- ./api:/app # bind mount pour hot-reload
- /app/node_modules # ⚠ ne pas écraser node_modules du conteneur
depends_on:
db:
condition: service_healthy
command: npm run dev
web:
build: ./web
ports:
- "5173:5173"
volumes:
- ./web:/app
- /app/node_modules
environment:
VITE_API_URL: http://localhost:3000
command: npm run dev
volumes:
db-data:
Fenêtre de terminal
docker compose up # tout démarre
docker compose up -d # en arrière-plan
docker compose logs -f api # suivre les logs d'un service
docker compose down # tout arrêter
docker compose down -v # arrêter + supprimer les volumes (reset DB)

Pour Node.js (Express, Fastify…), utilise un wrapper qui surveille les fichiers :

# dans le Dockerfile dev
CMD ["npx", "tsx", "watch", "src/index.ts"]

Ou avec nodemon, ts-node-dev, etc. Couplé au bind mount, tu modifies un fichier sur ta machine → le conteneur recharge automatiquement.

Pour Vite (front), pas besoin de plus — le HMR (Hot Module Replacement) marche tant que le port est exposé et que le bind mount est en place.

Dev Containers — coder dans un container, pas sur ton OS

Section intitulée « Dev Containers — coder dans un container, pas sur ton OS »

Et si tu n’avais rien à installer sur ta machine — ni Node, ni Python, ni Postgres ? Tu ouvres VS Code, il détecte un fichier .devcontainer/devcontainer.json, build un conteneur, ouvre ton éditeur dedans. Toutes les extensions, tous les outils CLI vivent dans le conteneur.

C’est ce qu’on appelle Dev Containers — la spec officielle d’onboarding moderne, supportée par VS Code, Cursor, JetBrains, et GitHub Codespaces (qui te donne le conteneur dans le cloud).

.devcontainer/devcontainer.json
{
"name": "Taskly Dev",
"image": "mcr.microsoft.com/devcontainers/typescript-node:24",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/postgres:1": {}
},
"forwardPorts": [3000, 5173, 5432],
"postCreateCommand": "npm install",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}
}
}

VS Code (ou Cursor) détecte ce fichier, te propose « Reopen in Container », build, et tu es dans un environnement identique pour toute l’équipe — peu importe que tu sois sur Windows, macOS Intel, Mac M3 ou Linux.

Sans Dev ContainersAvec Dev Containers
« Mon Node est en 18, le tien en 24 »Tout le monde a le même Node (celui défini dans l’image)
Onboarding nouveau dev = 2-3 h d’installOnboarding = clone + Reopen in Container = 5 min
« Ça marche chez moi »L’env est versionné dans le repo
Mac M3 incompatible avec un binaire natifLe conteneur Linux résout

Pour les stacks complexes (api + web + db), tu peux utiliser docker-compose.yml directement :

.devcontainer/devcontainer.json
{
"name": "Taskly Full Stack",
"dockerComposeFile": "../compose.yml",
"service": "api",
"workspaceFolder": "/app",
"shutdownAction": "stopCompose"
}

→ Tu réutilises le même compose.yml qu’en dev local. Pas de duplication.

GitHub Codespaces — le dev container dans le cloud

Section intitulée « GitHub Codespaces — le dev container dans le cloud »

Avec un devcontainer.json dans le repo, tu peux ouvrir GitHub Codespaces (gratuit 60 h/mois) depuis n’importe quel navigateur — y compris un Chromebook ou un iPad. C’est le futur de l’onboarding pour les contributions open-source.

Compose profiles — un seul compose.yml pour dev et prod

Section intitulée « Compose profiles — un seul compose.yml pour dev et prod »

Pour éviter d’avoir compose.dev.yml et compose.prod.yml séparés, utilise les profiles :

services:
db:
image: postgres:16-alpine
# actif tout le temps (pas de profile)
pgadmin:
image: dpage/pgadmin4
profiles: [dev] # actif seulement en dev
ports: ["5050:80"]
api:
build:
target: ${BUILD_TARGET:-dev} # variable selon profile
profiles: [dev, prod] # actif partout
monitoring:
image: grafana/grafana
profiles: [prod] # actif seulement en prod
Fenêtre de terminal
docker compose --profile dev up # api + db + pgadmin
docker compose --profile prod up # api + db + monitoring
docker compose up # SEULS les services sans profile (= db)

Plus de duplication. Tu choisis ton « mode » au lancement.

3 façons de les passer :

Fenêtre de terminal
# 1. CLI direct
docker run -e DATABASE_URL=postgres://... mon-app
# 2. Fichier .env
docker run --env-file .env mon-app
# 3. Compose
services:
api:
env_file: .env
environment:
NODE_ENV: development

Règle : ne jamais coder en dur des secrets dans le Dockerfile — ils sont visibles dans les couches de l’image.

ImageTaille typiqueUsage
node:22 (Debian)~1 GoBeaucoup d’outils dispo, lourd
node:22-slim~250 MoBon compromis
node:22-alpine~150 MoUltra léger, mais musl libc parfois problématique
gcr.io/distroless/nodejs22-debian12~200 MoPas de shell, pas de package manager — sécurité maximale
# Ne JAMAIS tourner en root en prod
FROM node:22-alpine AS runtime
RUN addgroup -g 1001 -S app && adduser -u 1001 -S app -G app
USER app
WORKDIR /app
# ...
Fenêtre de terminal
export DOCKER_BUILDKIT=1
docker build .
# ou plus simplement :
docker buildx build .

BuildKit parallélise les stages indépendants et cache plus efficacement.

Kubernetes orchestre des centaines de conteneurs sur un cluster de machines. Concepts à connaître de loin :

Objet K8sRôle
Pod1+ conteneurs colocalisés
DeploymentDéfinit un ensemble de pods avec scaling et rolling updates
ServiceAdresse réseau stable pour atteindre des pods
IngressRoutage HTTP externe (équivalent reverse proxy)
ConfigMap / SecretConfiguration et secrets

Honnêtement : 95 % des projets web n’en ont pas besoin. PaaS comme Vercel, Render, Fly.io abstraient tout cela. Apprends K8s quand ton équipe en a besoin, pas avant.

Tu modifies un fichier source. Le `docker build` reconstruit toutes les couches après ton COPY . . , y compris npm install. Pourquoi ?
Pourquoi un Dockerfile multi-stage ?
Tu as un compose.yml avec api + db. L'API démarre avant que Postgres soit prêt et plante. Solution ?
Piège réel rencontré — better-sqlite3 ne compile pas sur Windows + Node 24 Build / Native

🩹 Symptôme

npm error gyp ERR! stack ... node-gyp rebuild
better-sqlite3 ... not ok
node -v v24.14.1

🔍 Cause

better-sqlite3 est un module natif C++ : il doit être compilé via node-gyp. Sous Windows, ça exige les Visual C++ Build Tools (~ 5 GB). Sous Node 24 (très récent), les binaires prébuilds ne sont pas toujours encore publiés au moment de la sortie de Node, ce qui force la compilation locale — laquelle échoue si l’environnement de build n’est pas configuré.

🩺 Fix

Trois options par ordre de préférence :

  1. Migrer vers @libsql/client — fork Turso de SQLite, binaires prébuilds pour toutes plateformes, pas de compilation. Drizzle a un driver drizzle-orm/libsql quasi drop-in. C’est ce que ce guide utilise dans l’exercice taskly-api.
  2. Installer les Build Tools Windows : npm install --global windows-build-tools ou via Visual Studio Installer → « C++ Build Tools ».
  3. Downgrader Node à 22 LTS où les prébuilds existent.

🧠 Leçon

Pour un projet pédagogique ou multi-plateforme, éviter les modules natifs quand un équivalent pure-JS / WASM existe. La friction d’installation tue les contributions. @libsql/client, @noble/hashes, @node-rs/argon2 (lui-même prébuild) sont des bons défauts 2026.

🔍 Tous les pièges du guide sont sur la page /pieges/ — searchable par symptôme.


Suite : 4.5 — Productivité — dotfiles, terminal moderne, ripgrep, fzf, tmux.