← Blogue

Déplacer une appli Next.js de Vercel vers votre propre serveur

Railyard

28 juin 2026 · 12 min de lecture

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

Votre appli Next.js tourne sur Vercel et vous voulez la mettre sur un serveur que vous contrôlez, avec une facture mensuelle fixe. Le code change à peine. Ce qui change, c’est tout ce que Vercel faisait autour : builder à chaque push, conserver vos variables d’environnement, définir quelques variables à lui, lancer des tâches cron, et servir le HTTPS. Ce guide déplace chacune de ces pièces vers un seul serveur, bascule ensuite le DNS sans interruption, et garde le déploiement Vercel comme solution de repli jusqu’à ce que le nouveau ait fait ses preuves.

Ce qu’il vous faut

  • Un serveur. Ce guide utilise un droplet DigitalOcean Basic, s-1vcpu-2gb (1 vCPU, 2 Go de mémoire, 50 Go SSD), à 12 $ par mois. Si votre build est lourd, s-2vcpu-4gb est à 24 $ par mois. N’importe quel serveur Ubuntu 24.04 avec une IP publique fonctionne de la même façon.
  • L’accès au DNS du domaine, peu importe où il est hébergé aujourd’hui (les serveurs de noms de Vercel ou votre registraire).
  • Le CLI Vercel sur votre propre machine (npm i -g vercel), connecté à l’équipe propriétaire du projet.
  • Le dépôt à partir duquel Vercel déploie, avec un fichier de verrouillage committé.

Les commandes supposent Next.js 16 (16.4.3 au moment d’écrire ces lignes) et Node.js 24, la ligne LTS actuelle.

Étape 1 : Lister ce que Vercel fait pour vous

Avant de toucher au serveur, vérifiez dans le projet les parties qui dépendent de Vercel lui-même :

grep -rn "VERCEL_" --include=*.ts --include=*.tsx --include=*.js --include=*.mjs . | grep -v node_modules
cat vercel.json 2>/dev/null

Vous trouverez typiquement certains de ces éléments :

  • Variables d’environnement système. Vercel définit VERCEL_ENV, VERCEL_URL, VERCEL_PROJECT_PRODUCTION_URL, VERCEL_GIT_COMMIT_SHA et d’autres à chaque déploiement. Aucune n’existe sur votre serveur. L’étape 3 remplace celles que votre code lit.
  • Les tâches cron définies sous crons dans vercel.json. Vercel les déclenche en envoyant une requête GET vers le chemin indiqué, selon un horaire en UTC. L’étape 8 les déplace vers le cron du serveur.
  • Les déploiements de prévisualisation pour chaque branche et pull request. Un seul serveur fait tourner un seul environnement ; la FAQ couvre vos options.
  • Les paquets propres à Vercel comme @vercel/analytics ou @vercel/functions. Vérifiez leur documentation pour savoir ce qu’ils font hors de Vercel avant de vous y fier.

Les fonctionnalités du framework lui-même vous suivent. Le guide d’auto-hébergement de Next.js affirme que l’Image Optimization via next/image et Proxy (la convention de fichier autrefois appelée Middleware) fonctionnent toutes les deux sans aucune configuration lorsque vous exécutez le serveur Next.js.

Étape 2 : Exporter vos variables d’environnement

Depuis le dossier du projet, liez-le et récupérez les variables de production dans un fichier :

vercel link
vercel env pull .env.vercel --environment=production

Ouvrez .env.vercel et vérifiez chaque valeur. Les variables stockées avec le type Secret de Vercel ne peuvent pas être relues, donc le CLI écrit un espace réservé pour chacune d’elles. Récupérez les vraies valeurs d’où elles viennent (votre fournisseur de base de données, Stripe, votre fournisseur d’authentification) et remplissez-les. Gardez ce fichier hors de Git ; le .gitignore par défaut de Next.js ignore déjà les fichiers .env*.

Vous devriez vous retrouver avec une liste complète de lignes NAME=value et aucun espace réservé.

Étape 3 : Remplacer les variables que Vercel définissait pour vous

Pour chaque variable VERCEL_ que votre code lit, ajoutez votre propre variable et pointez le code vers elle. Le cas courant est la construction d’URLs absolues :

const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";

Ajoutez ensuite NEXT_PUBLIC_SITE_URL=https://app.example.com à votre fichier d’environnement. Faites de même pour les bibliothèques d’authentification : Auth.js lit AUTH_URL (NextAuth v4 lit NEXTAUTH_URL), et sur Vercel elle pouvait déduire l’URL. Sur votre serveur, réglez-la sur votre vrai domaine.

