205 lines
8.7 KiB
Markdown
205 lines
8.7 KiB
Markdown
# 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. |