📍 Article principal du cluster : Svelte 5 et SvelteKit 2 en production : guide complet 2026
Introduction
Une startup edtech de Cotonou veut déployer son LMS SvelteKit 2 sans payer 50 € par mois à Vercel. Budget cible : 5 € HT par mois. Solution : un VPS Hetzner CX22 à 4,49 € HT (~2 950 FCFA), Caddy en reverse proxy avec HTTPS automatique, déploiement par rsync et mise à jour zéro-downtime via systemd. Ce tutoriel détaille la configuration complète, du provisioning du VPS jusqu’à la mise en place de Cloudflare devant pour absorber les pics. À la fin, vous avez une stack production-ready, sauvegardée, supervisée, qui tient 5 000 utilisateurs actifs.
Prérequis
- Compte Hetzner Cloud (création ici, paiement carte ou SEPA)
- Application SvelteKit 2 fonctionnelle avec
adapter-node - Domaine sur Cloudflare ou registrar avec accès DNS
- Connaissance basique SSH et Linux (Debian 12/13 ou Ubuntu 24.04)
- Niveau : intermédiaire — Temps : 1 h 30 pour la première mise en ligne
Étape 1 — Provisionner le VPS Hetzner
Connecté sur la console Hetzner, créer un serveur avec ces choix :
- Localisation : Falkenstein (FSN1) ou Nuremberg (NBG1) — latence vers Dakar 90-120 ms ; Helsinki (HEL1) à éviter (latence 180+ ms)
- Image : Debian 13 ou Ubuntu 24.04 LTS
- Type : CX22 (2 vCPU, 4 Go RAM, 40 Go SSD) — 4,49 € HT/mois — suffisant pour ~5 000 utilisateurs/jour
- Réseau : IPv4 + IPv6 (l’IPv4 facture 0,50 €/mois — utile)
- Clé SSH : ajouter sa clé publique (jamais de mot de passe en SSH)
- Firewall : créer un firewall qui n’ouvre que 22, 80, 443
Une fois le serveur créé, on note l’IP et on se connecte : ssh root@1.2.3.4. Premier réflexe : créer un utilisateur non-root.
adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
Désactiver l’auth root et l’auth par mot de passe dans /etc/ssh/sshd_config : PermitRootLogin no et PasswordAuthentication no. Recharger : systemctl reload ssh. Voir notre tutoriel VPS SSH hardening complet pour le détail.
Étape 2 — Installer Caddy comme reverse proxy
Caddy est préféré à nginx pour deux raisons : HTTPS automatique via Let’s Encrypt sans configuration, et fichier de config trivial à lire. Installation :
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy
Configurer DNS : créer un enregistrement A pointant app.example.sn vers l’IP du VPS. Attendre 5 minutes la propagation.
Editer /etc/caddy/Caddyfile :
app.example.sn {
reverse_proxy localhost:3000
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
log {
output file /var/log/caddy/access.log
format json
}
}
sudo systemctl reload caddy. À ce stade Caddy obtient automatiquement un certificat Let’s Encrypt et écoute sur 443. Sans backend, on a une 502 — normal, on lance Node ensuite.
Étape 3 — Installer Node 22 LTS via NVM
En tant qu’utilisateur deploy :
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 22
nvm alias default 22
node --version # v22.x.x
Étape 4 — Build et déploiement par rsync
Sur le poste de dev, configurer SvelteKit pour adapter-node :
npm i -D @sveltejs/adapter-node
// svelte.config.js
import adapter from '@sveltejs/adapter-node';
export default { kit: { adapter: adapter({ out: 'build' }) } };
Script de déploiement deploy.sh :
#!/usr/bin/env bash
set -euo pipefail
HOST="deploy@app.example.sn"
APP_DIR="/home/deploy/app"
npm run build
rsync -az --delete build/ ${HOST}:${APP_DIR}/build-staging/
rsync -az package.json package-lock.json ${HOST}:${APP_DIR}/
ssh ${HOST} "cd ${APP_DIR} && npm ci --omit=dev"
ssh ${HOST} "ln -sfn ${APP_DIR}/build-staging ${APP_DIR}/build-current && sudo systemctl restart sveltekit-app.service"
echo "✓ Déployé"
Cette stratégie symlink + restart donne un downtime ~200 ms — suffisant pour la majorité des SaaS. Pour vraiment zéro-downtime, voir étape 6.
Étape 5 — Service systemd
Sur le serveur, créer /etc/systemd/system/sveltekit-app.service :
[Unit]
Description=SvelteKit App
After=network.target
[Service]
Type=simple
User=deploy
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=ORIGIN=https://app.example.sn
Environment=BODY_SIZE_LIMIT=5M
WorkingDirectory=/home/deploy/app
ExecStart=/home/deploy/.nvm/versions/node/v22.10.0/bin/node /home/deploy/app/build-current/index.js
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now sveltekit-app
sudo systemctl status sveltekit-app
journalctl -u sveltekit-app -f # logs en direct
Pour autoriser deploy à redémarrer le service sans mot de passe sudo : echo "deploy ALL=NOPASSWD: /bin/systemctl restart sveltekit-app" | sudo tee /etc/sudoers.d/deploy.
Étape 6 — Mise à jour zéro-downtime
Pour passer de ~200 ms de coupure à 0 ms, on lance deux instances en parallèle sur deux ports (3000, 3001), Caddy fait du load-balancing avec health-check, et le déploiement permute. Configuration Caddy étendue :
app.example.sn {
reverse_proxy localhost:3000 localhost:3001 {
lb_policy first
health_uri /api/healthz
health_interval 5s
fail_duration 30s
}
}
Côté SvelteKit, ajouter un endpoint /api/healthz qui retourne 200 si la base est joignable. Le script de déploiement met à jour l’instance 3001 d’abord, attend qu’elle réponde 200, puis met à jour 3000. Aucune requête perdue.
Étape 7 — Cloudflare devant
Activer Cloudflare devant le domaine apporte trois bénéfices : cache des assets statiques côté edge (latence quasi nulle depuis Lagos, Accra, Abidjan grâce au PoP local), protection DDoS gratuite, masquage de l’IP réelle du VPS. Configuration : enregistrement A proxifié (nuage orange), mode SSL « Full (strict) », cache standard. Les routes API restent dynamiques. Pour les pages statiques (blog, doc), on active « Cache Everything » via une page rule, gain de TTFB de 80 ms en moyenne depuis Dakar.
Étape 8 — Sauvegardes automatisées
Stratégie 3-2-1 minimale : snapshot Hetzner quotidien (gratuit dans le plan), sauvegarde PostgreSQL chiffrée vers Hetzner Storage Box, et copie hebdomadaire vers Backblaze B2. Le script backup.sh :
#!/usr/bin/env bash
DATE=$(date +%Y%m%d)
pg_dump -Fc app_db | gpg --batch --passphrase-file /etc/backup-pass --symmetric -o /tmp/db-${DATE}.dump.gpg
rclone copy /tmp/db-${DATE}.dump.gpg storagebox:backups/
rm /tmp/db-${DATE}.dump.gpg
Cron : 0 3 * * * /home/deploy/backup.sh. Tester la restauration tous les trois mois — un backup non testé est un backup illusoire.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
502 Bad Gateway après systemctl restart |
Variable ORIGIN manquante |
Ajouter Environment=ORIGIN=https://app.example.sn |
| Caddy ne charge pas le certificat | DNS non propagé ou ports 80/443 fermés | Vérifier DNS, ouvrir firewall |
| Erreur « EACCES port 3000 » | Port déjà occupé | ss -tulpn | grep 3000 puis kill |
| Build local OK, prod plante | Variable d’env $env/static/private manquante |
Définir dans le service systemd |
| Mémoire insuffisante au build | CX22 limité, build local recommandé | Builder en local, rsync le build/ |
| Logs Caddy vides | Permissions /var/log/caddy |
chown -R caddy:caddy /var/log/caddy |
Adaptation au contexte ouest-africain
Trois ajustements concrets pour les déploiements pilotés depuis Dakar, Abidjan ou Bamako. Premièrement, le build local plutôt que sur le VPS — la connexion CI vers GitHub depuis FSN1 est plus rapide que votre fibre Sonatel ou Orange CI, mais le build npm consomme beaucoup de bande passante VPS facturée. Builder en local, pousser le résultat compilé. Deuxièmement, configurer un mirror npm proche : npm config set registry https://registry.npmmirror.com ou un Verdaccio personnel. Troisièmement, surveiller les coupures Sonatel/MTN — un cron toutes les 5 minutes via Uptime Kuma hébergé sur un autre VPS chez Contabo permet de détecter immédiatement quand votre app n’est plus joignable depuis l’Afrique alors qu’elle l’est depuis l’Europe.
Tutoriels frères
- Maîtriser les runes Svelte 5
- Formulaires SvelteKit 2 avec form actions et Zod
- Tester SvelteKit avec Vitest et Playwright
Pour aller plus loin
- 🔝 Pilier : Svelte 5 et SvelteKit 2 en production
- Articles connexes : Hetzner Cloud Afrique · VPS hardening 2026
- Doc officielle : Caddy · adapter-node
FAQ
CX22 suffit-il vraiment pour 5 000 users actifs ?
Oui pour un SaaS B2B classique (CRM, dashboard). Pour 5 000 utilisateurs concurrents en lecture lourde, il faudrait CX32. Le vrai goulot reste souvent la base de données plus que SvelteKit lui-même.
Pourquoi pas Docker ?
Docker ajoute une couche de complexité (build, registre, orchestration) qui n’apporte rien pour une app monolithique sur un seul VPS. systemd + Node natif sont plus simples à diagnostiquer en cas de panne.
Et PM2 à la place de systemd ?
PM2 fonctionne mais doublonne ce que systemd fait déjà. Préférer systemd qui est natif au système et au journal.
Comment migrer depuis Vercel sans downtime ?
Pointer un sous-domaine staging vers le VPS, tester, puis basculer le DNS racine. TTL bas (60 s) pendant la bascule.
Faut-il configurer un swap ?
Oui, 2 Go de swap sur CX22 évitent les OOM lors des builds locaux occasionnels. fallocate -l 2G /swapfile puis mkswap et swapon.
Supervision et monitoring en production
Une application déployée sans monitoring est une application qu’on découvre cassée par les clients. Trois couches de supervision se complètent et coûtent zéro à mettre en place sur cette stack. La première couche, la plus essentielle, c’est la disponibilité externe. Un service comme Uptime Kuma installé sur un second petit VPS (ou même un container chez un fournisseur tiers) interroge l’URL publique toutes les 60 secondes et notifie en cas d’échec. La seconde couche, ce sont les métriques système locales du VPS : charge CPU, RAM disponible, espace disque, IO disque. Netdata ou Glances se déploient en cinq minutes et exposent un dashboard web léger qu’on protège derrière un mot de passe Caddy. La troisième couche, ce sont les logs applicatifs : journalctl pour les logs systemd, agrégation optionnelle vers Loki ou Grafana Cloud Free pour la recherche.
Pour les SaaS qui servent des clients ouest-africains, surveiller la latence depuis l’Afrique est non-négociable. La latence depuis l’Europe peut être parfaite alors que celle depuis Dakar est désastreuse — typiquement à cause d’un cache Cloudflare mal configuré ou d’un PoP saturé. Configurer une sonde Uptime Kuma hébergée chez Contabo Atlanta ou Hetzner FSN ne donne pas la vraie expérience client. Préférer un fournisseur avec présence africaine : Africloud, Server Hosting Senegal, ou un VPS Vultr Johannesburg pour les sondes périodiques.
Côté alerting, la règle est de ne pas se laisser noyer. Un seul canal critique (Telegram, WhatsApp Business via Twilio, ou SMS via Hubtel/Africa’s Talking) pour les pannes vraies. Les alertes de seuils doux (CPU à 80 % pendant 10 minutes) vont dans un canal séparé. Sans cette discipline, l’équipe finit par couper les notifications et rate la vraie panne.
Sécurité niveau 2 : fail2ban, audit, TLS strict
Au-delà du SSH hardening de base, trois mesures supplémentaires limitent l’exposition. Fail2ban surveille les tentatives échouées et banni temporairement les IP fautives — installation apt install fail2ban et configuration par défaut suffisent pour SSH. Pour Caddy, on peut écrire un filtre custom qui scrute le log JSON et banni les IP qui tentent des chemins louches (/wp-login.php, /.env, /phpinfo). L’audit du système se fait avec auditd qui logue les modifications de fichiers sensibles : /etc/passwd, /etc/sudoers, le code applicatif. En cas d’incident, on dispose d’une trace forensic.
Le TLS strict mérite attention. Caddy utilise par défaut TLS 1.2+ mais on peut imposer TLS 1.3 uniquement, désactiver les ciphers faibles, et activer HSTS preload. Vérifier sa configuration sur SSL Labs après chaque changement Caddy — viser une note A+. Pour les applications fintech ou santé soumises à compliance UEMOA-CEDEAO, on documente la configuration TLS dans le dossier de conformité.
Calcul de coût total et alternatives
Coût total mensuel pour cette stack : VPS Hetzner CX22 à 4,49 € HT (~2 950 FCFA), IPv4 0,50 € HT (~330 FCFA), Storage Box optionnelle pour backups 3,49 € HT (~2 290 FCFA). Total : 5 à 8 € HT par mois selon options, soit environ 5 600 FCFA. Ajouter Cloudflare Free (0 €) et Backblaze B2 pour archives (~1 €/mois pour 50 Go) — total très en dessous des 20 € minimum chez Vercel ou Render équivalent. Le paiement se fait en SEPA depuis l’Europe, ou via PayPal/Wise depuis l’Afrique pour les comptes Hetzner créés depuis l’étranger — voir notre guide Paiement Hetzner depuis l’Afrique.
Alternatives valables si Hetzner ne convient pas : OVH SBG ou GRA pour la souveraineté française, Contabo VPS S pour le prix encore plus serré (3,99 €/mois) au prix d’une stabilité moins constante, Scaleway pour l’écosystème complet mais plus cher. Render et Railway sont valides pour les équipes qui ne veulent rien administrer mais facturent 2-3× plus pour la même charge.
Pipeline CI/CD GitHub Actions
Pour automatiser le déploiement, on connecte GitHub Actions au VPS via une clé SSH dédiée. Le workflow lance les tests, build localement chez le runner GitHub, puis pousse via rsync. Coût : zéro, dans les limites du plan gratuit (2 000 minutes/mois). La sécurité repose sur trois principes : la clé SSH est limitée à l’utilisateur deploy, sans privilège root direct ; le secret est stocké dans GitHub Secrets, jamais commité ; le déploiement est conditionné à la branche main uniquement.
L’avantage d’un déploiement piloté depuis GitHub Actions plutôt que depuis le poste local : reproductibilité. Le build se fait toujours dans le même environnement (Node 22.x sur Ubuntu 24.04), avec les mêmes dépendances figées par package-lock.json. Aucune surprise du type « ça marche chez Aïssatou mais pas chez Mamadou ». Pour les équipes distribuées entre Dakar et Abidjan, c’est essentiel.
Une étape « smoke test » après déploiement valide que l’app répond bien. Un simple curl https://app.example.sn/api/healthz qui doit retourner 200 dans les 10 secondes après bascule. Si échec, le job rollback symlink-swap réactive l’ancienne version automatiquement.
Multi-région et failover
Pour les SaaS critiques qui ne tolèrent pas une heure d’indisponibilité, on déploie en deux régions Hetzner (FSN1 + NBG1) avec failover DNS Cloudflare Load Balancer (gratuit jusqu’à 50 000 requêtes/mois sur le plan Pro 20 €/mois). La base de données reste dans une seule région (PostgreSQL master), avec une réplique asynchrone dans la seconde. Cette configuration ajoute 5 €/mois au budget et divise par cinq le risque de panne globale. Pour la majorité des projets ouest-africains qui démarrent, c’est over-engineering — à activer après la première année quand les revenus le justifient.