Skip to Content

Chapitre 9.2 — systemd pour Node

⏱️ TL;DR — Un next start lancé à la main meurt dès que tu fermes ton SSH, et ne remonte pas après un reboot ou un crash. Inacceptable en prod. La solution standard sur Linux : en faire un service systemd. Tu écris un unit file (/etc/systemd/system/formacampus-web.service) qui dit à systemd quoi lancer, sous quel utilisateur, avec quelles variables, et quoi faire s’il tombe. Deux directives sont non négociables : WantedBy=multi-user.target avec systemctl enable (démarrage au boot) et Restart=always (redémarrage après crash). Tu tournes sous l’utilisateur deploy (jamais root), sur un port >1024, et tu lis les logs avec journalctl -u formacampus-web -f.

🎯 Objectifs

  • Comprendre pourquoi une app de prod doit tourner en service et pas en process lancé à la main.
  • Écrire un unit file systemd complet pour Next.js.
  • Maîtriser le cycle daemon-reloadenable --nowstatus → logs.
  • Savoir pourquoi enable (boot) et Restart=always (crash) sont obligatoires.
  • Faire tourner l’app en utilisateur non-root sur un port non privilégié.

Pourquoi un service, et pas un next start nu

Reprends le next start -p 3000 & du chapitre précédent. Il a trois défauts rédhibitoires en prod :

  1. Il meurt avec ta session. Ferme le SSH, et l’app s’arrête. Un tmux prolonge le problème mais ne le résout pas : au reboot, tout disparaît.
  2. Il ne remonte pas au boot. Le VPS redémarre (mise à jour kernel, incident fournisseur) ? Ton site reste down jusqu’à ce que tu te reconnectes pour le relancer à la main. À 3 h du matin.
  3. Il ne survit pas à un crash. Une exception non gérée, un OOM, et le process s’éteint. Personne ne le relance.

systemd — le système d’init de Linux, déjà présent sur ta machine (Partie 3.5) — résout les trois d’un coup. Il démarre ton app au boot, la surveille, la relance si elle tombe, capture ses logs, et te donne un contrôle uniforme (start/stop/status) valable pour tous tes services.

💡 Réflexe — Sur un serveur Linux, la question n’est pas « comment je lance mon app » mais « comment mon app devient un service ». Dès qu’un programme doit tourner en continu (une app web, un worker, un cron long), le réflexe est : unit systemd. C’est le standard, il est déjà là, et tu n’as qu’un système à apprendre pour tout gérer.

Anatomie d’un unit file

Un service systemd est décrit par un fichier .service dans /etc/systemd/system/. Il a trois sections : [Unit] (métadonnées et ordre de démarrage), [Service] (comment lancer et surveiller le process), [Install] (comment l’activer au boot).

Voici le unit complet pour le front FormaCampus. Chaque ligne compte :

# /etc/systemd/system/formacampus-web.service [Unit] Description=FormaCampus - front Next.js # On veut le reseau disponible avant de demarrer l'app : After=network.target [Service] # --- Qui et ou --- User=deploy Group=deploy WorkingDirectory=/var/www/formacampus # --- Environnement --- Environment=NODE_ENV=production Environment=PORT=3000 # Les secrets vivent dans un fichier hors git, lu au demarrage : EnvironmentFile=/var/www/formacampus/.env.production # --- Comment lancer --- # Chemin ABSOLU du binaire (systemd n'herite pas de ton PATH interactif) : ExecStart=/usr/bin/npm start # --- Resilience --- Restart=always RestartSec=5 [Install] # Demarre l'app quand la machine atteint l'etat multi-utilisateurs (au boot) : WantedBy=multi-user.target

