Aller au contenu

Next.js : build et déploiement

Noms génériques : application monapp, utilisateur monapp, dossier /srv/monapp, port local 3000, domaine exemple.com.

Mode output Résultat Serveur Node.js
Serveur (défaut) (rien) .next/ + next start Oui
Autonome 'standalone' Dossier minimal exécutable avec node Oui
Export statique 'export' Fichiers HTML dans out/ Non

L’export statique n’est possible que sans fonctions serveur (pas de route handler, de Server Action, de rendu à la demande, d’optimisation d’images à la volée).

Fenêtre de terminal
npm ci
npm run build

La sortie du build liste chaque route avec ○ (statique), ● (statique avec paramètres générés) ou ƒ (dynamique, rendue à chaque requête). Un build qui échoue signale en général une erreur de types, de lint ou de données.

next.config.ts :

const config: NextConfig = { output: 'standalone', poweredByHeader: false };
export default config;

Après npm run build, le dossier .next/standalone/ contient un serveur minimal avec ses dépendances. Il faut y copier les assets :

Fenêtre de terminal
cp -r public .next/standalone/public
mkdir -p .next/standalone/.next
cp -r .next/static .next/standalone/.next/static
cd .next/standalone
HOSTNAME=127.0.0.1 PORT=3000 node server.js

Déployer ce dossier sur le serveur :

Fenêtre de terminal
rsync -az --delete --dry-run .next/standalone/ utilisateur@serveur.exemple.com:/srv/monapp/app/
rsync -az --delete .next/standalone/ utilisateur@serveur.exemple.com:/srv/monapp/app/

Lire la sortie du --dry-run. Le dossier standalone est déjà autonome : pas besoin de node_modules sur le serveur (sauf éléments natifs : construire sur une machine de même architecture et même système que le serveur).

/etc/monapp.env (droits 640) :

NODE_ENV=production
HOSTNAME=127.0.0.1
PORT=3000
DATABASE_URL=postgres://app@127.0.0.1:5432/boutique

/etc/systemd/system/monapp.service :

[Unit]
Description=Application Next.js monapp
After=network.target
[Service]
User=monapp
Group=monapp
WorkingDirectory=/srv/monapp/app
EnvironmentFile=/etc/monapp.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/monapp
[Install]
WantedBy=multi-user.target
Fenêtre de terminal
sudo adduser --system --group --home /srv/monapp monapp
sudo systemctl daemon-reload
sudo systemctl enable --now monapp
curl -I http://127.0.0.1:3000/
sudo journalctl -u monapp -f

Le service doit pouvoir écrire dans le dossier de cache de Next.js (.next/cache : images optimisées, pages revalidées) : d’où ReadWritePaths. Voir systemd : écrire un service, durcissement et Node.js : mise en production. Avec PM2 : PM2 : démarrage automatique.

exemple.com {
encode zstd gzip
@statiques path /_next/static/*
header @statiques Cache-Control "public, max-age=31536000, immutable"
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
reverse_proxy 127.0.0.1:3000
}
www.exemple.com {
redir https://exemple.com{uri} permanent
}

Les fichiers de /_next/static/ ont un nom avec empreinte : cache durable. Voir Caddy : exemples réutilisables et, pour le pare-feu, Exemple : VPS avec Caddy.

const config: NextConfig = {
output: 'export',
images: { unoptimized: true },
trailingSlash: true,
};
Fenêtre de terminal
npm run build # produit out/
rsync -az --delete out/ utilisateur@serveur.exemple.com:/var/www/monsite/

Servi comme n’importe quel site statique (voir Astro : rendu et déploiement pour un exemple de bloc Caddy avec page 404).

  • NEXT_PUBLIC_* : intégrées au build (reconstruire pour les changer).
  • Les autres : lues à l’exécution par le serveur (/etc/monapp.env).
  • Aucun secret dans NEXT_PUBLIC_* ni dans le dépôt.
Fenêtre de terminal
# sur la machine de build
npm ci && npm run build
rsync -az .next/standalone/ utilisateur@serveur.exemple.com:/srv/monapp/app.nouvelle/
# sur le serveur
sudo systemctl stop monapp
mv /srv/monapp/app /srv/monapp/app.ancienne && mv /srv/monapp/app.nouvelle /srv/monapp/app
sudo systemctl start monapp
curl -fsS http://127.0.0.1:3000/ >/dev/null && echo OK

Garder l’ancienne version pour un retour arrière rapide (mv inverse). Pour un rechargement sans coupure avec plusieurs instances : PM2 : mode cluster et reload.

Le cache de Next.js (pages revalidées, fetch) est par instance et stocké sur le disque local : avec plusieurs instances ou plusieurs serveurs, chaque instance a son propre cache, ce qui peut donner des réponses différentes. Pour un cache partagé, configurer un gestionnaire de cache personnalisé (voir la documentation cacheHandler).

  • output: 'standalone' et copie de public/ et .next/static.
  • Service avec un utilisateur dédié et des droits d’écriture sur le cache.
  • Écoute sur 127.0.0.1, reverse proxy HTTPS.
  • Cache long sur /_next/static/* seulement.
  • Pas de secret dans NEXT_PUBLIC_*.
  • Journaux : journalctl -u monapp.
Symptôme Piste
502 Service arrêté ou mauvais port : systemctl status monapp, curl http://127.0.0.1:3000
Styles et images manquants .next/static et public non copiés dans standalone
Le service redémarre en boucle journalctl -u monapp -n 50 : variable manquante, port occupé, fichier server.js absent
EACCES dans .next/cache Droits d’écriture de l’utilisateur du service
HOSTNAME ignoré (écoute sur toutes les interfaces) Définir HOSTNAME=127.0.0.1 dans l’environnement du service
Le build échoue sur le serveur (mémoire) Construire ailleurs (poste, CI), NODE_OPTIONS=--max-old-space-size=4096
Variable NEXT_PUBLIC_* obsolète Reconstruire
Erreur sharp sur le serveur Architecture ou système différents de la machine de build

Ubuntu 24.04 et 26.04 : différences à connaître

Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »

Aucune propre à Next.js. Le chemin de node (which node) dépend de l’installation ; construire avec la même version majeure de Node.js que celle du serveur.