8.7 KiB
08 — Déploiement conteneurisé (Docker / DockerHub)
Ce document est la spécification de la dockerisation de CV-Maker. Les fichiers
Dockerfile,docker-compose.ymlet.dockerignoreseront créés lors de l'implémentation, conformément au contenu détaillé ci-dessous.
1. Objectif
- Packager l'application Next.js dans une image Docker de production reproductible.
- Pouvoir pousser cette image sur Docker Hub (
docker push) pour la distribuer et la déployer n'importe où (docker run/docker compose up). - 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
# ── É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 enableactive pnpm à partir du champpackageManagerdupackage.json(écrit parcreate-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.jsde Serwist) mais est copié depuis le builder → toujours cohérent.
3.2 .dockerignore
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.envne sont pas copiés).- Le contexte
docker builddoit contenir uniquement ce dont le builder a besoin : sourcesapp/,components/,lib/,public/,next.config.ts,tsconfig.json,package.json,pnpm-lock.yaml.
3.3 docker-compose.yml
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
wgetest fourni par busybox sur Alpine (utilisable pour le healthcheck).127.0.0.1(et nonlocalhost) : busybox wget privilégie::1(IPv6) quand/etc/hostsdéfinitlocalhosten dual-stack ; le serveur Next écoute en IPv4 (0.0.0.0) uniquement → pas de fallback, erreurConnection 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
# 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
# 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 patternlatest+1.0.0(semver) est recommandé :latestpour les déploiements courants, un tag précis pour les retours arrière.
4.3 Déploiement depuis l'image publiée
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→pushsur Docker Hub uniquement sur les tags/main; secretsDOCKERHUB_USERNAME/DOCKERHUB_TOKENstockés dans les paramètres du dépôt. - Multiplateformes :
docker buildx build --platform linux/amd64,linux/arm64pour ARM (Raspberry, Apple Silicon serveurs). - Scan de sécurité :
docker scout cves <image>avant chaque push.