Déploiement
Source principale :
docs/DEPLOIEMENT.md(référence complète, conservée en place).
Architecture
Internet
│
▼
┌─────────────────────────────────────────┐
│ Traefik │
│ (TLS Let's Encrypt, CORS MinIO) │
└──────────────────┬──────────────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌─────────────┐ ┌─────────────────────┐
│ Next.js │◄──────►│ MinIO (S3) │
│ port 3000 │ upload │ port 9000 (API) │
│ │ │ port 9001 (console)│
└──────┬──────┘ └─────────────────────┘
│ ▲
▼ │ GET audio (direct)
┌─────────────┐ Navigateur client
│ PostgreSQL │
│ port 5432 │
└─────────────┘
Rôle de chaque composant
| Composant | Rôle |
|---|---|
| Traefik | Reverse proxy, TLS, middleware CORS pour MinIO |
| Next.js | Application + API REST (auth, upload audio, proxy fallback dev) |
| PostgreSQL | Données métier (langues, cartes, items, speakers, admins) |
| MinIO | Stockage audio — bucket audios public en lecture |
Accès direct navigateur → MinIO pour les audios : zéro charge sur Next.js lors de la lecture, URLs stables compatibles avec le Service Worker.
Prérequis
| Composant | Version minimale |
|---|---|
| Docker | ≥ 24 |
| Docker Compose | ≥ 2.20 |
| Traefik | Déployé sur le réseau Docker externe |
| GitLab Runner | Avec accès Docker (pour CI/CD) |
Variables d'environnement
Variables build-time (NEXT_PUBLIC_*)
Intégrées au bundle lors du docker build. Un changement de valeur nécessite un rebuild.
| Variable | Description | Exemple |
|---|---|---|
NEXT_PUBLIC_APP_URL |
URL publique de l'app (emails de confirmation) | https://racines.kiyara.net |
NEXT_PUBLIC_PWA_ENABLED |
Activer le Service Worker | true (prod), false (dev) |
AUDIO_CDN_URLn'est plus un build-arg — c'est une variable runtime injectée au démarrage du container.
Variables runtime (container)
Passées via env_file dans docker-compose.yml. Aucun rebuild requis.
Audio CDN (critique) :
| Variable | Description | Valeur exemple |
|---|---|---|
AUDIO_CDN_URL |
URL publique MinIO (inclut le bucket) — obligatoire | https://racines-s3.id2real.net/audios |
AUDIO_PROXY_DISABLED |
Désactiver le fallback proxy | true (prod) |
AUDIO_CDN_DOMAIN |
Hostname pour la CSP — dérivé automatiquement si absent | racines-s3.id2real.net |
⚠️ L'entrypoint Docker (
docker/entrypoint.sh) refuse de démarrer siAUDIO_CDN_URLest vide.
PostgreSQL :
| Variable | Valeur exemple |
|---|---|
PG_HOST |
postgres |
PG_PORT |
5432 |
PG_DATABASE |
racines_db |
PG_USER |
racines |
PG_PASSWORD |
[secret] |
PG_SSL |
false |
MinIO (accès serveur pour upload) :
| Variable | Description |
|---|---|
MINIO_ENDPOINT |
URL interne (Node.js → MinIO) : http://minio:9000 |
MINIO_ACCESS_KEY |
Clé d'accès applicative |
MINIO_SECRET_KEY |
Clé secrète applicative |
MINIO_BUCKET |
Nom du bucket : audios |
MINIO_PUBLIC_URL |
URL publique côté serveur (construction des audio_url) |
Application :
| Variable | Description |
|---|---|
JWT_SECRET |
Secret JWT (sessions admin/speaker) |
NODE_ENV |
production |
SMTP (formulaire de contact) :
| Variable | Description |
|---|---|
SMTP_HOST |
Serveur SMTP |
SMTP_PORT |
Port SMTP (587 STARTTLS, 465 SSL) |
SMTP_SECURE |
true pour port 465, sinon false |
SMTP_USER |
Utilisateur SMTP |
SMTP_PASS |
Mot de passe SMTP |
SMTP_FROM_EMAIL |
Adresse expéditeur |
ADMIN_EMAIL_1/2/3 |
Destinataires des messages de contact |
Format du fichier .app_env
# Toutes les valeurs entre double quotes (obligatoire pour les valeurs avec #, =, espaces)
AUDIO_CDN_URL="https://racines-s3.id2real.net/audios"
JWT_SECRET="un_secret_long_et_aleatoire_generé"
PG_PASSWORD="mot_de_passe_postgres"
MINIO_SECRET_KEY="cle_secrete_minio"
Pipeline CI/CD GitLab
Stages
push branche
│
├── test
│ ├── test:quality:sonarqube (main/develop/qa/test-ci + tags)
│ └── test:build:docker (MR uniquement — target builder)
│
├── containerize
│ ├── package:image:dev (branche dev)
│ ├── package:image:staging (develop/qa/test-ci)
│ └── package:image:prod (main + tags)
│
└── deploy
├── deploy:int:ansible (develop/test-ci → staging, automatique)
├── deploy:dev:ansible (dev → dev, automatique)
└── deploy:prod:manual (main/tags → prod, ⚠️ déclenchement MANUEL)
Une seule image par commit — identique pour tous les environnements. L'URL CDN MinIO est injectée au démarrage du container.
Images Docker
| Job | Branche(s) | Tag image |
|---|---|---|
package:image:dev |
dev |
dev |
package:image:staging |
develop, qa, test-ci |
develop / qa |
package:image:prod |
main, tags |
main / v1.2.3 |
Variables GitLab CI/CD
À définir dans GitLab → Settings → CI/CD → Variables :
| Variable | Description |
|---|---|
DOCKER_REG_URL |
URL du registry Docker |
DOCKER_REG_LOGIN |
Login registry |
DOCKER_REG_PASS |
Mot de passe registry |
SSHKEY_CI |
Clé SSH vers le serveur staging |
SSHKEY_DEV |
Clé SSH vers le serveur dev |
DEPLOY_SSHKEY_FILE |
Clé SSH vers le serveur production |
SONAR_HOST_URL |
URL SonarQube (analyse qualité) |
Déploiement en production
GitLab → Pipelines → pipeline sur main → deploy:prod:manual → ▶ (cliquer Play)
Le déploiement est manuel : aucune mise en production automatique.
Infrastructure MinIO
Bucket et politiques
mc alias set minio https://racines-s3.id2real.net ACCESS_KEY SECRET_KEY
# Créer le bucket avec versioning
mc mb minio/audios --ignore-existing
mc version enable minio/audios
# Lecture publique (navigateur → audios sans auth)
mc anonymous set download minio/audios
# Compte applicatif (upload/delete, sans droits admin)
mc admin user add minio app_racines_rw MOT_DE_PASSE_FORT
mc admin policy attach minio readwrite --user app_racines_rw
CORS MinIO via Traefik
MinIO AGPL ne supporte pas la configuration CORS via l'API S3. Le CORS est géré par un middleware Traefik :
labels:
- traefik.http.routers.racines-minio-api.middlewares=minio-cors
- traefik.http.middlewares.minio-cors.headers.accesscontrolalloworiginlist=https://racines.kiyara.net,https://staging.racinesbykiyara.net
- traefik.http.middlewares.minio-cors.headers.accesscontrolallowmethods=GET,HEAD,OPTIONS
- traefik.http.middlewares.minio-cors.headers.accesscontrolallowheaders=*
- traefik.http.middlewares.minio-cors.headers.accesscontrolmaxage=86400
Ajouter le domaine de chaque nouvel environnement à
accesscontrolalloworiginlist.
Healthcheck MinIO
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 30s
timeout: 20s
retries: 3
⚠️ Utiliser
mc ready local— pasmc admin info(requiert droits admin) nicurl(absent de l'image).
Dockerfile (multi-stage)
# Stage 1: deps — dépendances Node.js
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: builder — build Next.js
FROM node:20-alpine AS builder
ARG NEXT_PUBLIC_PWA_ENABLED=true
ARG NEXT_PUBLIC_APP_URL
ENV NEXT_PUBLIC_PWA_ENABLED=$NEXT_PUBLIC_PWA_ENABLED
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
WORKDIR /app
COPY . .
RUN npm ci && NODE_ENV=production npm run build
# Supprimer le SW si PWA désactivée
RUN if [ "$NEXT_PUBLIC_PWA_ENABLED" != "true" ]; then \
rm -f public/sw*.js public/register-sw*; \
fi
# Stage 3: runner — image finale (standalone)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
COPY docker/entrypoint.sh ./entrypoint.sh
RUN chmod +x entrypoint.sh
EXPOSE 3000
ENTRYPOINT ["./entrypoint.sh"]
Build en production :
docker build \
--build-arg NEXT_PUBLIC_PWA_ENABLED=true \
--build-arg NEXT_PUBLIC_APP_URL=https://racines.kiyara.net \
-t registry.id2real.net/racines-app:main .
Premier déploiement
1. Préparer le serveur
mkdir -p /docker-apps/racines
cd /docker-apps/racines
# Copier le docker-compose de l'environnement (fourni par Ansible)
cp ansible/docker-compose.integration.yaml docker-compose.yml
# Créer le fichier de variables
cp .env.example .app_env
nano .app_env # Remplir toutes les valeurs (double quotes obligatoires)
2. Configurer MinIO
# Démarrer MinIO uniquement
docker compose up -d minio
# Configurer le bucket
docker exec -it racines-minio mc alias set local http://localhost:9000 root root_password
docker exec -it racines-minio mc mb local/audios --ignore-existing
docker exec -it racines-minio mc version enable local/audios
docker exec -it racines-minio mc anonymous set download local/audios
3. Initialiser la base de données
Les migrations sont dans le dossier migrations/ :
# Démarrer PostgreSQL uniquement
docker compose up -d postgres
# Appliquer les migrations
docker exec -i racines-postgres psql -U racines -d racines_db \
< migrations/001_initial_schema.sql
docker exec -i racines-postgres psql -U racines -d racines_db \
< migrations/002_create_admins.sql
docker exec -i racines-postgres psql -U racines -d racines_db \
< migrations/004_quiz_results.sql
003_idb_card_id_index_note.sqlest une note de documentation — ne pas l'exécuter.
4. Créer le premier compte admin
# Sur le serveur, une fois le container app démarré
docker exec -it racines-app npm run create-admin
# → Saisir email et mot de passe
Ou depuis un poste de développement avec accès à la base :
npm run create-admin
5. Lancer l'application
docker compose pull
docker compose up -d
docker compose logs -f app
6. Vérifier le démarrage
# Liveness probe Next.js
curl https://racines.kiyara.net/api/health/live
# → {"status":"ok"}
# Accès audio direct MinIO
curl -I https://racines-s3.id2real.net/audios/pul/uuid-item/audio.mp3
# → HTTP 200, Content-Type: audio/mpeg
# CORS audio
curl -I -H "Origin: https://racines.kiyara.net" \
https://racines-s3.id2real.net/audios/pul/uuid-item/audio.mp3
# → Access-Control-Allow-Origin: https://racines.kiyara.net
Mise à jour
Via pipeline CI/CD (méthode normale)
git push origin develop # → build + deploy staging automatique
git push origin main # → build prod, puis deploy MANUEL
Mise à jour manuelle sur le serveur
cd /docker-apps/racines
docker compose pull app
docker compose up -d --force-recreate app
Après un changement de variables d'environnement uniquement
# Modifier .app_env sur le serveur, puis :
docker compose up -d --force-recreate app
# Aucun rebuild nécessaire — AUDIO_CDN_URL est lu au démarrage du container
PWA et déploiements
Impact d'un déploiement sur les utilisateurs offline
- Les utilisateurs avec une langue téléchargée continuent à l'utiliser offline (ancienne version en cache).
- Au prochain chargement en ligne, le nouveau SW s'installe → invalide les anciens caches (
racines-cache-vXX→racines-cache-vXX+1). - Un re-téléchargement de la langue met à jour le contenu offline.
Si AUDIO_CDN_URL change entre deux déploiements
Les audios précédemment mis en cache (clés = URLs absolues) deviennent orphelins. Les utilisateurs doivent re-télécharger la langue pour avoir les audios offline avec les nouvelles URLs.
Checklist post-déploiement
[ ] GET /api/health/live → {"status":"ok"}
[ ] Interface admin accessible → /admin → page de login
[ ] Login admin fonctionne → identifiants valides acceptés
[ ] Liste des langues → /api/languages → JSON valide
[ ] Audio direct MinIO → curl -I [AUDIO_CDN_URL]/pul/uuid/audio.mp3 → 200
[ ] CORS MinIO → Access-Control-Allow-Origin présent
[ ] Pas de violation CSP → DevTools → Console, aucune erreur CSP
[ ] Lecture audio sur une carte → bouton 🔊 fonctionne
[ ] PWA installable → icône d'installation visible dans le navigateur
[ ] Téléchargement offline → télécharger une langue, mode avion, naviguer
[ ] Formulaire de contact → soumission → email reçu + toast succès
[ ] Statistiques tracées → actions dans l'app → dashboard admin mis à jour
Troubleshooting
Container refuse de démarrer : AUDIO_CDN_URL must be set
Cause : Variable absente du docker-compose.yml ou du .app_env.
Fix : Ajouter AUDIO_CDN_URL="https://[CDN]/audios" et relancer. Aucun rebuild.
Audios bloqués par CSP
Symptôme : Refused to connect to 'https://s3.example.com/...' because it violates CSP
Cause : AUDIO_CDN_DOMAIN incorrect ou absent (dérivation automatique a échoué).
Fix : Définir explicitement AUDIO_CDN_DOMAIN="s3.example.com" et relancer.
Erreur CORS sur les audios
Symptôme : Access-Control-Allow-Origin absent dans les headers de réponse MinIO.
Cause : Domaine de l'app absent du middleware Traefik minio-cors.
Fix : Ajouter le domaine à accesscontrolalloworiginlist dans les labels Traefik.
Healthcheck MinIO unhealthy : Access Denied
Cause : Commande mc admin info utilisée (requiert droits admin).
Fix : Utiliser mc ready local dans le healthcheck.
SW non mis à jour après déploiement
Symptôme : L'utilisateur voit l'ancienne version.
Fix : Hard refresh (Ctrl+Shift+R) ou DevTools → Application → Service Workers → Unregister.
Page offline → retour à la home à la place de la carte
Cause possible 1 : La page n'a pas été précachée (téléchargement interrompu).
Cause possible 2 : Le SW n'est pas encore controller (première installation).
Fix : Recharger en ligne pour activer le nouveau SW, puis re-télécharger la langue.