docs: versionner spec + guide de déploiement (hors portfolios-examples)

Guide de déploiement prod : VM Debian nue (sans Docker), service systemd,
déploiement par push Git (bare repo + post-receive), branchement rsync/SSH
en lecture seule à la prod Grav, reverse proxy HTTPS, dimensionnement VM.
This commit is contained in:
2026-07-06 13:48:31 +02:00
parent 48b7f97954
commit 1069bba2e4
3 changed files with 1067 additions and 3 deletions

View File

@@ -0,0 +1,485 @@
# Déploiement en production
Ce document décrit comment déployer le **générateur de portfolios** sur une VM
**Debian nue** (sans Docker) et le brancher, en lecture seule, à la VM **Grav de
production** de Figures Libres.
Le déploiement se fait par **push Git** vers un dépôt *bare* sur la VM (même
principe que les autres projets Figures Libres :
<https://figureslibres.io/dokuwiki/developement:git_deployement>). L'app tourne
comme **service systemd** (serveur Nitro), derrière un **reverse proxy HTTPS**.
> ⚠️ **Exception à la règle « tout en Docker ».** Docker n'est utilisé qu'en
> développement local. En production, l'app tourne **nativement** sur Debian.
---
## 1. Architecture cible
```
Poste dev VM « portfolios » (Debian nue) VM Grav (prod)
┌───────────┐ git push prod ┌──────────────────────────────────────┐ rsync/SSH ┌──────────────┐
│ generator │ ─────────────────▶ │ bare repo .git ──post-receive──▶ deploy│ ─(lecture ────▶│ user/pages/ │
│ (repo) │ │ → checkout + npm ci + build │ seule) │ 02.projets/ │
└───────────┘ │ → systemctl restart │◀──── pull ──────│ (inchangé) │
│ │ └──────────────┘
│ systemd: node .output/server/index.mjs │
│ écoute 127.0.0.1:3000 │
│ données: /var/lib/portfolio-generator/ │
└──────────────┬─────────────────────────┘
│ 127.0.0.1:3000
┌────────▼─────────┐
│ nginx :443 (TLS) │ ◀── navigateurs (équipe + liens publics)
└──────────────────┘
```
Points clés :
- Le contenu ne circule que **dans un sens** : la VM portfolios *tire* les projets
depuis la prod Grav par `rsync`/SSH en **lecture seule**. La prod n'est jamais
modifiée et **ignore** l'existence de l'app.
- On **ne réimporte pas** `grav-reference` en prod (c'est un jeu d'exemple de dev).
Le contenu prod vit dans un cache local rafraîchi par le bouton « Rafraîchir ».
- Le rendu PDF appelle le serveur en interne sur **`localhost:3000`** : le port
Nitro **doit** rester `3000` (voir §6).
---
## 2. Dimensionnement de la VM
### RAM
| Poste | Empreinte |
|-------|-----------|
| Serveur Nitro au repos | ~150300 Mo |
| Pic à la **génération PDF** : Chromium (~300600 Mo) + traitement d'images `sharp` en 2× sur des pages A3 + Ghostscript | **12 Go** ponctuels |
- **Recommandé : 4 Go.** Plancher : 2 Go (risqué sur des portfolios très riches en
images — le processus de génération peut être tué par l'OOM killer).
- La génération est **séquentielle** (une seule instance Chromium à la fois via une
file d'attente) : la RAM ne monte pas linéairement avec le nombre d'utilisateurs.
### Disque
Ordres de grandeur mesurés / à prévoir (l'essentiel vit dans `data/`) :
| Élément | Taille | Note |
|---------|--------|------|
| Debian + Node + outils | ~34 Go | système |
| `node_modules` | ~11,5 Go | Nuxt + Playwright + sharp |
| Chromium (Playwright) | ~0,5 Go | navigateur de rendu |
| `.output` (build Nitro) | ~0,10,3 Go | régénéré à chaque déploiement |
| `data/content-cache/` | ~0,75 Go aujourd'hui | miroir de `02.projets` (croît avec le contenu) |
| `data/frozen/` | ~0,4 Go aujourd'hui | médias figés des portfolios générés (croît à l'usage) |
| `data/exports/` | ~0,015 Go | PDF (18 Mo pièce, régénérables) |
| `data/cache/` | transitoire | rendu/travail PDF |
| `data/portfolios/` | quelques Mo | **la donnée métier** (JSON) |
- **Recommandé : 30 Go** (confort + croissance du contenu et des instantanés).
Plancher : 20 Go.
### CPU
- **2 vCPU recommandés.** La génération PDF est *CPU-bound* (rendu Chromium +
ré-échantillonnage d'images + Ghostscript), par à-coups.
---
## 3. Prérequis logiciels (installation unique, en root)
Sur la **VM portfolios**, une seule fois :
```bash
apt update && apt install -y curl git rsync openssh-client ghostscript ffmpeg nginx
# Node.js 22 LTS (dépôt NodeSource)
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs
node --version # v22.x
# Dépendances système de Chromium (pour Playwright) — installées plus tard
# par « npx playwright install-deps » (§7), qui nécessite root.
```
---
## 4. Utilisateur et arborescence
On dédie un utilisateur système `portfolio` qui **possède tout** (dépôt bare,
checkout, données, service). Cela évite tout jonglage de permissions.
```bash
# en root
adduser --disabled-password --gecos "" portfolio
# arborescences
install -d -o portfolio -g portfolio /home/portfolio/gitBareRepos
install -d -o portfolio -g portfolio /var/www/portfolio-generator
install -d -o portfolio -g portfolio /var/lib/portfolio-generator/data
install -d -o portfolio -g portfolio -m 700 /var/lib/portfolio-generator/.ssh
```
| Chemin | Rôle |
|--------|------|
| `/home/portfolio/gitBareRepos/portfolio-generator.git` | dépôt *bare* (cible du push) |
| `/var/www/portfolio-generator/public_html` | checkout de travail (build) |
| `/var/www/portfolio-generator/deploy.sh` | script de déploiement |
| `/var/lib/portfolio-generator/data` | **données persistantes** (`NUXT_DATA_DIR`) |
| `/var/lib/portfolio-generator/.ssh/portfolio_sync` | clé privée de synchro rsync |
| `/etc/portfolio-generator.env` | variables d'environnement du service |
---
## 5. Déploiement par push Git
> Tout le §5 s'exécute **en tant qu'utilisateur `portfolio`** (`sudo -iu portfolio`).
### 5.1 Dépôt *bare*
```bash
cd ~/gitBareRepos
git init --bare portfolio-generator.git
```
### 5.2 Hook `post-receive`
`~/gitBareRepos/portfolio-generator.git/hooks/post-receive` :
```bash
#!/usr/bin/env bash
PRODDIR="/var/www/portfolio-generator"
while read oldrev newrev refname; do
if [ "$refname" = "refs/heads/prod" ]; then
echo "===== DÉPLOIEMENT ====="
unset GIT_DIR
cd "$PRODDIR" || exit 1
. deploy.sh
echo "===== OK ====="
else
echo "Commit non déployé — pushez la branche prod."
fi
done
```
```bash
chmod +x ~/gitBareRepos/portfolio-generator.git/hooks/post-receive
```
### 5.3 Checkout de travail
Cloné une fois depuis le dépôt bare (il aura ainsi `origin` = bare) :
```bash
git clone ~/gitBareRepos/portfolio-generator.git /var/www/portfolio-generator/public_html
```
### 5.4 Script `deploy.sh`
`/var/www/portfolio-generator/deploy.sh` :
```bash
#!/usr/bin/env bash
set -euo pipefail
APPDIR="/var/www/portfolio-generator/public_html"
echo "→ mise à jour du code"
cd "$APPDIR"
git fetch origin
git reset --hard origin/prod
echo "→ dépendances + build Nitro"
npm ci
npm run build
echo "→ redémarrage du service"
sudo systemctl restart portfolio-generator
```
```bash
chmod +x /var/www/portfolio-generator/deploy.sh
```
`npm ci` télécharge automatiquement Chromium dans le cache de l'utilisateur
`portfolio` (les dépendances **système** de Chromium sont, elles, installées une
fois en root à l'étape §7).
### 5.5 Autoriser le redémarrage du service (sudoers)
`deploy.sh` a besoin de redémarrer le service ; on limite ce `sudo` au strict
nécessaire. `/etc/sudoers.d/portfolio-generator` (en root, `visudo -f …`) :
```
portfolio ALL=(root) NOPASSWD: /usr/bin/systemctl restart portfolio-generator
```
---
## 6. Service systemd et environnement
### 6.1 Fichier d'environnement
`/etc/portfolio-generator.env` (droits `600`, propriétaire `portfolio` — il
contient des secrets) :
```dotenv
# --- Serveur ---
# Le port DOIT rester 3000 : le rendu PDF appelle le serveur sur localhost:3000.
NITRO_HOST=127.0.0.1
NITRO_PORT=3000
# --- Secrets (valeurs aléatoires longues : openssl rand -hex 32) ---
NUXT_PORTFOLIO_PASSWORD=<mot-de-passe-partagé>
NUXT_SESSION_SECRET=<aléatoire-≥32-caractères>
NUXT_INTERNAL_TOKEN=<aléatoire-≥32-caractères>
# --- Données persistantes (hors du checkout, préservées aux déploiements) ---
NUXT_DATA_DIR=/var/lib/portfolio-generator/data
# --- Source de contenu : production (cache rsync) ---
NUXT_CONTENT_SOURCE=prod
NUXT_SYNC_REMOTE=portfolio-sync@<HOTE_GRAV>:/var/www/figureslibres/user/pages/02.projets/
NUXT_SYNC_SSH_KEY=/var/lib/portfolio-generator/.ssh/portfolio_sync
```
### 6.2 Unité systemd
`/etc/systemd/system/portfolio-generator.service` :
```ini
[Unit]
Description=Générateur de portfolios Figures Libres
After=network.target
[Service]
Type=simple
User=portfolio
Group=portfolio
WorkingDirectory=/var/www/portfolio-generator/public_html
EnvironmentFile=/etc/portfolio-generator.env
ExecStart=/usr/bin/node .output/server/index.mjs
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
```bash
systemctl daemon-reload
systemctl enable portfolio-generator # démarrage auto (ne pas encore start : voir §7)
```
---
## 7. Premier déploiement (bootstrap)
1. **Côté poste dev**, dans le dépôt `generator`, ajouter le distant et pousser la
branche `prod` (déclenche le hook) :
```bash
git remote add prod portfolio@<VM_PORTFOLIOS>:gitBareRepos/portfolio-generator.git
git push prod main:prod # pousse la branche locale main sur la branche prod distante
```
Le hook lance `deploy.sh` → `npm ci` + `npm run build`. (Au tout premier push,
`git reset --hard origin/prod` échoue si le checkout est vide : voir la note.)
2. **Côté VM (utilisateur `portfolio`)**, une fois le code présent dans
`public_html`, installer les **dépendances système de Chromium** (root) :
```bash
sudo npx --prefix /var/www/portfolio-generator/public_html playwright install-deps chromium
```
3. Démarrer le service :
```bash
sudo systemctl start portfolio-generator
sudo systemctl status portfolio-generator
journalctl -u portfolio-generator -f
```
> **Note premier push** : si `git reset --hard origin/prod` du `deploy.sh` bute sur
> un checkout vierge, faire une fois manuellement dans `public_html` :
> `git fetch origin && git checkout -B prod origin/prod`, puis relancer un push.
---
## 8. Brancher à la prod Grav (rsync/SSH en lecture seule)
### 8.1 Clé de synchronisation (VM portfolios, utilisateur `portfolio`)
```bash
ssh-keygen -t ed25519 -N "" \
-f /var/lib/portfolio-generator/.ssh/portfolio_sync -C "portfolio-sync"
cat /var/lib/portfolio-generator/.ssh/portfolio_sync.pub # ← à déposer côté Grav
```
### 8.2 Côté VM Grav : accès en lecture seule
Créer un utilisateur dédié et l'autoriser à **lire** les projets, sans shell ni
écriture :
```bash
sudo adduser --disabled-password --gecos "" portfolio-sync
sudo setfacl -R -m u:portfolio-sync:rX /var/www/figureslibres/user/pages/02.projets
sudo setfacl -R -d -m u:portfolio-sync:rX /var/www/figureslibres/user/pages/02.projets
```
Déposer la clé publique dans `~portfolio-sync/.ssh/authorized_keys` en la
**restreignant à rsync** (recommandé) via `rrsync` :
```
command="rrsync -ro /var/www/figureslibres/user/pages/02.projets",no-agent-forwarding,no-port-forwarding,no-pty,no-X11-forwarding ssh-ed25519 AAAA...portfolio-sync...
```
> `rrsync` accompagne rsync (`/usr/bin/rrsync` ou
> `/usr/share/doc/rsync/scripts/rrsync`). Il verrouille la clé à un `rsync` en
> lecture seule sur ce seul dossier. À défaut, une clé standard en lecture seule
> convient. La commande exécutée par l'app est :
> `rsync -az --delete -e "ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes -i <clé>" <remote>/ <cache>/`.
### 8.3 Réseau
- Ouvrir **SSH (22)** en sortie de la VM portfolios vers la VM Grav.
- Renseigner `NUXT_SYNC_REMOTE` avec le **chemin absolu** de `02.projets` sur la VM
Grav, terminé par `/` (voir §6.1).
- Tester la connexion une fois pour accepter la clé d'hôte :
```bash
sudo -u portfolio ssh -i /var/lib/portfolio-generator/.ssh/portfolio_sync \
portfolio-sync@<HOTE_GRAV> "echo ok"
```
---
## 9. Reverse proxy HTTPS (indispensable)
Le HTTPS est **requis**, pas seulement confortable :
- les **liens de partage publics** (`/view/<id>`) sont faits pour être ouverts hors
réseau local ;
- la **copie du lien dans le presse-papiers** n'est autorisée par les navigateurs
qu'en *contexte sécurisé* (HTTPS ou `localhost`) — en HTTP sur IP elle est bloquée.
`/etc/nginx/sites-available/portfolio-generator` :
```nginx
server {
listen 80;
server_name portfolios.figureslibres.io;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name portfolios.figureslibres.io;
# ssl_certificate ... ; # via certbot (Let's Encrypt)
# ssl_certificate_key ... ;
client_max_body_size 64m; # médias volumineux
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600s; # la génération PDF peut être longue
}
}
```
```bash
ln -s /etc/nginx/sites-available/portfolio-generator /etc/nginx/sites-enabled/
apt install -y certbot python3-certbot-nginx
certbot --nginx -d portfolios.figureslibres.io
nginx -t && systemctl reload nginx
```
Le service Nitro écoutant sur `127.0.0.1:3000`, il **n'est pas** exposé
directement : seul le port 443 est public. Un pare-feu (`ufw allow 443,22`) durcit
l'ensemble.
---
## 10. Première synchronisation & vérification
1. Ouvrir `https://portfolios.figureslibres.io`.
2. Se connecter avec `NUXT_PORTFOLIO_PASSWORD`.
3. Cliquer **« Rafraîchir »** : premier `rsync` vers
`/var/lib/portfolio-generator/data/content-cache/`. Le toast indique le nombre
de fichiers et de projets synchronisés.
4. Créer un portfolio de test et **générer un PDF** pour valider toute la chaîne
(Chromium + Ghostscript).
---
## 11. Sauvegardes
L'état applicatif vit dans **`/var/lib/portfolio-generator/data/`** :
| Dossier | Contenu | Sauvegarder ? |
|---------|---------|---------------|
| `portfolios/` | portfolios (JSON) — **la donnée métier** | **Oui** |
| `frozen/` | instantanés figés (contenu + composition) | Oui |
| `exports/` | PDF générés | Optionnel (régénérables) |
| `content-cache/` | miroir rsync de la prod | Non (re-tirable) |
| `cache/` | cache de rendu | Non |
Sauvegarde à chaud (les écritures des portfolios sont atomiques) :
```bash
tar czf portfolios-backup-$(date +%F).tar.gz \
-C /var/lib/portfolio-generator/data portfolios frozen
```
---
## 12. Mises à jour (au quotidien)
Depuis le poste dev, une fois le travail prêt :
```bash
git push prod main:prod
```
Le hook redéploie tout seul (`git reset --hard` + `npm ci` + `npm run build` +
redémarrage). Les données de `/var/lib/portfolio-generator/data/` sont préservées.
Suivre : `journalctl -u portfolio-generator -f`.
---
## 13. Dépannage
| Symptôme | Piste |
|----------|-------|
| Le push ne déploie pas | Vérifier qu'on pousse bien vers la branche **`prod`** (`git push prod main:prod`) et que `post-receive` est exécutable. |
| `deploy.sh` échoue sur `systemctl restart` | Vérifier l'entrée `sudoers.d/portfolio-generator` (§5.5). |
| Service qui ne démarre pas | `journalctl -u portfolio-generator -e` ; souvent `.output` absent (build échoué) ou `EnvironmentFile` manquant. |
| Génération PDF en échec | Dépendances Chromium manquantes → `npx playwright install-deps chromium` (§7.2) ; sinon vérifier RAM (OOM) dans `journalctl`. |
| « Rafraîchir » échoue (« Échec rsync ») | Tester la commande SSH (§8.3) ; vérifier `NUXT_SYNC_REMOTE` (chemin absolu, `/` final), les droits de lecture et le pare-feu sortant (22). |
| Copie du lien public impossible | Accès en HTTP/IP → passer par l'URL **HTTPS** (§9). |
| PDF « à jour » ne se met pas à jour | La régénération réutilise l'instantané figé ; utiliser « Réactualiser depuis Grav » pour resynchroniser le contenu. |
---
## 14. Récapitulatif express
```bash
# ── VM portfolios (root, une fois) ──
apt update && apt install -y curl git rsync openssh-client ghostscript ffmpeg nginx
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt install -y nodejs
adduser --disabled-password --gecos "" portfolio
install -d -o portfolio -g portfolio /home/portfolio/gitBareRepos /var/www/portfolio-generator
install -d -o portfolio -g portfolio /var/lib/portfolio-generator/data
install -d -o portfolio -g portfolio -m 700 /var/lib/portfolio-generator/.ssh
# → créer bare repo + hook + deploy.sh (§5), env + service systemd (§6),
# sudoers (§5.5), clé de synchro (§8.1)
# ── Poste dev ──
git remote add prod portfolio@<VM>:gitBareRepos/portfolio-generator.git
git push prod main:prod
# ── VM portfolios (après 1er push) ──
sudo npx --prefix /var/www/portfolio-generator/public_html playwright install-deps chromium
sudo systemctl start portfolio-generator
# ── Côté Grav : déposer la clé publique en lecture seule (§8.2) ──
# ── nginx + certbot en façade (§9), puis « Rafraîchir » dans l'UI ──
```