Pourquoi self-héberger Next.js sur un VPS ?
Next.js est souvent associé à une plateforme d'hébergement spécifique, mais son serveur Node.js standalone tourne parfaitement sur n'importe quel VPS. L'auto-hébergement devient pertinent dès que vous utilisez intensivement le SSR, l'ISR (régénération statique incrémentale) ou les routes API : ces fonctionnalités consomment des invocations facturées sur les plateformes managées, alors qu'elles sont gratuites et sans quota imposées sur votre serveur. Vous contrôlez le cache ISR sur disque, la bande passante des images optimisées et le temps d'exécution des fonctions, sans limite de 10 secondes. Pour une agence qui héberge plusieurs sites clients, un VPS mutualise les coûts et simplifie la facturation.
Bénéfices concrets d'un Next.js auto-hébergé
- SSR et API routes sans quota d'invocations ni facturation à la fonction.
- Cache ISR persistant sur disque, pas de revalidation perdue entre déploiements.
- Bande passante incluse, idéale pour les sites riches en images et vidéos.
- Plusieurs projets Next.js sur un même VPS, mutualisés sous Nginx.
- Optimisation d'images
next/imageservie localement sans surcoût par transformation. - Build et déploiement maîtrisés via Git, CI/CD ou simple
git pullet rebuild.
Prérequis matériels et logiciels
Le build de Next.js est gourmand en mémoire : prévoyez au moins 2 Go de RAM (4 Go pour un gros projet avec beaucoup de pages), sinon le build peut échouer par manque de mémoire. Côté runtime, 1 à 2 vCPU suffisent pour servir le SSR. Installez Node.js 18 ou 20 LTS, soit nativement avec nvm, soit via une image Docker node:20-alpine. Utilisez PM2 pour superviser le processus Node, ou Docker pour l'isolation. Un domaine pointé vers le VPS est nécessaire pour le SSL. Activez output: 'standalone' dans next.config.js pour un déploiement léger.
Build Docker multi-stage : node:20-alpine
Un build Docker multi-stage réduit l'image finale à l'essentiel et évite d'embarquer les outils de compilation en production. La stratégie en trois étapes — deps, builder, runner — est la plus courante pour Next.js en mode standalone.
# Étape 1 — dépendances
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Étape 2 — build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Étape 3 — runner (image finale)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]L'étape deps installe les dépendances de production uniquement (--omit=dev). L'étape builder copie tout le source et lance npm run build avec l'accès aux modules. L'étape runner ne contient que le dossier standalone généré par Next.js, les assets statiques et le dossier public — aucun node_modules source, aucun fichier de configuration de build. L'image finale pèse souvent deux à trois fois moins qu'une image naïve.
Variables d'environnement : .env.local vs .env.production
Next.js distingue deux familles de variables selon leur portée.
Variables publiques (NEXT_PUBLIC_*) : elles sont intégrées au bundle JavaScript au moment du build et exposées au navigateur. Elles conviennent à l'URL d'une API publique, à un identifiant Google Analytics, à un flag de fonctionnalité. Ne jamais y placer un secret.
Variables serveur : lues uniquement côté Node (API routes, Server Components, getServerSideProps). Elles ne sont jamais envoyées au client. C'est ici que vivent les clés d'API tierces, la chaîne de connexion à la base de données, les secrets JWT.
# .env.local — développement local (jamais committé)
NEXT_PUBLIC_API_URL=http://localhost:4000
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
# .env.production — valeurs de production (jamais committé non plus)
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://user:[email protected]:5432/mydbSur le VPS, deux approches coexistent : injecter les variables dans l'environnement du processus PM2 via son fichier d'écosystème (ecosystem.config.js), ou les passer à Docker via --env-file .env.production. La seconde est préférable : le fichier reste sur le disque du serveur, hors du dépôt git, et Docker le monte au démarrage du conteneur sans l'exposer dans les logs.
// ecosystem.config.js (PM2)
module.exports = {
apps: [{
name: 'nextjs',
script: '.next/standalone/server.js',
env_production: {
NODE_ENV: 'production',
PORT: 3000
}
}]
};À noter : les variables NEXT_PUBLIC_* doivent être connues au moment du build, pas seulement au démarrage. Si vous changez une variable publique après le build, il faut reconstruire l'application — changer uniquement l'environnement du processus ne suffira pas.
Déployer Next.js étape par étape
Préparer le serveur
En SSH, installez Node.js 20 LTS et PM2 (
npm install -g pm2), ou Docker. Clonez le dépôt et créez.env.productionavec vos variables (NEXT_PUBLIC_*pour le client, secrets serveur pour les API routes).Builder l'application
Lancez
npm cipuisnpm run build. Avecoutput: 'standalone', Next.js génère un dossier.next/standaloneautonome contenant uniquement les dépendances nécessaires, ce qui allège fortement l'image.Démarrer le serveur Node
Lancez le serveur avec
pm2 start node --name nextjs -- .next/standalone/server.jssur le port 3000, ou via un conteneur Docker. Configurezpm2 startupetpm2 savepour un redémarrage automatique au reboot du VPS.Configurer Nginx en frontal
Créez un bloc serveur qui fait
proxy_pass http://localhost:3000, transmet les en-têtesHostetX-Forwarded-For, et sert directement/_next/static/depuis le disque pour soulager Node. Activez la compression gzip.Installer le certificat SSL
Obtenez un certificat Let's Encrypt via Certbot pour
votredomaine.com, forcez la redirection HTTPS et configurez le renouvellement automatique. Vérifiez que les en-têtesX-Forwarded-Protosont bien transmis pour le SSR.Mettre en place les déploiements
Automatisez le cycle
git pull && npm ci && npm run build && pm2 reload nextjsvia un script ou un webhook Git. LereloadPM2 assure un redémarrage sans coupure (zero-downtime) entre deux versions.
Configuration Nginx complète avec upstream
Un bloc Nginx complet pour Next.js dépasse le proxy_pass minimal. Il faut gérer les assets statiques directement sur disque, les en-têtes de cache, la compression et la transmission correcte des informations client.
upstream nextjs_upstream {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name votredomaine.com www.votredomaine.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name votredomaine.com www.votredomaine.com;
ssl_certificate /etc/letsencrypt/live/votredomaine.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/votredomaine.com/privkey.pem;
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
# Assets statiques Next.js — cache long durée
location /_next/static/ {
alias /home/deploy/monapp/.next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Fichiers publics (images, favicons…)
location /public/ {
alias /home/deploy/monapp/public/;
expires 30d;
}
# Tout le reste vers Next.js
location / {
proxy_pass http://nextjs_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}Le bloc upstream avec keepalive 64 maintient un pool de connexions persistantes vers Node, ce qui évite la négociation TCP à chaque requête. La directive proxy_http_version 1.1 est obligatoire pour que les connexions keepalive fonctionnent effectivement. Les assets _next/static reçoivent Cache-Control: immutable car Next.js y place des hashes de contenu — le fichier ne change jamais à URL égale.
Stratégie ISR : revalidation et persistance du cache
L'ISR (Incremental Static Regeneration) permet à Next.js de régénérer une page statique en arrière-plan après un délai donné, sans rebuild complet. Sur un VPS, cela nécessite que le cache survive aux redémarrages.
// app/produits/[id]/page.tsx (App Router)
export const revalidate = 3600; // régénère toutes les heures
// pages/produits/[id].tsx (Pages Router)
export async function getStaticProps() {
return {
props: { ... },
revalidate: 3600
};
}Sur le VPS, le cache ISR est stocké dans .next/cache. Pour qu'il survive aux redéploiements, deux options existent.
Option 1 — volume persistant : montez .next/cache en dehors du dossier de déploiement. Lors d'un git pull et rebuild, copiez l'ancien cache avant d'écraser .next puis restaurez-le. Le script de déploiement ressemble à :
cp -r .next/cache /tmp/nextjs-cache-backup
npm run build
cp -r /tmp/nextjs-cache-backup .next/cache
pm2 reload nextjsOption 2 — cache handler Redis : pour plusieurs instances Node ou un besoin de partage entre machines, Next.js supporte les cache handlers personnalisés depuis la version 13.4. Cela permet d'externaliser le cache ISR vers Redis, ce qui garantit la cohérence entre instances.
Si votre trafic est faible, l'option 1 suffit. L'option 2 devient nécessaire dès que vous avez plusieurs processus Node (cluster PM2) ou plusieurs VPS derrière un load balancer.
Server Components vs Client Components : impact RAM et CPU
Depuis Next.js 13 et l'App Router, les composants sont Server Components par défaut. La distinction a des conséquences directes sur la consommation de ressources de votre VPS.
Server Components s'exécutent uniquement côté serveur. Ils peuvent lire la base de données directement, accéder au système de fichiers, et ne sont jamais envoyés au bundle JavaScript du navigateur. Le résultat est du HTML pur, ce qui réduit la taille du bundle client et améliore le Core Web Vitals (LCP). Sur le serveur, ils consomment du CPU à chaque requête SSR non mise en cache.
Client Components ('use client' en tête de fichier) sont hydratés dans le navigateur. Ils sont nécessaires pour les interactions utilisateur : événements, état local, hooks (useState, useEffect). Ils augmentent la taille du bundle JavaScript envoyé au client.
// Server Component — s'exécute sur le serveur, jamais envoyé au client
async function ProductList() {
const products = await db.query('SELECT * FROM products LIMIT 20');
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// Client Component — hydraté dans le navigateur
'use client';
import { useState } from 'react';
function AddToCart({ productId }: { productId: number }) {
const [added, setAdded] = useState(false);
return <button onClick={() => setAdded(true)}>{added ? 'Ajouté' : 'Ajouter'}</button>;
}Sur un VPS, la règle pratique est : garder les Server Components pour tout ce qui touche aux données, et limiter les Client Components aux zones d'interaction. Un composant rendu 10 000 fois par heure côté serveur consomme du CPU ; le même rendu côté client consomme la RAM du navigateur du visiteur, pas votre serveur.
CI/CD : GitHub Actions ou Forgejo
Automatiser le déploiement évite les erreurs humaines et garantit que chaque merge sur la branche principale déclenche un rebuild propre. Deux solutions courantes sur VPS : GitHub Actions (si votre dépôt est sur GitHub) et Forgejo (forge auto-hébergée, alternative open source à GitHub/Gitea).
GitHub Actions — workflow de déploiement via SSH :
name: Deploy Next.js
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: deploy
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /home/deploy/monapp
git pull origin main
npm ci
npm run build
pm2 reload nextjsForgejo — si vous auto-hébergez votre forge sur le même VPS ou un serveur dédié, utilisez un runner Forgejo Act. La syntaxe des workflows est identique à GitHub Actions, ce qui facilite la migration.
Dans les deux cas, les secrets (clé SSH, variables d'environnement) se configurent dans les secrets du dépôt et ne transitent jamais dans les logs. Pour les builds lourds (projet avec beaucoup de pages), envisagez de builder l'image Docker sur le runner CI et de la pousser dans un registre privé, puis de la puller sur le VPS — cela évite de consommer la RAM du serveur de production pendant le build.
Monitoring avec PM2
PM2 est à la fois le gestionnaire de processus et le premier outil de monitoring de votre application Next.js. Quelques commandes essentielles.
# État des processus
pm2 list
# Logs en temps réel
pm2 logs nextjs
# Métriques CPU et RAM
pm2 monit
# Redémarrage sans coupure (zero-downtime)
pm2 reload nextjs
# Redémarrage complet (coupure courte)
pm2 restart nextjsPM2 peut aussi journaliser les métriques dans un fichier et les exporter vers des systèmes externes. Pour un monitoring plus poussé, activez le module pm2-logrotate pour éviter que les fichiers de log ne saturent le disque :
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7En production, sauvegardez la liste des processus après chaque changement : pm2 save. Cette liste est rechargée automatiquement au démarrage du système si vous avez exécuté pm2 startup et validé la commande qu'il vous fournit.
Si vous utilisez l'ISR, montez un volume persistant pour le dossier .next/cache afin que les pages revalidées survivent aux redéploiements. Sans cela, chaque rebuild repart d'un cache vide et provoque un pic de génération à la volée. Pour plusieurs instances Node derrière un load balancer, externalisez ce cache vers un stockage partagé ou Redis avec un cache handler personnalisé.
Dépannage : erreurs courantes en production
Trois à cinq erreurs reviennent systématiquement lors du premier déploiement d'une application Next.js sur VPS.
ENOMEM pendant npm run build
Le build Next.js compile en parallèle et peut dépasser 2 Go de RAM sur un projet moyen. Si le build s'interrompt avec Killed ou JavaScript heap out of memory, augmentez la limite mémoire de Node :
NODE_OPTIONS="--max-old-space-size=4096" npm run buildAlternativement, passez par le build Docker multi-stage décrit plus haut : le build s'effectue dans le conteneur et seule l'image finale (légère) est copiée sur le VPS.
Port 3000 already in use
Un ancien processus PM2 ou un processus Node orphelin écoute déjà sur le port 3000. Identifiez-le et arrêtez-le :
lsof -i :3000
kill -9 <PID>
# Ou, si c'est PM2 qui a déjà le processus :
pm2 delete nextjs
pm2 start ecosystem.config.js --env productionMODULE_NOT_FOUND en production avec output: 'standalone'
Le dossier .next/standalone contient une copie des dépendances nécessaires, mais pas les assets statiques ni le dossier public. Si vous copiez uniquement standalone, ces ressources manquent et le serveur plante au démarrage. Copiez les trois dossiers :
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/publicApplication lente après redémarrage
Le premier chargement d'une page SSR après un redémarrage de PM2 est plus lent : le moteur V8 doit recompiler le code JavaScript (JIT). Pour réchauffer l'application automatiquement après le démarrage, ajoutez un script de warmup qui appelle les routes principales :
pm2 reload nextjs && sleep 5 && curl -sf https://votredomaine.com > /dev/nullErreur X-Forwarded-Proto et boucle de redirection HTTPS
Si Next.js détecte que la requête est en HTTP alors qu'elle est déjà passée par Nginx en HTTPS, il peut créer une boucle de redirection. Assurez-vous que Nginx transmet bien X-Forwarded-Proto: https et que votre application lit cet en-tête pour déterminer le protocole réel.
Sauvegarder le cache .next/cache
Le cache .next/cache contient deux types de données précieuses : les pages ISR revalidées et le cache de compilation Webpack/SWC. Perdre ce cache oblige Next.js à régénérer toutes les pages ISR à la volée dès le premier hit, ce qui peut créer un pic de charge si votre site a beaucoup de pages statiques.
Mettez en place une sauvegarde simple avec rsync ou tar avant chaque déploiement :
#!/bin/bash
APP_DIR=/home/deploy/monapp
BACKUP_DIR=/home/deploy/backups
# Sauvegarde du cache avant le rebuild
tar -czf $BACKUP_DIR/nextjs-cache-$(date +%Y%m%d-%H%M%S).tar.gz $APP_DIR/.next/cache
# Nettoyage des sauvegardes de plus de 7 jours
find $BACKUP_DIR -name 'nextjs-cache-*.tar.gz' -mtime +7 -delete
# Déploiement
cd $APP_DIR
git pull origin main
npm ci
npm run build
# Restauration du cache de compilation (pas ISR)
CACHE_ARCHIVE=$(ls -t $BACKUP_DIR/nextjs-cache-*.tar.gz | head -1)
tar -xzf $CACHE_ARCHIVE $APP_DIR/.next/cache/webpack -C /
pm2 reload nextjsLe cache Webpack accélère les rebuilds suivants de manière significative — sur un projet moyen, un rebuild avec cache complet prend 40 à 60 % moins de temps qu'un build à froid.
Déployer Next.js en un clic depuis le Marketplace
Le Marketplace ServOrbit propose un template Next.js Stack qui configure automatiquement Node.js LTS, PM2, Nginx et PostgreSQL sur votre VPS. En quelques minutes, votre environnement de production est prêt à recevoir votre application React SSR — sans configuration manuelle.
Comparé à l'installation décrite dans ce guide, le template gère la mise en place initiale et vous permet de démarrer directement à l'étape « déployer votre code ».