← Blogue

Déployer SvelteKit sur Hetzner

Railyard

20 septembre 2026 · 8 min de lecture

Suivez ça sur votre propre serveur. Commencer gratuitement, sans carte

Vous avez une application SvelteKit et vous voulez qu’elle réponde sur votre propre domaine en HTTPS, sur un serveur que vous payez au mois plutôt qu’à la requête. Voici la version entièrement manuelle : un vrai serveur Hetzner, un Dockerfile construit autour de @sveltejs/adapter-node, et Caddy devant pour les certificats. À la fin, vous aurez vite build qui tourne dans une petite image de production, servie derrière HTTPS, avec une façon répétable de reconstruire et de redémarrer.

Ce qu’il vous faut

  • Un compte Hetzner Cloud et un jeton API.
  • Un serveur : un CX23 (2 vCPU, 4 Go de RAM, 40 Go NVMe, 5,99 €/mois hors TVA) fait tourner sans problème une application SvelteKit petite à moyenne en exécution. Si vite build lui-même manque de mémoire sur un gros site, redimensionnez temporairement vers un CX33 (4 vCPU, 8 Go de RAM, 8,99 €/mois) pour la construction, puis redescendez ensuite.
  • Un domaine ou sous-domaine avec un enregistrement A que vous pouvez pointer vers le serveur.
  • Une application SvelteKit utilisant @sveltejs/adapter-node, sur Node.js 20.19 ou plus récent, la version minimale exigée par Vite 7 (et donc par SvelteKit actuel).
  • Un accès SSH au serveur.

Étape 1 : créer le serveur

Depuis la console Hetzner Cloud, créez un serveur de type cx23 (ou votre choix redimensionné) sous Ubuntu 24.04, ajoutez votre clé SSH à la création, et notez l’IP publique une fois qu’il tourne. Installez Docker et le plugin Compose par SSH :

curl -fsSL https://get.docker.com | sh
apt-get install -y docker-compose-plugin

Étape 2 : pointer le DNS vers le serveur

Créez un enregistrement A pour votre domaine ou sous-domaine pointant vers l’IP du serveur, par exemple app.yourdomain.com. Laissez-lui quelques minutes pour se propager avant l’étape 6, puisque Caddy a besoin que ça résolve correctement pour demander un certificat.

Étape 3 : passer à adapter-node

SvelteKit choisit son adaptateur depuis svelte.config.js. L’adapter-auto par défaut devine une cible à partir de votre plateforme de déploiement, qui n’a aucune idée que vous construisez une simple image Docker, alors remplacez-le explicitement par l’adaptateur Node :

npm install -D @sveltejs/adapter-node@6
import adapter from "@sveltejs/adapter-node";

export default {
  kit: {
    adapter: adapter(),
  },
};

Ce guide utilise @sveltejs/adapter-node 6.0.0 avec SvelteKit 3.1.0 et Svelte 5.57.2, les versions actuelles au moment de l’écriture. Exécuter npm run build maintenant produit un répertoire build/ au lieu de ce que adapter-auto aurait choisi, contenant build/index.js et build/handler.js, les deux points d’entrée décrits par la documentation de l’adaptateur.

Étape 4 : écrire le Dockerfile

ARG NODE_VERSION=24-alpine

FROM node:${NODE_VERSION} AS deps
WORKDIR /app
COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
RUN if [ -f package-lock.json ]; then npm ci; \
    elif [ -f yarn.lock ]; then corepack enable yarn && yarn install --frozen-lockfile; \
    elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install --frozen-lockfile; \
    else echo "No lockfile found" && exit 1; fi

FROM node:${NODE_VERSION} AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
RUN npm prune --omit=dev

FROM node:${NODE_VERSION} AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOST=0.0.0.0
COPY --from=builder /app/build ./build
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER node
EXPOSE 3000
CMD ["node", "build"]

node:24-alpine suit Node 24, la ligne LTS actuellement active. La disposition en trois étapes garde l’installation des dépendances, la construction proprement dite, et l’image d’exécution séparées ; npm prune --omit=dev dans l’étape de construction retire les devDependencies avant qu’elles soient copiées dans l’image d’exécution, puisque la sortie d’adapter-node a encore besoin de ses node_modules de production à côté, contrairement à un bundle entièrement autonome. USER node fait tourner le processus comme l’utilisateur non root node déjà présent dans les images Node officielles.

Étape 5 : écrire le fichier Compose et le Caddyfile

