Files
portfolio-generator-stack/docs/deploiement-production.md
Valentin Le Moign 1069bba2e4 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.
2026-07-06 13:48:31 +02:00

18 KiB
Raw Permalink Blame History

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 :

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.

# 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

cd ~/gitBareRepos
git init --bare portfolio-generator.git

5.2 Hook post-receive

~/gitBareRepos/portfolio-generator.git/hooks/post-receive :

#!/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
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) :

git clone ~/gitBareRepos/portfolio-generator.git /var/www/portfolio-generator/public_html

5.4 Script deploy.sh

/var/www/portfolio-generator/deploy.sh :

#!/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
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) :

# --- 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 :

[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
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) :

    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.shnpm 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) :

    sudo npx --prefix /var/www/portfolio-generator/public_html playwright install-deps chromium
    
  3. Démarrer le service :

    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)

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 :

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 :
    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 :

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
    }
}
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) :

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 :

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

# ── 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 ──