Décortiquons les directives clés.

  • User=deploy / Group=deploy : l’app tourne sous l’utilisateur non-root deploy (celui créé en Partie 3.1). Si l’app est compromise, l’attaquant n’a que les droits limités de deploy, pas les clés de la machine.
  • WorkingDirectory= : le dossier depuis lequel la commande s’exécute. C’est là que Next trouve .next/, package.json, node_modules.
  • Environment= : pose une variable directement. On y met le non-secret (NODE_ENV, PORT).
  • EnvironmentFile= : charge un fichier de variables — l’endroit propre pour les secrets (.env.production hors git). systemd lit ce fichier au démarrage du service.
  • ExecStart= : la commande à lancer, en chemin absolu. Ici /usr/bin/npm start, qui exécute le script start de ton package.json (typiquement next start).
  • Restart=always + RestartSec=5 : si le process s’arrête (crash, OOM, exit), systemd le relance au bout de 5 secondes. Toujours.
  • WantedBy=multi-user.target : c’est ce qui, combiné à enable, fait démarrer le service au boot.

⚠️ PiègeExecStart=npm start sans chemin absolu échoue souvent : un service systemd n’hérite pas de ton PATH de shell interactif. Écris le chemin complet du binaire : /usr/bin/npm (trouve-le avec which npm). Avec nvm, npm et node sont dans le home de l’utilisateur (ex. /home/deploy/.nvm/versions/node/vX/bin/) : donne ce chemin, ou ajoute-le via Environment=PATH=.... C’est la cause n°1 d’un service qui refuse de démarrer.

💡 Réflexe — Fais pointer ExecStart vers npm start (le script) plutôt que vers un next start codé en dur. Ton package.json reste la source de vérité de la commande de démarrage : "start": "next start -p 3000". Le jour où tu passes en mode standalone, tu changes une ligne de package.json, pas ton unit systemd.

{ "scripts": { "start": "next start -p 3000" } }

Le port : non privilégié, en local

Ton app écoute sur le port 3000. C’est un port >1024, dit non privilégié : un processus non-root a le droit de l’ouvrir. À l’inverse, les ports <1024 (comme 80 et 443) exigent des privilèges root — raison de plus pour ne pas faire écouter Next directement dessus. Next reste sur 127.0.0.1:3000 (local), et Nginx (chapitre 9.4) écoute en façade sur 443 et relaie.

🔒 Sécurité — Deux barrières ici. Un : l’app tourne en deploy non-root, donc ne peut pas ouvrir un port <1024 ni toucher au système — c’est le moindre privilège. Deux : elle écoute en local (127.0.0.1), injoignable de l’extérieur ; seul Nginx, sur la même machine, lui parle. Même si le pare-feu tombait, personne ne pourrait frapper directement le port 3000. Cloisonnement en profondeur.

Le cycle de vie : installer, activer, surveiller

Une fois le fichier écrit, la manœuvre est toujours la même.

# 1. Recharger systemd pour qu'il decouvre le nouveau (ou modifie) unit sudo systemctl daemon-reload # 2. Activer AU BOOT + demarrer MAINTENANT, en une commande sudo systemctl enable --now formacampus-web # 3. Verifier que l'app tourne systemctl status formacampus-web # -> "active (running)" en vert = c'est bon # 4. Suivre les logs en direct (Ctrl+C pour sortir) journalctl -u formacampus-web -f
  • daemon-reload : à lancer après chaque modification d’un .service, pour que systemd relise le fichier. L’oublier fait tourner l’ancienne version de ta config.
  • enable --now : enable inscrit le service pour le boot ; --now le démarre aussi tout de suite. Deux effets, une commande.
  • status : état courant + les dernières lignes de log. Ton premier réflexe de diagnostic.
  • journalctl -u <service> : tous les logs du service (sortie standard de l’app comprise), capturés par systemd. -f suit en direct, -n 100 montre les 100 dernières lignes, --since "10 min ago" filtre par temps.
# Gestes courants apres une mise a jour du code : sudo systemctl restart formacampus-web # redemarre (stop puis start) sudo systemctl reload formacampus-web # recharge sans couper (si le service le gere) sudo systemctl stop formacampus-web # arrete sudo systemctl disable formacampus-web # ne demarrera PLUS au boot journalctl -u formacampus-web --since today # les logs du jour

enable et Restart=always : non négociables

Ces deux réglages sont la raison d’être du service. Sans eux, tu n’as gagné qu’un lanceur compliqué.