services:
  web:
    build: .
    restart: unless-stopped
    environment:
      NODE_ENV: production
      PORT: "3000"
      HOST: "0.0.0.0"
      ORIGIN: "https://app.yourdomain.com"
    expose:
      - "3000"

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - web

volumes:
  caddy-data:
  caddy-config:
app.yourdomain.com {
	reverse_proxy web:3000
}

PORT et HOST sont ce que le serveur intégré d’adapter-node lit pour décider où écouter ; HOST=0.0.0.0 est ce qui le rend joignable depuis le service caddy à côté, plutôt que seulement depuis l’intérieur de son propre conteneur. ORIGIN compte chaque fois que SvelteKit a besoin de connaître sa propre URL absolue, le plus souvent pour valider l’en-tête Origin sur les actions de formulaire, une protection contre la falsification de requête intersite ; le laisser non défini derrière un proxy inverse est la cause la plus courante d’actions de formulaire qui échouent en production tout en fonctionnant bien en développement local. Toute autre variable que votre code côté serveur lit avec $env/dynamic/private va dans ce même bloc environment: ; seules les variables préfixées PUBLIC_ et lues via $env/static/public ou $env/dynamic/public sont intégrées pour le client au moment de la construction.

Vérifiez que le fichier s’analyse correctement :

docker compose config

Vous devriez voir la configuration entièrement résolue affichée sans ligne d’erreur.

Étape 6 : déployer

docker compose up -d --build

La première exécution construit l’image, ce qui est la partie la plus lente, puis démarre les deux conteneurs et Caddy demande son certificat. Suivez les journaux pour confirmer que les deux ont réussi :

docker compose logs -f

Vous devriez voir une ligne comme Listening on 0.0.0.0:3000 provenant du conteneur web, et le journal de Caddy inclure l’obtention réussie d’un certificat pour votre domaine. Visitez https://app.yourdomain.com et confirmez que l’application se charge avec un certificat valide, sans avertissement du navigateur.

Pour un changement de code ultérieur, la même commande reconstruit seulement l’image qui a changé :

docker compose up -d --build web

Ce n’est pas sans interruption. Compose arrête l’ancien conteneur web avant de démarrer le nouveau, donc il y a un écart mesuré en le temps que prend le processus Node à démarrer et à servir sa première requête. Si cet écart compte pour votre trafic, la solution standard est de faire tourner deux conteneurs d’application derrière l’équilibrage de charge propre à Caddy et de les remplacer un à la fois, plus d’infrastructure que ne couvre ce guide à serveur unique.

Le raccourci : avec Railyard

SvelteKit n’a pas son propre chemin de construction sur Railyard, et c’est voulu : une application SvelteKit construite avec adapter-node ou laissée sur adapter-auto ressemble, de l’extérieur, exactement à n’importe quel autre serveur Node.js qui lit package.json et démarre avec node build. La détection de dépôt de Railyard lit votre fichier de verrouillage pour choisir npm, pnpm, yarn ou bun, confirme @sveltejs/adapter-node (ou adapter-auto qui s’y résout) et un script de construction correspondant à vite build ou svelte-kit build, puis fait tourner l’application de la même façon que les étapes 4 à 6 le font à la main, sans que vous ayez à écrire de Dockerfile du tout. Si votre dépôt fournit déjà son propre Dockerfile, c’est celui-là qui est utilisé directement.

Déployez directement via le chemin Node.js : /fr/deploy/nodejs-to-hetzner. Voyez déployer depuis un dépôt pour savoir comment la détection et l’étape de construction fonctionnent de bout en bout, et le démarrage rapide si c’est votre premier déploiement sur Railyard.

Dépannage

La construction échoue, le processus se fait tuer en plein milieu, sans erreur claire. Presque toujours un manque de mémoire pendant vite build sur un petit serveur ; redimensionnez temporairement comme mentionné dans « Ce qu’il vous faut », ou ajoutez du swap comme solution de secours moins coûteuse (fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile).

Les actions de formulaire échouent avec un 403 en production mais fonctionnent en local. C’est presque toujours un ORIGIN manquant ou erroné. SvelteKit vérifie l’en-tête Origin de la requête contre cette valeur pour toute soumission de formulaire autre que GET ; derrière un proxy inverse, l’application ne peut pas le déduire toute seule, donc il doit être défini explicitement à l’URL HTTPS exacte que voient les utilisateurs.

