← Blogue

Déployer Next.js sur Hetzner

Railyard

28 mai 2026 · 8 min de lecture

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

Vous avez une application Next.js et un compte Hetzner, et vous voulez que l’application réponde sur votre propre domaine en HTTPS, sans confier le tout à une plateforme qui facture à la requête. Voici la version entièrement manuelle : un vrai serveur, un Dockerfile qui produit une petite image de production, et Caddy devant pour les certificats. À la fin, vous aurez next build qui tourne en mode standalone dans un conteneur, servi derrière HTTPS, avec une façon reproductible de reconstruire et 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 € par mois hors TVA) fait tourner une application Next.js petite à moyenne sans problème à l’exécution ; si next build lui-même manque de mémoire sur une grosse application, redimensionnez temporairement vers un CX33 (4 vCPU, 8 Go de RAM, 8,99 € par mois) pour le build, puis redescendez après, ou construisez l’image sur une machine séparée et exécutez-la seulement sur le CX23.
  • Un domaine ou sous-domaine avec un enregistrement A que vous pouvez pointer vers le serveur.
  • Une application Next.js avec un script build et start dans package.json, sur Node.js 20.9 ou plus récent, qui est le minimum que Next.js exige actuellement.
  • 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é) sur 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.votredomaine.com. Laissez quelques minutes de propagation avant l’étape 6, puisque Caddy a besoin que ça résolve correctement pour demander un certificat.

Étape 3 : activer la sortie standalone

Le build de Next.js lui-même n’inclut les fichiers dont une page a besoin que si vous activez le traçage des fichiers de sortie. Ajoutez ceci à next.config.js (ou next.config.ts) :

module.exports = {
  output: "standalone",
}

Ça fait produire par next build un dossier .next/standalone contenant un server.js minimal et seulement les node_modules que votre application utilise réellement à l’exécution, plutôt que de nécessiter que l’arbre entier de node_modules soit copié dans l’image de production. Le serveur standalone ne copie pas automatiquement votre dossier public/ ni .next/static, les deux sont copiés explicitement dans la sortie standalone dans le Dockerfile ci-dessous.

Étape 4 : écrire le Dockerfile

Ceci suit le modèle de l’exemple Docker officiel de Next.js, simplifié :

ARG NODE_VERSION=22-slim

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 . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

FROM node:${NODE_VERSION} AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
COPY --from=builder /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]

node:22-slim est une version LTS actuelle de Node.js, confortablement au-dessus du minimum de 20.9 que Next.js documente aujourd’hui. La disposition en trois étapes garde l’installation des dépendances, le build lui-même, et l’image d’exécution séparés, donc l’image finale ne contient que ce que output: "standalone" a tracé comme nécessaire plus les deux dossiers qu’il ne copie pas de lui-même. USER node exécute le processus comme l’utilisateur non-root node déjà présent dans les images Node officielles, plutôt que root, ce qui ne coûte rien et vaut la peine par défaut.

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

services:
  web:
    build: .
    restart: unless-stopped
    environment:
      NODE_ENV: production
      PORT: "3000"
    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.votredomaine.com {
  reverse_proxy web:3000
}

PORT et HOSTNAME sont les deux variables d’environnement que le server.js standalone de Next.js lit directement, HOSTNAME=0.0.0.0 (défini dans le Dockerfile) est ce qui fait écouter le serveur sur toutes les interfaces à l’intérieur du conteneur plutôt que seulement localhost, ce qui le rendrait sinon inatteignable depuis le service caddy juste à côté. Toute variable que votre application lit à l’exécution avec process.env, par opposition à une variable intégrée au moment du build, va aussi dans ce bloc environment: ; seules les variables préfixées NEXT_PUBLIC_ sont intégrées dans le bundle client pendant next build, tout le reste reste côté serveur uniquement et peut changer sans reconstruction.

É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 logs pour confirmer que le build et l’émission du certificat ont tous les deux réussi :

docker compose logs -f

Vous devriez voir le serveur Next.js journaliser une ligne comme Ready in <n>ms, et le log de Caddy inclure certificate obtained successfully pour votre domaine. Visitez https://app.votredomaine.com et confirmez que l’application se charge en HTTPS avec un certificat valide.

Pour un changement de code ultérieur, la même commande reconstruit juste l’image qui a changé et recrée ce seul conteneur :

docker compose up -d --build web

Ce n’est pas vraiment sans interruption, Compose arrête l’ancien conteneur web avant de démarrer le nouveau, donc il y a un écart mesuré par le temps que votre application prend pour démarrer et répondre à 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 de Caddy et de les remplacer un à la fois, ce qui est plus d’infrastructure que ce guide ne couvre pour une configuration à serveur unique.