enable = survie au reboot. Sans enable, ton service tourne… jusqu’au prochain redémarrage du VPS, après quoi il ne remonte pas. Or un VPS redémarre : mise à jour du noyau, maintenance du fournisseur, incident. Un service enable remonte tout seul au boot. Un service non-enable te réveille la nuit.

Restart=always = survie au crash. Une app Node finit par crasher un jour (bug, fuite mémoire, dépendance qui panique). Sans Restart=always, elle reste morte jusqu’à intervention humaine. Avec, systemd la relance en secondes, et tes visiteurs voient au pire un hoquet.

⚠️ Piège — Écrire un beau unit, faire systemctl start (ça marche !)… et oublier enable. Tout est vert, tu es content. Puis le VPS reboote une nuit, et le site est down — le service n’était pas inscrit au boot. C’est l’un des pièges les plus courants et les plus vicieux, parce que rien ne le signale tant que la machine ne redémarre pas. Le réflexe : enable --now, jamais start seul. Vérifie avec systemctl is-enabled formacampus-web (doit répondre enabled).

⚠️ Piège — Une boucle de crash-restart. Si l’app crashe immédiatement au démarrage (mauvaise variable d’env, build absent, port déjà pris), Restart=always la relance en boucle, cinq fois par minute, polluant les logs et chauffant le CPU. systemd a un garde-fou (StartLimitIntervalSec/StartLimitBurst) qui finit par abandonner. Le vrai correctif n’est pas de désactiver le restart : c’est de lire journalctl pour trouver pourquoi ça crashe au démarrage, et de le régler.

Diagnostiquer un service qui ne démarre pas

Le scénario classique : systemctl status affiche failed ou activating (auto-restart). La méthode :

systemctl status formacampus-web # etat + extrait de log + code de sortie journalctl -u formacampus-web -n 50 --no-pager # les 50 dernieres lignes

Causes les plus fréquentes, dans l’ordre : chemin d’ExecStart faux (npm introuvable → chemin absolu), WorkingDirectory faux (pas de package.json/.next → build manquant ou mauvais dossier), variable d’env manquante (l’app panique au démarrage → vérifier EnvironmentFile), port déjà pris (EADDRINUSE → un autre process écoute sur 3000), droits (l’utilisateur deploy ne peut pas lire un fichier → chown).

🐚 Au terminal — Le workflow complet, du unit à l’app en service, prêt à copier :

sudo nano /etc/systemd/system/formacampus-web.service # coller le unit sudo systemctl daemon-reload # systemd relit les units sudo systemctl enable --now formacampus-web # boot + demarrage immediat systemctl status formacampus-web # doit etre "active (running)" curl -I http://127.0.0.1:3000 # doit repondre 200 en local journalctl -u formacampus-web -f # surveiller au premier lancement

🧭 Sur FormaCampus — Le front tourne désormais comme le service formacampus-web : User=deploy, WorkingDirectory=/var/www/formacampus, NODE_ENV=production, secrets dans .env.production via EnvironmentFile, Restart=always, enable au boot. L’équipe ferme son SSH sans crainte, reboote le VPS pour un patch noyau — le site remonte tout seul. L’API Symfony aura son propre service (formacampus-api, Partie 10) et Postgres tourne sous postgres (Partie 11). Trois services, trois utilisateurs, une seule console de contrôle : systemctl.

📚 La doc — Les directives d’un unit sont innombrables (limites de ressources, durcissement ProtectSystem, PrivateTmp, dépendances entre services…). La référence fait autorité : man systemd.service et man systemd.exec. Pour aller plus loin sur le durcissement d’un service (le confiner encore davantage), c’est la Partie 3.5 et le cookbook (Partie 16).

✏️ Exercices

Exercice 1 — Le service ne remonte pas au reboot. Un collègue a écrit un unit correct, l’a lancé avec sudo systemctl start monapp, tout marchait. Après un reboot du VPS, le site est down. Qu’a-t-il oublié, et quelle commande corrige ?

✅ Solution

