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:
485
docs/deploiement-production.md
Normal file
485
docs/deploiement-production.md
Normal 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 | ~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=<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 ──
|
||||
```
|
||||
Reference in New Issue
Block a user