Le raccourci : faites-le avec Railyard

Tout ce qui est dans les étapes 1 et 3 à 6 est ce que la détection Node de Railyard fait automatiquement quand vous déployez un dépôt plutôt que de bricoler le Dockerfile à la main. Pour un dépôt sans son propre Dockerfile, elle lit package.json, choisit votre gestionnaire de paquets d’après le fichier de verrouillage présent (npm, pnpm, yarn ou bun), et exécute votre script start comme processus web une fois le build terminé, tout ça sans que vous écriviez de Dockerfile. Si le dépôt fournit son propre Dockerfile, celui-ci est utilisé directement à la place du chemin automatique.

Le conteneur écoute sur le port 3000 par défaut spécifiquement pour un dépôt Next.js, ce qui correspond au port que le Dockerfile de ce guide expose. Sur Hetzner, le serveur lui-même peut être créé directement depuis Railyard avec la région et la taille que vous choisissez à la création, plutôt que via l’étape séparée de la console Cloud de l’étape 1.

Déployez directement : /fr/deployer/nextjs-sur-hetzner. Voir Déployer depuis un dépôt pour le fonctionnement complet de la détection et du build.

Dépannage

Le build échoue avec le processus tué en plein milieu, aucune erreur claire. C’est presque toujours une mémoire insuffisante pendant next build sur un petit serveur ; soit redimensionnez temporairement comme mentionné dans « Ce qu’il vous faut », soit ajoutez du swap comme solution de secours moins chère (fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile).

Le HTTPS ne s’émet jamais, Caddy journalise un délai d’attente dépassé contre Let’s Encrypt. Confirmez que l’enregistrement DNS A de l’étape 2 résout bien vers l’IP de ce serveur depuis l’extérieur de votre réseau, et que les ports 80 et 443 sont ouverts dans le pare-feu de Hetzner autant que dans les règles ufw de la machine elle-même, les deux doivent autoriser le trafic entrant pour que le défi ACME se termine.

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

Les variables NEXT_PUBLIC_ montrent encore l’ancienne valeur après les avoir changées. Elles sont intégrées dans le bundle JavaScript au moment du build, donc changer une d’elles nécessite de relancer docker compose up -d --build web, redémarrer seulement le conteneur sans reconstruire ne fait rien pour elles.

Le port 3000 (ou 80) est déjà utilisé sur l’hôte. Quelque chose d’autre y est déjà lié, souvent un ancien proxy inverse ou un conteneur restant d’une tentative précédente. sudo ss -tlnp | grep ':80\|:3000' montre ce qui l’occupe ; le fichier Compose de ce guide ne publie pas du tout le port 3000 vers l’hôte (il est seulement expose-é au service caddy), donc un conflit ici pointe généralement plutôt vers le port 80 ou 443.

FAQ

Ai-je besoin de output: "standalone", ou un next start normal fonctionne-t-il aussi dans Docker ? next start fonctionne très bien dans un conteneur, mais il a besoin que votre node_modules complet soit copié dedans, ce qui donne une image notablement plus grosse et un déploiement plus lent ; le mode standalone est le chemin officiellement recommandé spécifiquement pour les déploiements conteneurisés.

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 versionné.

Et si mon application utilise l’optimisation d’images de next/image ? Ça fonctionne sans aucune configuration avec next start, y compris en mode standalone, tant que le conteneur a la mémoire pour faire le traitement d’image à la volée ; c’est une préoccupation différente du problème de mémoire au moment du build dans le Dépannage ci-dessus.

Est-ce sûr à exécuter plus tard avec plusieurs instances derrière un équilibreur de charge ? En grande partie, mais la documentation de Next.js signale quelques points à régler d’abord si vous le faites : un gestionnaire de cache partagé au lieu de celui sur disque par défaut, et une NEXT_SERVER_ACTIONS_ENCRYPTION_KEY cohérente entre instances si vous utilisez les Server Actions, sinon une instance ne peut pas déchiffrer ce qu’une autre a chiffré.

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

Le Dockerfile et le fichier Compose ici ne changent pas beaucoup d’un projet Next.js à l’autre, ce qui varie vraiment, c’est ce que votre application lit depuis l’environnement à l’exécution, à 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 le Dockerfile

Collez l'URL du dépôt dans Railyard et le build, les ports et les variables d'environnement sont détectés pour vous sur un serveur Hetzner.

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