Il a oublié enable. systemctl start démarre le service pour maintenant, mais ne l’inscrit pas au boot : au redémarrage, il ne remonte pas. Il faut sudo systemctl enable monapp (ou, du premier coup, sudo systemctl enable --now monapp qui active et démarre). On vérifie avec systemctl is-enabled monapp, qui doit répondre enabled. Combiné à WantedBy=multi-user.target dans le [Install], c’est ce qui garantit la survie au reboot.

Exercice 2 — Le service refuse de démarrer. systemctl status formacampus-web affiche failed, et journalctl montre npm: command not found. Diagnostic et correctif.

✅ Solution

L’ExecStart référence npm sans chemin absolu, or un service systemd n’hérite pas du PATH interactif : il ne trouve pas le binaire. Correctif : mettre le chemin complet, trouvé avec which npm (souvent /usr/bin/npm en NodeSource). Avec nvm, npm est dans le home (/home/deploy/.nvm/versions/node/vX/bin/npm) : donner ce chemin exact, ou définir Environment=PATH=.... Puis sudo systemctl daemon-reload et sudo systemctl restart formacampus-web.

Exercice 3 — Écris le unit. Rédige un unit systemd minimal mais correct pour une app Node lancée par npm start, tournant sous l’utilisateur deploy depuis /var/www/monapp, en NODE_ENV=production, qui redémarre après un crash et démarre au boot.

✅ Solution

[Unit] Description=Mon app Node After=network.target [Service] User=deploy WorkingDirectory=/var/www/monapp Environment=NODE_ENV=production ExecStart=/usr/bin/npm start Restart=always RestartSec=5 [Install] WantedBy=multi-user.target

L’essentiel y est : User=deploy (non-root), WorkingDirectory (où trouver le code), ExecStart en chemin absolu, Restart=always (survie au crash) et WantedBy=multi-user.target (couplé à enable, survie au boot). Installation : daemon-reload puis enable --now.

🧠 Quiz de révision

1. Pourquoi faire tourner Next en service systemd plutôt qu’avec un next start à la main ?

Parce qu’un next start nu meurt avec la session SSH, ne remonte pas au reboot et ne survit pas à un crash. systemd démarre l’app au boot, la relance après un crash (Restart=always), capture ses logs (journald) et offre un contrôle uniforme (start/stop/status) pour tous les services.

2. À quoi servent respectivement enable et Restart=always ?

enable (avec WantedBy=multi-user.target) fait démarrer le service au boot de la machine — survie au reboot. Restart=always fait relancer le process par systemd s’il s’arrête — survie au crash. Les deux sont non négociables en prod : sans eux, on n’a qu’un lanceur compliqué.

3. Pourquoi mettre un chemin absolu dans ExecStart ?

Parce qu’un service systemd n’hérite pas du PATH de ton shell interactif : ExecStart=npm start donne souvent npm: command not found. Il faut le chemin complet du binaire (/usr/bin/npm, ou le chemin nvm dans le home de l’utilisateur). C’est la cause n°1 d’un service qui ne démarre pas.

4. Pourquoi l’app tourne-t-elle sous deploy et sur un port supérieur à 1024 ?

Sous deploy (non-root) pour le moindre privilège : une app compromise n’a que des droits limités, pas ceux du système. Sur un port >1024 (non privilégié) parce qu’un process non-root ne peut pas ouvrir un port <1024 (comme 80/443) — ceux-là sont réservés à Nginx en façade. L’app reste en local sur 127.0.0.1:3000, injoignable de l’extérieur.

5. Quelle commande pour lire les logs d’un service, et que faire après avoir modifié le unit ?

Les logs : journalctl -u formacampus-web (avec -f pour suivre en direct, -n 50 pour les dernières lignes). Après toute modification d’un fichier .service, il faut sudo systemctl daemon-reload (pour que systemd relise le fichier) puis sudo systemctl restart <service>. Oublier le daemon-reload fait tourner l’ancienne config.


Chapitre suivant : PM2, l’alternative — l’autre façon, très populaire côté Node, de faire tourner ton app, et comment choisir entre les deux.

Last updated on