# 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 : ). 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 | ~150–300 Mo | | Pic à la **génération PDF** : Chromium (~300–600 Mo) + traitement d'images `sharp` en 2× sur des pages A3 + Ghostscript | **1–2 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 | ~3–4 Go | système | | `node_modules` | ~1–1,5 Go | Nuxt + Playwright + sharp | | Chromium (Playwright) | ~0,5 Go | navigateur de rendu | | `.output` (build Nitro) | ~0,1–0,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 (1–8 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= NUXT_SESSION_SECRET= NUXT_INTERNAL_TOKEN= # --- 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@:/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@: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 " / /`. ### 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@ "echo ok" ``` --- ## 9. Reverse proxy HTTPS (indispensable) Le HTTPS est **requis**, pas seulement confortable : - les **liens de partage publics** (`/view/`) 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@: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 ── ```