3e60f48c5e
- Liens et assets via {base}, polices déplacées vers src/lib/assets (hachage Vite)
- Service worker base-aware, précache sur les pages prérendues
- CSP hash documentée : l'hôte ne doit pas fixer script-src
- Docs : déploiement Apache/YunoHost (DEPLOYMENT, APACHE_AUTOINDEX)
207 lines
8.6 KiB
Markdown
207 lines
8.6 KiB
Markdown
# Déployer lage-chat-control sur un VPS
|
|
|
|
Le site est **100 % statique** (ADR-001) : le déploiement consiste à poser le
|
|
contenu du dossier `frontend/build/` derrière un serveur web. Pas de base de
|
|
données, pas de service Node à maintenir, pas de tâche cron. Un miroir du site
|
|
= une copie du dossier.
|
|
|
|
## 0. Prérequis
|
|
|
|
- Un VPS (1 vCPU / 512 Mo suffisent très largement) sous Debian/Ubuntu
|
|
- Un nom de domaine pointant sur le VPS (enregistrement A/AAAA)
|
|
- En local : Node ≥ 22.12 et pnpm (le build se fait **sur ta machine**,
|
|
le VPS n'a besoin ni de Node ni de pnpm)
|
|
|
|
## 1. Construire le site
|
|
|
|
```sh
|
|
git clone https://labola.o-k-i.net/cyber-mawonaj/lage-chat-control.git
|
|
cd lage-chat-control/frontend
|
|
pnpm install
|
|
pnpm build # produit frontend/build/ (~60 pages statiques)
|
|
```
|
|
|
|
> **Racine de domaine** : le site est servi à la racine de
|
|
> `https://chatcontrol.o-k-i.net/` (YunoHost, voir §5) — aucun `paths.base`.
|
|
> Pour un déploiement sous un sous-chemin, ajoute `paths: { base: '/prefixe' }`
|
|
> dans `frontend/svelte.config.js` avant `pnpm build` et adapte les règles
|
|
> `try_files` côté serveur (ex. nginx : `location /prefixe/ { … }`).
|
|
|
|
> **Mutualisé Apache (o2switch)** : si les liens internes affichent
|
|
> « Index of /… » (listing de dossier) au lieu des pages, c'est le conflit
|
|
> fichier/dossier classique — voir [APACHE_AUTOINDEX.md](./APACHE_AUTOINDEX.md).
|
|
|
|
> **CSP et hydratation (critique)** : SvelteKit démarre côté navigateur via
|
|
> un script *inline* (qui embarque les données de la page). Un en-tête HTTP
|
|
> `Content-Security-Policy` contenant `script-src 'self'` — comme celui posé
|
|
> par le compte o2switch — **bloque ce script et tue toute l'interactivité**
|
|
> (recherche, filtres, sélecteur de langue), sans message visible. Chaque page
|
|
> prérendue porte donc sa propre `<meta>` CSP avec l'empreinte (`sha256-…`) de
|
|
> son script inline (`csp.mode: 'hash'` dans `frontend/svelte.config.js`).
|
|
> Règle côté serveur : **ne jamais fixer `script-src` dans l'en-tête HTTP** —
|
|
> le retirer de la config cPanel/`.htaccess`/reverse proxy (le reste de la CSP
|
|
> peut y rester). Si l'hébergeur impose un `script-src` en en-tête, le régler
|
|
> sur `script-src 'self' 'unsafe-inline'` (plus faible, mais fonctionnel).
|
|
|
|
## 2. Option A — Caddy (recommandé : TLS automatique)
|
|
|
|
```sh
|
|
# sur le VPS
|
|
sudo apt install caddy
|
|
sudo mkdir -p /srv/lage-chat-control
|
|
```
|
|
|
|
`/etc/caddy/Caddyfile` :
|
|
|
|
```caddy
|
|
chatcontrol.o-k-i.net {
|
|
root * /srv/lage-chat-control
|
|
file_server
|
|
encode zstd gzip
|
|
|
|
# SvelteKit adapter-static : pages pré-générées + fallback
|
|
try_files {path} {path}.html {path}/index.html /404.html
|
|
|
|
header {
|
|
# le service worker doit pouvoir se mettre à jour
|
|
Cache-Control "no-cache" /service-worker.js
|
|
# assets immuables (noms hachés)
|
|
Cache-Control "public, max-age=31536000, immutable" /_app/immutable/*
|
|
# en-têtes de sécurité
|
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
X-Content-Type-Options "nosniff"
|
|
Referrer-Policy "no-referrer"
|
|
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
|
# CSP stricte : le site n'a aucune dépendance externe.
|
|
# NE PAS remettre script-src ici : il est porté par la <meta> CSP de
|
|
# chaque page (empreinte du script inline de démarrage, différente
|
|
# d'une page à l'autre — inexprimable dans un en-tête statique).
|
|
Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'"
|
|
}
|
|
}
|
|
```
|
|
|
|
Le TLS (Let's Encrypt) est automatique, rien d'autre à faire.
|
|
|
|
## 3. Option B — nginx
|
|
|
|
```sh
|
|
sudo apt install nginx certbot python3-certbot-nginx
|
|
```
|
|
|
|
`/etc/nginx/sites-available/lage-chat-control` :
|
|
|
|
```nginx
|
|
server {
|
|
server_name chatcontrol.o-k-i.net;
|
|
root /srv/lage-chat-control;
|
|
index index.html;
|
|
|
|
# pages pré-générées : /outils/ → outils/index.html, fallback 404
|
|
location / {
|
|
try_files $uri $uri/ $uri.html $uri/index.html /404.html;
|
|
}
|
|
|
|
location /_app/immutable/ {
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
}
|
|
|
|
location = /service-worker.js {
|
|
add_header Cache-Control "no-cache";
|
|
}
|
|
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header Referrer-Policy "no-referrer" always;
|
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'" always;
|
|
# NB : pas de script-src ici — voir le commentaire CSP de l'option A.
|
|
|
|
gzip on;
|
|
gzip_types text/html text/css application/javascript application/json image/svg+xml;
|
|
}
|
|
```
|
|
|
|
```sh
|
|
sudo ln -s /etc/nginx/sites-available/lage-chat-control /etc/nginx/sites-enabled/
|
|
sudo nginx -t && sudo systemctl reload nginx
|
|
sudo certbot --nginx -d chatcontrol.o-k-i.net # TLS
|
|
```
|
|
|
|
## 4. Publier (et republier)
|
|
|
|
```sh
|
|
# depuis ta machine, après chaque `pnpm build`
|
|
rsync -avz --delete frontend/build/ vps:/srv/lage-chat-control/
|
|
```
|
|
|
|
C'est l'intégralité du « pipeline de déploiement ». Pour automatiser :
|
|
un runner Forgejo Actions sur labola peut exécuter build + rsync à chaque
|
|
push sur `main` (à ajouter quand le besoin se présente, pas avant).
|
|
|
|
## 5. Option C — YunoHost (cible de production)
|
|
|
|
Le site tourne en production sur **`chatcontrol.o-k-i.net`** via l'app
|
|
**`my_webapp`** (site statique) :
|
|
|
|
1. Installe `my_webapp` depuis le catalogue YunoHost avec le domaine
|
|
`chatcontrol.o-k-i.net`, le chemin `/`, un accès **public** (visiteurs
|
|
compris), sans PHP ni base de données.
|
|
2. Copie le contenu de `frontend/build/` dans le `www/` de l'app, avec
|
|
l'utilisateur SFTP dédié créé par l'app (son nom exact figure dans la
|
|
page de l'app, section « Accès SFTP ») :
|
|
```sh
|
|
rsync -avz --delete frontend/build/ my_webapp@chatcontrol.o-k-i.net:www/
|
|
```
|
|
3. C'est tout : la conf nginx générée sert déjà `<route>/index.html` (avec
|
|
redirection automatique vers le `/` final). YunoHost gère TLS, HSTS et
|
|
le renouvellement des certificats.
|
|
|
|
Durcissements optionnels (panneau de config de l'app, ou
|
|
`/etc/nginx/conf.d/chatcontrol.o-k-i.net.d/my_webapp.conf`) :
|
|
|
|
- `error_page 404 /404.html;` — notre page 404 plutôt que celle de nginx ;
|
|
- cache long sur les assets immuables :
|
|
`location /_app/immutable/ { add_header Cache-Control "public, max-age=31536000, immutable"; }`
|
|
- revalidation du service worker :
|
|
`location = /service-worker.js { add_header Cache-Control "no-cache"; }`
|
|
|
|
Ne pose **pas** d'en-tête CSP globale côté YunoHost : la `<meta>` CSP de
|
|
chaque page suffit (voir l'encadré CSP en §1). À noter : l'overlay du portail
|
|
YunoHost, injecté uniquement pour les visiteurs déjà connectés au SSO, est
|
|
bloqué par cette CSP — cosmétique, sans effet pour les visiteurs anonymes.
|
|
|
|
## 6. Option D — Docker
|
|
|
|
Le dépôt n'embarque volontairement aucun `Dockerfile` (un site 100 % statique
|
|
n'en a pas besoin — c'est un dossier de fichiers). Si le reste de l'infra OKI
|
|
tourne déjà en conteneurs, ce `Dockerfile` minimal (nginx durci, sans build
|
|
step) suffit :
|
|
|
|
```dockerfile
|
|
FROM nginx:alpine
|
|
COPY frontend/build/ /usr/share/nginx/html/
|
|
COPY docs/nginx.conf.example /etc/nginx/conf.d/default.conf
|
|
```
|
|
|
|
Reprends le bloc `server {}` de l'option B ci-dessus dans
|
|
`docs/nginx.conf.example` (à créer), en retirant `server_name`/`certbot`
|
|
(le TLS se termine en amont, sur un reverse proxy ou le load balancer).
|
|
Sinon, les options A/B sont plus simples et suffisent dans l'immense
|
|
majorité des cas.
|
|
|
|
## 7. Vérifier après mise en ligne
|
|
|
|
- [ ] `curl -s https://chatcontrol.o-k-i.net/outils/ | grep -o 'http-equiv="Content-Security-Policy"'`
|
|
→ la `<meta>` CSP par page est présente (elle porte `script-src` + empreinte)
|
|
- [ ] `curl -sI https://chatcontrol.o-k-i.net/outils/ | grep -i content-security`
|
|
→ l'en-tête HTTP éventuelle ne contient **pas** de `script-src` bloquant
|
|
- [ ] Ouvrir `/outils`, taper « signal » dans la recherche → la liste
|
|
se filtre sans rechargement (preuve que l'hydratation fonctionne) ;
|
|
cliquer une catégorie → l'URL gagne `?cat=…` et la liste se filtre
|
|
- [ ] DevTools > Console : aucune erreur CSP (« violates the following Content
|
|
Security Policy directive ») au chargement
|
|
- [ ] DevTools > Application > Service worker : enregistré ; passer hors-ligne
|
|
et recharger une fiche déjà visitée → elle s'affiche
|
|
- [ ] observatory.mozilla.org : note A/A+ attendue avec les en-têtes ci-dessus
|
|
- [ ] Aucune requête sortante vers un domaine tiers (onglet Réseau)
|