Files
cv-maker/docs/conception/08-deploiement-docker.md
T
2026-09-16 21:34:09 +02:00

205 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 08 — Déploiement conteneurisé (Docker / DockerHub)
> Ce document est la **spécification** de la dockerisation de CV-Maker. Les fichiers `Dockerfile`, `docker-compose.yml` et `.dockerignore` seront créés lors de l'implémentation, **conformément** au contenu détaillé ci-dessous.
## 1. Objectif
1. Packager l'application Next.js dans une **image Docker** de production reproductible.
2. Pouvoir **pousser cette image sur Docker Hub** (`docker push`) pour la distribuer et la déployer n'importe où (`docker run` / `docker compose up`).
3. Maîtriser la taille de l'image : seule la **sortie standalone** de Next.js est embarquée (pas le code source ni les dépendances de build).
## 2. Architecture de l'image (multi-stage)
```
node:20-alpine
│
├─ stage [deps] pnpm install --frozen-lockfile (lockfile seul)
│
├─ stage [builder] sources + node_modules → pnpm build
│ → produit : .next/standalone, .next/static, public/
│
└─ stage [runner] .next/standalone + .next/static + public/
→ node server.js (HOSTNAME=0.0.0.0 PORT=3000)
```
| Stage | Image | Contenu | Rôle |
| --- | --- | --- | --- |
| `deps` | `node:20-alpine` | `package.json` + `pnpm-lock.yaml` | Télécharge et fige les dépendances (cache de build efficace) |
| `builder` | `node:20-alpine` | sources + `node_modules` | Compile `pnpm build` ; produit l'artefact standalone |
| `runner` | `node:20-alpine` | `.next/standalone`, `.next/static`, `public/` | Exécution seule : image légère, aucun source ni tooling |
### Pourquoi `output: "standalone"` ?
Next.js trace à la compilation toutes les dépendances réellement utilisées et génère un `server.js` autonome dans `.next/standalone`. Cela permet une image de production **sans node_modules complète** (économie de l'ordre de ×5 à ×10 en taille).
### Pourquoi `node:20-alpine` ?
Base minimale (≈50 Mo) pour la phase runtime, cohérente avec le prérequis Node 20 du projet. Compatible glibc/musl — aucune dépendance native dans ce projet (marked/dompurify/lucide sont des packages JS purs).
## 3. Fichiers à créer — spécifications
### 3.1 `Dockerfile`
```dockerfile
# ── Étape de dépendances ────────────────────────────────────────
FROM node:20-alpine AS deps
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# ── Étape de build ─────────────────────────────────────────────
FROM node:20-alpine AS builder
RUN corepack enable
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build de production ; la PWA (Serwist) génère public/sw.js ici
RUN pnpm build
# ── Étape d'exécution ──────────────────────────────────────────
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
# .next/standalone auto-contain server.js, package.json…
COPY --from=builder /app/.next/standalone ./
# Assets statiques (CSS/JS optimisés) + public/ (manifest, icônes, sw.js)
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
```
Notes d'intégration :
- `corepack enable` active pnpm à partir du champ `packageManager` du `package.json` (écrit par `create-next-app`) → version de pnpm **figée**.
- `pnpm install --frozen-lockfile` : échec si le lockfile n'est pas à jour (déploiement déterministe).
- `public/` contient génère tout ce qui est généré pendant le build (ex. `sw.js` de Serwist) mais est copié depuis le builder → toujours cohérent.
### 3.2 `.dockerignore`
```gitignore
node_modules
.next
out
.git
.gitignore
docs
*.md
Dockerfile
docker-compose.yml
.dockerignore
*.log
.env*
.DS_Store
```
Justification :
- `node_modules`, `.next`, `docs`, `*.md` : ne doivent **jamais** entrer dans le contexte de build (cache, rapidité, confidentialité — les secrets éventuels d'une doc `.env` ne sont pas copiés).
- Le contexte `docker build` doit contenir **uniquement** ce dont le builder a besoin : sources `app/`, `components/`, `lib/`, `public/`, `next.config.ts`, `tsconfig.json`, `package.json`, `pnpm-lock.yaml`.
### 3.3 `docker-compose.yml`
```yaml
services:
cv-maker:
build:
context: .
dockerfile: Dockerfile
image: cv-maker:local # nom local ; remplacé par <mon-user-dockerhub>/cv-maker:tag au push
container_name: cv-maker
restart: unless-stopped
ports:
- "3000:3000"
environment:
- HOSTNAME=0.0.0.0
- PORT=3000
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
```
- `wget` est fourni par busybox sur Alpine (utilisable pour le healthcheck).
- `127.0.0.1` (et non `localhost`) : busybox wget privilégie `::1` (IPv6) quand `/etc/hosts` définit `localhost` en dual-stack ; le serveur Next écoute en IPv4 (`0.0.0.0`) uniquement → pas de fallback, erreur `Connection refused`. L'adresse IP explicite évite ce piège (validé C6).
- Le **stockage des données reste dans le localStorage du navigateur** : le conteneur n'a besoin d'aucun volume persistant.
## 4. Workflow Docker Hub
### 4.1 Build et test local
```bash
# Build de l'image
docker build -t cv-maker:latest .
# Test rapide
docker run --rm -p 3000:3000 cv-maker:latest
# → http://localhost:3000
# Inspection (taille, couches)
docker images cv-maker
```
### 4.2 Publication sur Docker Hub
```bash
# 1. Se connecter au registre
docker login
# 2. Taguer avec le namespace du compte (placeholder à remplacer)
docker tag cv-maker:latest <mon-user-dockerhub>/cv-maker:latest
# 3. Optionnel : tag avec version (semver)
docker tag cv-maker:latest <mon-user-dockerhub>/cv-maker:1.0.0
# 4. Pousser
docker push <mon-user-dockerhub>/cv-maker:latest
docker push <mon-user-dockerhub>/cv-maker:1.0.0
```
> Remplacez `<mon-user-dockerhub>` par votre identifiant Docker Hub. Le pattern `latest` + `1.0.0` (semver) est recommandé : `latest` pour les déploiements courants, un tag précis pour les retours arrière.
### 4.3 Déploiement depuis l'image publiée
```bash
docker pull <mon-user-dockerhub>/cv-maker:latest
docker run -d -p 3000:3000 --name cv-maker <mon-user-dockerhub>/cv-maker:latest
# ou via compose (après édition de image: dans docker-compose.yml)
docker compose up -d
docker compose down # arrêt
```
## 5. Recette de validation conteneurisée
| # | Action | Résultat attendu |
| --- | --- | --- |
| C1 | `docker build -t cv-maker:latest .` | Build multi-stage sans erreur ; maintenant `deps` cache le layer npm |
| C2 | `docker images cv-maker` | Image < 200 Mo (standalone) |
| C3 | `docker run --rm -p 3000:3000 cv-maker:latest` puis ouvrir `localhost:3000` | L'éditeur s'affiche ; le PWA `sw.js` des réponses |
| C4 | Saisir du contenu, recharger | Persistance localStorage fonctionnelle dans le conteneur |
| C5 | `docker exec <ctr> wget -qO- localhost:3000/documentation` | HTTP 200, contenu de la page Documentation |
| C6 | `docker compose up -d` puis `docker compose ps` | Conteneur `healthy`, port mappé |
| C7 | `docker push <mon-user-dockerhub>/cv-maker:latest` | Image visible sur Docker Hub (page du compte) |
| C8 | `docker pull/run` depuis Docker Hub sur une autre machine | L'application démarre identiquement |
## 6. Dépannage
| Problème | Cause probable | Remède |
| --- | --- | --- |
| `pnpm: command not found` dans le build | `corepack enable` avant `pnpm` | Vérifier l'ordre des instructions dans `deps` |
| `Error: Cannot find module server.js` | sortie standalone absente | Vérifier `output: "standalone"` dans `next.config.ts` avant `pnpm build` |
| `sw.js` (PWA) non servi | `public/` oublié dans le runner | Ajouter `COPY --from=builder /app/public ./public` |
| Port déjà occupé en local | un autre process sur 3000 | Changer la mappage : `-p 3001:3000` |
| Image trop lourde | `.dockerignore` absent/incomplet | Re-vérifier les exclusions `node_modules`, `.next` |
| `frozen-lockfile` échoue | `pnpm-lock.yaml` obsolète | Re-synchroniser en local (`pnpm install`) puis commit du lockfile |
## 7. Évolutions (non bloquantes v1)
- **CI/CD** : workflow GitHub Actions (ou GitLab CI) : stage `build` → `push` sur Docker Hub uniquement sur les tags/`main` ; secrets `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` stockés dans les paramètres du dépôt.
- **Multiplateformes** : `docker buildx build --platform linux/amd64,linux/arm64` pour ARM (Raspberry, Apple Silicon serveurs).
- **Scan de sécurité** : `docker scout cves <image>` avant chaque push.