Chapitre 9.1 — Build de prod Next.js
⏱️ TL;DR — Déployer Next.js à la main suit un ordre précis : installer Node LTS sur le serveur (via NodeSource en prod, ou nvm pour jongler entre versions), récupérer le code (
git clonesur la branche de prod), installer les dépendances avecnpm ci(pasnpm install— on verra pourquoi), poser les variables d’environnement (NODE_ENV=productionet un.env.productionhors de git), builder (next build), puis démarrer (next start -p 3000). Deux pièges à graver : une variableNEXT_PUBLIC_*est envoyée au navigateur (donc jamais de secret dedans), et si ton app est 100 % statique, tu n’as même pas besoin de Node qui tourne — unnext exportservi par Nginx suffit.
🎯 Objectifs
- Installer Node LTS sur un serveur, proprement, et activer
corepackpourpnpm/yarn. - Récupérer le code sur la bonne branche et installer les deps de façon reproductible (
npm ci). - Gérer les variables d’environnement de prod et repérer le piège
NEXT_PUBLIC_. - Lancer un build de production (
next build) et le démarrer (next start). - Choisir entre
next start,output: 'standalone'et export statique selon ton app.
L’ordre du déploiement manuel
Avant les détails, garde la séquence en tête. C’est toujours la même, et chaque étape dépend de la précédente :
On fait ces gestes à la main dans ce chapitre pour comprendre. On les transformera en service (chapitre suivant), puis on les automatisera en CI/CD (Partie 13). Mais tu ne peux pas automatiser ce que tu ne sais pas faire au clavier.
1. Installer Node LTS sur le serveur
Ton portable a peut-être Node installé via un installeur graphique ou brew. Sur un serveur, on veut quelque chose de reproductible et maintenu. Deux voies, selon ton besoin.
Voie A — NodeSource (recommandée en prod). Le dépôt officiel NodeSource ajoute Node à apt, ce qui te donne les mises à jour via le gestionnaire de paquets habituel. Tu choisis la ligne LTS courante (pas la « Current », trop mouvante pour un serveur).
# Ajouter le dépôt NodeSource pour la ligne LTS courante, puis installer
# (la commande exacte du script est sur le site NodeSource — vérifie-la)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
node -v # vérifie la version installée
npm -v # npm vient avec NodeVoie B — nvm (si tu jongles entre versions). nvm (Node Version Manager) installe Node dans le home de l’utilisateur, sans sudo, et permet d’avoir plusieurs versions côte à côte. Pratique si tu héberges plusieurs apps qui n’exigent pas la même version.
# Installer nvm (récupère le script officiel — vérifie l'URL sur le dépôt nvm)
# puis, dans une nouvelle session :
nvm install --lts # installe la dernière LTS
nvm use --lts # l'active pour cette session
nvm alias default lts/* # en fait la version par défaut⚠️ Piège — Avec nvm, Node est installé par utilisateur, dans son
$HOME. Un service systemd qui tourne sousdeployne « voit » pas forcément ce Node : il faut lui donner le chemin absolu du binaire (ExecStartpointant vers le vrainode), sinonnode: command not foundau démarrage du service. Pour un serveur mono-app, NodeSource évite ce piège : le binaire est dans/usr/bin/node, visible par tous. On y revient au chapitre 9.2.
corepack : pnpm et yarn sans installation globale
Ton projet utilise peut-être pnpm ou yarn au lieu de npm. Depuis les Node récents, corepack est livré avec Node et sait activer le bon gestionnaire, à la bonne version (celle déclarée dans le champ packageManager de ton package.json).
sudo corepack enable # active corepack (pnpm, yarn deviennent disponibles)
corepack prepare pnpm@latest --activate # optionnel : fige une version de pnpm
pnpm -v # vérifie💡 Réflexe — Utilise sur le serveur le même gestionnaire de paquets qu’en dev (celui dont tu committes le lockfile). Si ton repo a un
pnpm-lock.yaml, installe avecpnpm, pasnpm: mélanger les gestionnaires génère des arbres de dépendances différents et des bugs « ça marche chez moi ».
2. Récupérer le code
Le code arrive sur le serveur par git clone, sur la branche de production (souvent main, parfois une branche production dédiée). On clone dans un emplacement clair — la convention FHS est /var/www/.
# Dans /var/www, en tant qu'utilisateur deploy (pas root)
cd /var/www
git clone --branch main --single-branch https://github.com/formacampus/web.git formacampus
# --single-branch : ne récupère que la branche voulue (clone plus léger)
cd formacampus🔒 Sécurité — Pour un dépôt privé, n’utilise pas un mot de passe ni un token en clair dans l’URL (il finit dans l’historique shell et dans
.git/config). Préfère une clé SSH de déploiement (deploy key) en lecture seule, propre à ce serveur, ou un token à portée minimale stocké hors de la ligne de commande. On détaille l’accès Git côté serveur en Partie 13.
3. Installer les dépendances : npm ci, pas npm install
Le réflexe local, c’est npm install. En prod, c’est npm ci. La différence est fondamentale :
npm install | npm ci | |
|---|---|---|
| Source de vérité | package.json (peut mettre à jour le lockfile) | package-lock.json uniquement |
node_modules existant | le modifie au besoin | le supprime et réinstalle à neuf |
| Reproductibilité | variable selon les versions résolues | identique à chaque fois |
| Vitesse | correcte | souvent plus rapide (pas de résolution) |
| Sans lockfile | fonctionne | échoue (c’est voulu) |
npm ci (clean install) installe exactement ce que le lockfile décrit. Résultat : le serveur a les mêmes versions que ta CI et ta machine — le socle d’un build reproductible.
npm ci # installe pile ce que package-lock.json décrit, à neuf
# équivalents : pnpm install --frozen-lockfile | yarn install --immutable⚠️ Piège — Lancer
npm installen prod « parce que c’est l’habitude ». S’il met à jour le lockfile (une dépendance a publié une nouvelle version compatible), tu déploies des versions jamais testées en CI. Un bug apparaît en prod, introuvable en local. La règle : prod =npm ci(ou l’équivalent--frozen-lockfile). Et commite toujours ton lockfile.
4. Les variables d’environnement (et le piège NEXT_PUBLIC_)
Ton app a besoin de secrets (URL de base de données, clés d’API) et de config (URL publique, options). En prod, on ne met jamais ces valeurs dans le code : elles vivent dans l’environnement.
NODE_ENV=production: indispensable. Il active les optimisations (React en mode prod, pas d’avertissements de dev, dépendances allégées).next buildetnext startle posent en général eux-mêmes, mais dans un service tu le fixeras explicitement.- Un fichier
.env.production(ou.envsur le serveur), hors de git (dans.gitignore), lu au build et/ou au runtime.
# .env.production sur le serveur — jamais committé
NODE_ENV=production
DATABASE_URL=postgres://formacampus:motdepasse@127.0.0.1:5432/formacampus
SESSION_SECRET=une-longue-chaine-aleatoire
NEXT_PUBLIC_SITE_URL=https://formacampus.frVoici le piège de sécurité propre à Next.js. Toute variable préfixée NEXT_PUBLIC_ est inlinée dans le bundle JavaScript envoyé au navigateur. Elle est donc publique — n’importe quel visiteur peut la lire dans les sources de la page. C’est voulu pour les valeurs publiques (URL du site, clé publique d’analytics), mais catastrophique pour un secret.
🔒 Sécurité — Jamais de secret dans une variable
NEXT_PUBLIC_*. Une clé d’API privée, un mot de passe de base, un secret de session préfixésNEXT_PUBLIC_se retrouvent en clair dans le HTML/JS servi à tout le monde. Règle simple :NEXT_PUBLIC_= « affiché sur un panneau public ». Un secret n’a pas ce préfixe et n’est lu que côté serveur (dans une Route Handler, une Server Component,getServerSideProps…). Vérifie tes bundles : si ton secret apparaît dans.next/, tu l’as exposé.
⚠️ Piège — Les variables
NEXT_PUBLIC_*sont figées au moment dunext build, pas au démarrage. Changer unNEXT_PUBLIC_SITE_URLpuis relancernext startne suffit pas : il faut rebuilder. À l’inverse, une variable serveur (sans le préfixe) est lue au runtime et prise en compte à chaque redémarrage. Retiens : public = build-time, secret serveur = runtime.
5. Builder : next build
Le build transforme ton code source en artefacts de production optimisés (pages pré-rendues, JS minifié, découpage en chunks) dans le dossier .next/.
npm run build # exécute "next build" (le script "build" de package.json)
# ou directement : npx next buildC’est l’étape la plus gourmande en RAM : un build Next peut consommer plusieurs centaines de Mo à plus d’un Go. Sur un petit VPS, un build peut être tué par l’OOM killer (rappel Partie 1.4).
💡 Réflexe — Si le build meurt sur un petit VPS (processus tué, « Killed » dans la sortie), tu as trois options : ajouter un peu de swap temporaire, redimensionner le VPS le temps du build, ou — le plus propre — builder ailleurs (en CI) et n’envoyer sur le serveur que le résultat. La Partie 13 industrialise exactement ça : build en GitHub Actions, déploiement de l’artefact.
6. Démarrer : next start
Une fois buildé, on lance le serveur de production de Next. Attention : next start sert le résultat de next build, ce n’est pas next dev.
npx next start -p 3000 # démarre le serveur de prod sur le port 3000
# -p 3000 : port d'écoute (par défaut 3000). On garde un port > 1024,
# non privilégié, joignable seulement en local (Nginx fera la facade)Lancé comme ça, le process meurt dès que tu fermes ton SSH. C’est normal — on ne laisse jamais une app tourner ainsi en prod. Le chapitre 9.2 en fait un service qui survit à tout.
🐚 Au terminal — La séquence complète, de zéro à une app qui répond en local :
cd /var/www/formacampus
git pull origin main # récupère la dernière version de la branche prod
npm ci # deps exactes du lockfile
npm run build # build de production
npx next start -p 3000 & # démarre (le & le met en arriere-plan, temporaire)
curl -I http://127.0.0.1:3000 # doit répondre 200 : l'app tourne en local7. Trois modes de déploiement : start, standalone, statique
Tous les Next ne se déploient pas pareil. Choisis selon ce que fait ton app.
Mode 1 — next start classique. Tu déploies tout le projet (node_modules compris) et lances next start. Simple, universel, mais lourd : tout le dossier doit être présent sur le serveur. C’est le mode par défaut, parfait pour commencer.
Mode 2 — output: 'standalone'. Dans next.config.js, l’option output: 'standalone' fait générer par le build un dossier .next/standalone/ auto-suffisant : il contient un serveur Node minimal et seulement les dépendances réellement utilisées. Tu peux déployer ce dossier seul, sans embarquer tout node_modules. Idéal pour des images Docker minces (Partie 12) ou un déploiement léger.
// next.config.js
module.exports = {
output: 'standalone',
}# Après le build, on lance le serveur minimal généré :
node .next/standalone/server.js
# (pense à copier .next/static et public/ à cote, la doc le précise)Mode 3 — export statique (pas de Node qui tourne). Si ton site est entièrement statique (SSG : que des pages pré-rendues, pas de rendu serveur à la volée, pas de Route Handlers), tu n’as pas besoin de Node en prod du tout. Tu exportes du HTML/CSS/JS pur que Nginx sert directement, comme un site statique. Plus rapide, plus robuste, rien à surveiller.
// next.config.js — pour un site 100 % statique
module.exports = {
output: 'export', // génère un dossier out/ de fichiers statiques
}npm run build # avec output: 'export', produit le dossier out/
# On sert ensuite out/ directement avec Nginx (voir Partie 7.3),
# sans aucun process Node. Zéro service a surveiller.💡 Réflexe — Avant de monter tout un service Node, demande-toi si tu en as besoin. Un blog, une doc, une landing, un portfolio en SSG pur ? Un export statique servi par Nginx est plus simple, plus rapide et plus sûr (pas de runtime à patcher, pas de crash possible). Tu réserves Node/
next startaux apps qui font du vrai rendu serveur (SSR, API interne, ISR, auth côté serveur).
⚠️ Piège — Choisir l’export statique alors que ton app fait du SSR ou expose des Route Handlers (
/api/...). Le build échouera ou ces routes disparaîtront silencieusement : tout ce qui dépend du serveur à l’exécution ne peut pas être « exporté » en fichiers figés. Si tu as des données dynamiques par requête, de l’auth serveur, des API internes → tu as besoin de Node (next startoustandalone), pas de l’export.
🧭 Sur FormaCampus — Le front FormaCampus fait du SSR (pages apprenant personnalisées, sessions, données à jour) : pas d’export statique possible. L’équipe part donc sur
next build+next start -p 3000, avecoutput: 'standalone'activé pour préparer la future image Docker. Le tout sera lancé par un service systemd nomméformacampus-web, écoutant sur127.0.0.1:3000, derrière Nginx en HTTPS. Les secrets (URL Postgres, secret de session) vivent dans un.env.productionhors git, lu au runtime ; seuleNEXT_PUBLIC_SITE_URLest publique. C’est le socle des quatre chapitres suivants.
📚 La doc — Les détails exacts (contenu de
.next/standalone/, fichiers à copier pour le mode standalone, contraintes deoutput: 'export') évoluent avec les versions de Next. La doc officielle Next.js (sections Deploying et Output File Tracing) fait autorité — vérifie-la pour ta version plutôt que de te fier à un tutoriel figé.
✏️ Exercices
Exercice 1 — install ou ci ? Un collègue déploie en lançant npm install sur le serveur, puis s’étonne qu’une dépendance ait une version différente de sa CI. Explique la cause et donne la commande correcte.
✅ Solution
npm install peut résoudre et mettre à jour des versions (et le lockfile) selon ce qui est publié, donc installer des versions non testées en CI — d’où l’écart. En prod, on veut la reproductibilité : npm ci, qui installe exactement le contenu de package-lock.json, à neuf, et échoue si le lockfile manque ou ne correspond pas. La commande correcte : npm ci (ou pnpm install --frozen-lockfile / yarn install --immutable). Et il faut committer le lockfile.
Exercice 2 — Repère la fuite. Ce .env.production contient une erreur de sécurité. Laquelle, et pourquoi c’est grave ?
NODE_ENV=production
NEXT_PUBLIC_API_URL=https://api.formacampus.fr
NEXT_PUBLIC_DATABASE_PASSWORD=Sup3rSecret!✅ Solution
NEXT_PUBLIC_DATABASE_PASSWORD est catastrophique : le préfixe NEXT_PUBLIC_ fait inliner la valeur dans le bundle JavaScript envoyé au navigateur. Le mot de passe de la base se retrouve en clair dans les sources de la page, lisible par n’importe quel visiteur. Un mot de passe de base ne doit jamais être exposé au client : on le nomme sans le préfixe (ex. DATABASE_PASSWORD), et il n’est lu que côté serveur. NEXT_PUBLIC_API_URL, en revanche, est une URL publique — le préfixe est légitime ici.
Exercice 3 — Faut-il un service Node ? Tu déploies un portfolio : quelques pages statiques, aucun contenu dynamique, aucune API. Quelle stratégie de déploiement, et qu’est-ce que ça t’épargne ?
✅ Solution
Un site 100 % statique n’a pas besoin de Node en prod. On configure output: 'export' dans next.config.js, on lance next build (qui produit un dossier out/ de fichiers HTML/CSS/JS), et on sert out/ directement avec Nginx (comme un site statique, Partie 7.3). Ça t’épargne : aucun process à faire tourner, aucun service à surveiller, aucun crash possible, aucun runtime à patcher — et un site plus rapide. On réserve next start aux apps qui font du rendu serveur.
🧠 Quiz de révision
1. Pourquoi npm ci plutôt que npm install en production ?
npm ci plutôt que npm install en production ?npm ci installe exactement ce que package-lock.json décrit, en repartant d’un node_modules vierge — c’est reproductible et identique à la CI. npm install peut mettre à jour des versions et le lockfile, déployant du non testé. ci échoue même volontairement s’il n’y a pas de lockfile cohérent. Prod = npm ci.
2. Que devient une variable NEXT_PUBLIC_* et quelle est la conséquence ?
NEXT_PUBLIC_* et quelle est la conséquence ?Elle est inlinée dans le bundle JavaScript envoyé au navigateur : elle est donc publique, lisible par tout visiteur. Conséquence : jamais de secret dedans (clé privée, mot de passe, secret de session). Le préfixe est réservé aux valeurs réellement publiques (URL du site, clé publique). Les secrets se lisent côté serveur, sans ce préfixe.
3. À quel moment les variables NEXT_PUBLIC_* sont-elles figées ?
NEXT_PUBLIC_* sont-elles figées ?Au moment du next build, pas au démarrage. Changer une NEXT_PUBLIC_* puis relancer next start ne suffit pas : il faut rebuilder. Les variables serveur (sans préfixe) sont, elles, lues au runtime et prises en compte à chaque redémarrage.
4. Que fait output: 'standalone' et quand l’utiliser ?
output: 'standalone' et quand l’utiliser ?Il fait générer par le build un dossier .next/standalone/ auto-suffisant (serveur Node minimal + seules les dépendances utilisées), déployable sans embarquer tout node_modules. Utile pour des images Docker minces ou un déploiement léger. On lance alors node .next/standalone/server.js (en copiant static et public à côté).
5. Quand peut-on se passer complètement de Node en prod ?
Quand l’app est entièrement statique (SSG pur : pas de SSR, pas de Route Handlers, pas d’ISR). Avec output: 'export', next build produit un dossier out/ de fichiers HTML/CSS/JS que Nginx sert directement, sans aucun process Node. Plus simple, plus rapide, plus robuste. Dès qu’il y a du rendu serveur ou une API interne, il faut Node.
Chapitre suivant : systemd pour Node — transformer ce next start fragile en un service qui redémarre au boot et après un crash.