Le HTTPS ne s’émet jamais, Caddy continue de journaliser un délai d’attente dépassé contre Let’s Encrypt. Confirmez que l’enregistrement DNS A de l’étape 2 résout réellement vers l’IP de ce serveur depuis l’extérieur de votre propre réseau, et que les ports 80 et 443 sont ouverts dans le pare-feu de Hetzner ainsi que dans toute règle ufw sur la machine elle-même.

L’application se construit et démarre, mais la page ne se charge jamais, Caddy expire. Vérifiez que HOST=0.0.0.0 est bien présent dans le conteneur en cours d’exécution (docker compose exec web env) ; un serveur qui n’écoute que sur localhost dans son propre espace de noms réseau est injoignable depuis le service caddy même si les deux conteneurs tournent.

Les variables PUBLIC_ affichent l’ancienne valeur après que vous les avez changées. Elles sont intégrées dans le bundle client au moment de la construction, donc en changer une nécessite de relancer docker compose up -d --build web ; redémarrer seul le conteneur ne fait rien pour elles.

FAQ

Ai-je besoin d’adapter-node, ou est-ce qu’adapter-auto fonctionne aussi dans Docker ? adapter-auto ne se résout en un vrai adaptateur que lorsqu’il reconnaît la plateforme sur laquelle il tourne à partir des variables d’environnement que ces plateformes définissent ; une simple image Docker sur votre propre serveur n’en fait pas partie, donc ça doit être adapter-node explicitement, comme à l’étape 3.

Puis-je utiliser pnpm ou yarn au lieu de npm ? Oui, le Dockerfile de l’étape 4 fait déjà une branche selon le fichier de verrouillage présent, aucun changement nécessaire au-delà d’avoir ce fichier de verrouillage commité.

Est-ce que ça a besoin d’une base de données ? Pas par défaut. SvelteKit lui-même n’a aucune opinion sur la persistance ; si les fonctions load ou les actions de formulaire de votre application parlent à Postgres ou à une autre base de données, ajoutez-la comme son propre service Compose et passez sa chaîne de connexion dans le bloc environment: du service web de la même façon qu’ORIGIN est défini plus haut.

Que faire si mon application a des WebSockets ou des connexions de longue durée ? reverse_proxy dans Caddy relaie les en-têtes de mise à niveau WebSocket sans configuration supplémentaire, la même ligne unique à l’étape 5 couvre déjà ça.

En quoi est-ce différent d’un déploiement sur Vercel ? Fonctionnellement, votre application exécute le même code dans les deux cas ; ce que vous échangez, c’est une construction gérée et un réseau en périphérie contre le fait de posséder vous-même le serveur, le Dockerfile et le certificat, moins cher à trafic stable et plus de travail sur les marges (mise à l’échelle, prévisualisations, infrastructure d’optimisation d’images).

Le Dockerfile et le fichier Compose ici ne changent pas beaucoup d’un projet SvelteKit à l’autre. Ce qui varie, c’est ce que vos fonctions load et vos actions de formulaire lisent dans l’environnement à l’exécution, ça vaut la peine de le vérifier contre cette liste avant le premier vrai déploiement.

Pourquoi les gens choisissent Railyard pour ça

  • Votre propre compte infonuagique. Déployez sur DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou n'importe quel serveur Linux par SSH, facturé directement par votre fournisseur, sans majoration.
  • Un prix fixe par équipe. L'offre gratuite est gratuite pour toujours : un serveur plus un serveur de base de données, trois applications, sans carte. Pro coûte 19 $ par mois pour jusqu'à cinq serveurs et un nombre illimité d'applications. Team coûte 49 $ par mois avec des serveurs illimités. Aucuns frais par application, par déploiement, par siège ou par gigaoctet.
  • Des gabarits en un clic. Déployez des applications open source, comme des bases de données, des outils d'analyse et des outils de développement, sur votre propre serveur en un clic.
  • Des bases de données gérées. Postgres, MySQL et Redis, avec des sauvegardes planifiées, vérifiées par restauration, et la récupération à un point dans le temps.
  • Déploiement par push. Déployez depuis GitHub, GitLab ou la CLI, avec HTTPS automatique et des mises en production sans interruption.

Déployez ce dépôt sans Dockerfile

Collez l'URL du dépôt dans Railyard : la construction, les ports et les variables d'environnement sont détectés automatiquement sur un serveur Hetzner.

Offre gratuite, sans carte, et 14 jours de Pro pour chaque nouvelle équipe. Prix fixe par équipe.