# 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 `` 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 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à `/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 `` 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 `` 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)