# 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 /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 /cv-maker:latest # 3. Optionnel : tag avec version (semver) docker tag cv-maker:latest /cv-maker:1.0.0 # 4. Pousser docker push /cv-maker:latest docker push /cv-maker:1.0.0 ``` > Remplacez `` 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 /cv-maker:latest docker run -d -p 3000:3000 --name cv-maker /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 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 /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 ` avant chaque push.