Files
sucupira 3e60f48c5e feat: support du déploiement sous sous-chemin (paths.base)
- 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)
2026-07-21 13:41:10 -04:00

8.6 KiB

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

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.

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)

# sur le VPS
sudo apt install caddy
sudo mkdir -p /srv/lage-chat-control

/etc/caddy/Caddyfile :

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

sudo apt install nginx certbot python3-certbot-nginx

/etc/nginx/sites-available/lage-chat-control :

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

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

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)