Committez les changements de code. Vous devriez pouvoir relancer grep -rn "VERCEL_" et ne voir rien dont votre appli a encore besoin.

Étape 4 : Activer la sortie standalone

Ajoutez output: "standalone" à next.config.ts, en gardant vos autres options :

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

Avec ça, next build écrit un serveur autonome dans .next/standalone, donc l’image de production n’a pas besoin de votre node_modules complet.

Étape 5 : Ajouter un Dockerfile

Ce Dockerfile suit l’exemple officiel with-docker du dépôt Next.js : installer les dépendances avec le gestionnaire de paquets correspondant à votre fichier de verrouillage, builder, et exécuter uniquement la sortie standalone en tant qu’utilisateur non privilégié node.

FROM node:24-slim AS dependencies
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; \
  elif [ -f yarn.lock ]; then corepack enable yarn && yarn install --frozen-lockfile --production=false; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install --frozen-lockfile; \
  else echo "No lockfile found." && exit 1; \
  fi

FROM node:24-slim AS builder
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV NODE_ENV=production
RUN if [ -f package-lock.json ]; then npm run build; \
  elif [ -f yarn.lock ]; then corepack enable yarn && yarn build; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm build; \
  fi

FROM node:24-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
COPY --from=builder --chown=node:node /app/public ./public
RUN mkdir .next && chown node:node .next
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"]

Sur Vercel, chaque variable était présente pendant le build. Dans Docker, seul ce que vous transmettez l’est. Next.js intègre les variables NEXT_PUBLIC_ dans le bundle du navigateur pendant next build, donc chacune a besoin d’une paire ARG et ENV dans l’étape builder, comme NEXT_PUBLIC_SITE_URL ci-dessus. Les variables réservées au serveur n’en ont pas besoin : l’appli les lit au moment de l’exécution.

Ajoutez un .dockerignore pour que les fichiers locaux et les secrets restent hors du build :

.git
.next
node_modules
.env*

Construisez-la et lancez-la une fois sur votre propre machine avec les variables exportées, pour attraper les problèmes avant que le serveur ne soit impliqué :

docker build --build-arg NEXT_PUBLIC_SITE_URL=http://localhost:3000 -t my-app .
docker run --rm -p 3000:3000 --env-file .env.vercel my-app

Vous devriez voir l’appli à http://localhost:3000, pas un plantage à propos d’une variable manquante.

Étape 6 : Préparer le serveur

Créez le droplet (Ubuntu 24.04, Basic, 12 $), connectez-vous en SSH et installez Docker avec son script officiel :

curl -fsSL https://get.docker.com | sh
docker compose version

Vous devriez voir une version de Compose s’afficher. Clonez ensuite le dépôt et copiez-y votre fichier d’environnement, renommé en .env :

git clone https://github.com/you/your-app.git /opt/your-app
scp .env.vercel root@your-server-ip:/opt/your-app/.env

Lancez la première commande sur le serveur et la seconde sur votre propre machine. Sur le serveur, assurez-vous que .env contient NEXT_PUBLIC_SITE_URL avec votre vrai domaine, et verrouillez le fichier avec chmod 600 /opt/your-app/.env.

Étape 7 : Ajouter Compose et Caddy, puis démarrer

Le guide d’auto-hébergement de Next.js recommande un reverse proxy devant le serveur Node. Caddy remplit ce rôle et gère les certificats. Ajoutez compose.yaml au dépôt :

services:
  app:
    build:
      context: .
      args:
        NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL}
    restart: unless-stopped
    env_file: .env

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

volumes:
  caddy-data:
  caddy-config:

Et un Caddyfile avec votre domaine :

app.example.com {
	reverse_proxy app:3000
}

Committez les deux et faites git pull sur le serveur. Démarrez seulement l’appli pour l’instant. Caddy attend jusqu’à l’étape 9, car il ne peut pas obtenir de certificat tant que le domaine pointe encore vers Vercel :

cd /opt/your-app
docker compose up -d --build app

Le premier build prend quelques minutes. Vérifiez ensuite que l’appli répond à l’intérieur de son conteneur :

docker compose exec app node -e "fetch('http://localhost:3000').then((r) => console.log(r.status))"

Vous devriez voir 200. Si vous voyez une erreur à la place, docker compose logs app montre pourquoi l’appli n’a pas démarré, ce qui est généralement une variable manquante.

Étape 8 : Déplacer les tâches cron de Vercel vers le serveur

Pour chaque entrée sous crons dans vercel.json, ajoutez une ligne crontab qui demande le même chemin. Les horaires de Vercel sont toujours en UTC, tout comme un serveur Ubuntu fraîchement installé, donc les expressions se copient sans changement. Si votre route vérifie CRON_SECRET (Vercel l’envoie comme Authorization: Bearer <value>), continuez de la vérifier et envoyez l’en-tête vous-même.

Créez un petit script sur le serveur :

cat > /opt/your-app/cron-request.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
set -a
source /opt/your-app/.env
set +a
curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" "https://app.example.com$1"
EOF
chmod 700 /opt/your-app/cron-request.sh

Puis faites crontab -e et ajoutez une ligne par tâche, en utilisant le chemin et l’horaire de vercel.json :

0 5 * * * /opt/your-app/cron-request.sh /api/cron >> /var/log/your-app-cron.log 2>&1

Mettez un # devant chaque ligne pour l’instant, et retirez-le une fois que le DNS aura basculé à l’étape 9, pour qu’aucune tâche ne tourne sur les deux plateformes à la fois. Une fois en ligne, tail /var/log/your-app-cron.log après l’heure prévue devrait montrer la réponse de votre route.

Étape 9 : Basculer le DNS

Baissez le TTL de l’enregistrement DNS du domaine un jour à l’avance si vous le pouvez. Changez ensuite l’enregistrement pour pointer vers votre serveur : un enregistrement A avec l’adresse IPv4 du serveur, en remplaçant l’enregistrement Vercel. Si le domaine utilise les serveurs de noms de Vercel, faites le changement dans les paramètres DNS de Vercel, ou déplacez d’abord le domaine vers un autre fournisseur DNS.

Attendez que la nouvelle adresse soit ce que le monde voit :

dig +short app.example.com

Quand ça affiche l’IP de votre serveur, démarrez Caddy et observez-le obtenir le certificat :

docker compose up -d caddy
docker compose logs -f caddy

Vous devriez voir une ligne indiquant que le certificat a été obtenu avec succès. Ensuite, https://app.example.com se charge depuis votre serveur avec un certificat valide. Décommentez les lignes crontab et désactivez les tâches cron dans les paramètres du projet Vercel.

Gardez le projet Vercel pendant quelques jours. Les visiteurs dont le DNS ne s’est pas rafraîchi l’atteignent encore, et si quelque chose tourne mal vous pouvez repointer l’enregistrement en arrière.

Étape 10 : Déployer les mises à jour et garder des sauvegardes

Une version se résume maintenant à :

cd /opt/your-app
git pull
docker compose up -d --build app

Caddy continue de tourner et conserve ses certificats ; l’appli est indisponible pendant les quelques secondes que prend le nouveau conteneur pour démarrer. Vercel arrête aussi de builder vos pushes une fois que vous déconnectez l’intégration Git, donc faites-le quand vous avez confiance en la nouvelle configuration.

Le serveur lui-même ne détient rien que vous ne puissiez reconstruire à part .env, donc gardez-en une copie dans un gestionnaire de mots de passe. Votre base de données reste là où elle est aujourd’hui et garde ses propres sauvegardes ; vous pouvez la migrer séparément plus tard.

Le raccourci : avec Railyard

Les étapes 4 à 10 sont ce que Railyard fait pour vous. Connectez le même dépôt, et il construit et déploie l’appli sur un serveur dans votre propre compte cloud, avec HTTPS et un redéploiement à chaque push. Il n’y a ni Dockerfile, ni fichier Compose, ni Caddyfile à écrire.

  1. Ajoutez un serveur : dans le panneau de contrôle de DigitalOcean, ouvrez API, générez un jeton d’accès personnel avec un accès en lecture et en écriture, et créez le droplet depuis Servers → Add a server dans Railyard. Ou connectez un serveur que vous avez déjà (Ubuntu 22.04 ou 24.04, sudo sans mot de passe, ports 22, 80 et 443 ouverts).
  2. Déployez le dépôt, ou ouvrez https://app.railyard.run/deploy?repo=https://github.com/you/your-app.git avec votre propre URL.
  3. Collez les variables de l’étape 2 dans Variables → Bulk import.

Railyard choisit le gestionnaire de paquets à partir de votre fichier de verrouillage, lance votre script de build et démarre l’appli avec npm start, en écoutant sur $PORT. Postgres ou Redis ajouté depuis la page de l’appli a sa chaîne de connexion injectée comme variable. Recréez chaque tâche cron de vercel.json comme une entrée dans l’onglet Cron jobs de l’appli, qui exécute une commande dans l’image de l’appli selon un horaire (en UTC) et garde un historique de chaque exécution. Pour demander une route, utilisez le fetch intégré de Node avec la variable RAILYARD_URL de l’appli :

node -e "fetch(process.env.RAILYARD_URL + '/api/cron', { headers: { authorization: 'Bearer ' + process.env.CRON_SECRET } }).then((r) => process.exit(r.ok ? 0 : 1))"
``` Les prévisualisations de pull request peuvent être activées pour l'appli une fois GitHub connecté ; voir [Environnements et aperçus](/fr/docs/environments-and-previews).

Le plan Free couvre un serveur plus un serveur de base de données et trois applis, sans carte requise ; Pro est à 19 $ par mois par équipe et Team à 49 $ par mois par équipe. Commencez avec [Déployer depuis un dépôt](/fr/docs/deploy-from-a-repository), ou voir [Déployer Next.js sur DigitalOcean](/fr/deploy/nextjs-to-digitalocean) pour le côté fournisseur. Vous déplacez aussi des applis depuis Heroku, Render, Fly ou Railway ? [Importer depuis une autre plateforme](/fr/docs/import-from-another-platform) les lit avec un jeton API.

## Dépannage

**Le build échoue avec une variable manquante, ou une valeur `NEXT_PUBLIC_` est vide dans le navigateur.** Elle n'était pas présente pendant `next build`. Ajoutez une paire `ARG` et `ENV` pour elle dans le Dockerfile, transmettez-la sous `build.args` dans `compose.yaml`, et reconstruisez. Redémarrer le conteneur ne change pas les valeurs qui ont été intégrées au moment du build.

**Les connexions redirigent vers une adresse `vercel.app`.** Votre URL d'authentification contient encore une valeur Vercel provenant de l'export. Réglez `AUTH_URL` ou `NEXTAUTH_URL` sur votre domaine dans `.env` et lancez `docker compose up -d app`.

**Caddy journalise des erreurs de certificat pendant des heures.** Le DNS ne pointe pas encore vers le serveur, ou un pare-feu bloque les ports 80 et 443. `dig +short app.example.com` devrait retourner l'IP de votre serveur. Caddy réessaie par lui-même une fois que c'est le cas.

**Le code qui lit la géolocalisation ou les en-têtes de requête Vercel ne reçoit rien.** Des en-têtes comme `x-vercel-ip-country` sont ajoutés par le réseau de Vercel et n'existent pas derrière Caddy. Retirez ce chemin de code ou utilisez votre propre service de géolocalisation.

**Le build est tué sur le serveur de 2 Go.** Il a manqué de mémoire. Ajoutez un fichier swap de 2 Go (`fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile`), passez à la taille à 24 $, ou construisez l'image ailleurs et récupérez-la depuis un registre.

## FAQ

**Le code de mon appli devra-t-il changer ?** Généralement seulement là où il lit des variables `VERCEL_` ou utilise des paquets propres à Vercel. Le routage, les Server Components, les Server Actions, Image Optimization et Proxy tournent tous sur le serveur standalone.

**Et les déploiements de prévisualisation ?** Un serveur sert un seul environnement. Un second serveur, plus petit, pour la mise en scène, construit depuis une autre branche, couvre la plupart des besoins. Sur Railyard, les prévisualisations de pull request sont un réglage de l'appli.

**Est-ce que l'ISR continue de fonctionner ?** Oui. Next.js stocke son cache sur le disque local de chaque instance de serveur par défaut, ce qui fonctionne pour une instance unique. Le cache repart vide après chaque déploiement, car le conteneur est remplacé.

**Puis-je continuer à utiliser des services achetés via Vercel ?** Les services qui vous donnent une chaîne de connexion ou un jeton API, comme une base de données du Marketplace, continuent de fonctionner depuis n'importe quel serveur tant que vous copiez les identifiants. Vérifiez la documentation propre à chacun pour les limites en dehors de Vercel.

Le déménagement, c'est surtout de la tenue de registres : variables, tâches cron et DNS. Une fois ces trois choses faites, une version sur votre propre serveur se résume à un `git pull` et une commande.

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.

Connectez le même dépôt.

Pointez Railyard vers le dépôt à partir duquel Vercel build, et il déploie votre appli Next.js sur votre propre serveur, avec HTTPS et un redéploiement à chaque push.

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