Aller au contenu

Le média des artisans du web samedi 5 septembre 2026

DevOps & serveurs

Déployer une application Node.js en production : systemd et reverse proxy

Lancer node app.js suffit pour une démonstration, pas pour la production. Un service systemd et un reverse proxy Nginx forment un socle minimal et fiable pour mettre une application Node.js en ligne, avec redémarrage automatique, TLS et déploiement sans coupure.

Couverture guide deploiement Node.js en production

Lancer node app.js dans un terminal suffit à faire tourner une application le temps d’une démonstration. En production, cette commande laisse le service sans surveillance : le moindre plantage l’arrête, un redémarrage du serveur l’oublie, et rien ne gère le chiffrement ni la montée en charge. Passer d’un processus lancé à la main à un service fiable tient en deux briques éprouvées, un superviseur système et un reverse proxy. Voici un socle minimal, sans dépendance exotique, pour mettre une application Node.js en ligne proprement.

Pourquoi ne pas lancer Node directement

Un processus lancé au clavier hérite de la session qui l’a démarré. Il ne survit pas à une déconnexion SSH, ne redémarre pas après un crash, ne se relance pas au boot de la machine et écoute souvent sur un port haut exposé tel quel. La question n’est pas de savoir si le process va tomber, mais quand, et ce qui se passe ensuite.

ApprocheRedémarrage autoPersistance au bootDépendance
node app.jsNonNonAucune
Gestionnaire applicatif (PM2)OuiVia scriptPaquet npm global
Service systemdOuiNativeDéjà présent sur la distribution

systemd est installé par défaut sur la plupart des distributions serveur. Il sait superviser un processus, le relancer selon une politique définie, l’attacher au démarrage et centraliser ses journaux. Autant s’appuyer dessus plutôt que d’empiler un outil supplémentaire, comme le rappelaient déjà les retours sur l’auto-hébergement maîtrisé.

Un service systemd pour superviser le process

Première règle : l’application ne tourne jamais en root. Un utilisateur système dédié, sans shell de connexion, limite la surface d’attaque en cas de compromission.

sudo useradd --system --home /srv/monapp --shell /usr/sbin/nologin monapp
sudo chown -R monapp:monapp /srv/monapp

Le service se décrit dans un fichier unité placé sous /etc/systemd/system/. Les variables sensibles restent hors du dépôt, chargées depuis un fichier d’environnement à droits restreints.

[Unit]
Description=Application Node monapp
After=network.target

[Service]
Type=simple
User=monapp
Group=monapp
WorkingDirectory=/srv/monapp
EnvironmentFile=/srv/monapp/.env
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /srv/monapp/server.js
Restart=on-failure
RestartSec=2
# Durcissement
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/monapp/tmp

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now monapp
sudo systemctl status monapp
journalctl -u monapp -f

Les journaux partent dans journalctl, horodatés et rotés par le système, sans redirection manuelle vers un fichier. La politique Restart=on-failure relance le service en cas de sortie anormale, et RestartSec évite une boucle de redémarrage trop agressive.

Nginx en reverse proxy

L’application écoute en local, sur 127.0.0.1:3000, et n’est jamais exposée directement à Internet. Nginx se place devant : il termine le TLS, sert les fichiers statiques, applique la compression et transmet le reste à Node.

server {
    listen 80;
    server_name monapp.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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;
        # Support des WebSockets
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    gzip on;
    gzip_types text/plain text/css application/javascript application/json;
}

Le certificat s’obtient et se renouvelle avec Certbot, qui bascule automatiquement la configuration en HTTPS.

sudo certbot --nginx -d monapp.example.com

Une fois le proxy en place, seul le port 443 reste ouvert au public ; le port applicatif ne quitte pas la boucle locale. Ce cloisonnement va de pair avec des sauvegardes régulières, dont le principe est détaillé dans le guide sur les sauvegardes automatisées d’un serveur web.

Déploiement sans coupure

Un simple systemctl restart coupe le service quelques instants. Pour éviter la fenêtre d’indisponibilité, deux instances tournent en parallèle derrière un upstream, et le redémarrage se fait à tour de rôle.

upstream monapp {
    server 127.0.0.1:3000 max_fails=1 fail_timeout=5s;
    server 127.0.0.1:3001 max_fails=1 fail_timeout=5s;
}

Encore faut-il que l’application se ferme proprement : elle doit cesser d’accepter de nouvelles connexions, terminer les requêtes en cours, puis quitter. C’est le rôle d’un gestionnaire d’arrêt sur SIGTERM, le signal envoyé par systemd.

const server = app.listen(process.env.PORT || 3000);

process.on("SIGTERM", () => {
  server.close(() => {
    // connexions drainées, on libère les ressources
    process.exit(0);
  });
  // filet de sécurité si un socket reste bloqué
  setTimeout(() => process.exit(1), 10000).unref();
});

Les deux instances proviennent d’une unité gabarit : un fichier [email protected] reprend l’unité précédente en remplaçant la ligne d’environnement par Environment=PORT=%i, où %i vaut le nombre passé après l’arobase. Le redémarrage roulant relance alors chaque instance l’une après l’autre, en laissant Nginx router le trafic vers celle qui reste disponible.

sudo systemctl enable --now monapp@3000 monapp@3001
# mise à jour sans coupure
sudo systemctl restart monapp@3000
sleep 5
sudo systemctl restart monapp@3001

En production, la vraie question n’est pas si le processus va tomber, mais ce qui se passe à la seconde où il tombe.

Point de vigilance. Trois erreurs reviennent sans cesse : lancer l’application en root, oublier NODE_ENV=production, et versionner les secrets dans le dépôt. La première ouvre la porte à une élévation de privilèges, la deuxième désactive des optimisations et laisse des messages d’erreur trop bavards, la troisième expose des clés dès le premier git push. Un fichier d’environnement à droits 600, détenu par l’utilisateur applicatif, règle le dernier point.

Points de contrôle avant la mise en ligne

ContrôleAttendu
Utilisateur d’exécutionCompte système dédié, jamais root
Variable d’environnementNODE_ENV=production
Politique de redémarrageRestart=on-failure active
ChiffrementTLS via Certbot, renouvellement automatique
Port applicatifLié à 127.0.0.1, jamais public
Point de santéRoute /health pour la supervision
Pare-feuSeuls 80 et 443 ouverts

Ce qu’il faut retenir

Un déploiement Node.js robuste ne demande pas d’outillage lourd : un service systemd pour la supervision, un reverse proxy Nginx pour le TLS et le routage, un utilisateur dédié pour le cloisonnement, et un arrêt propre sur SIGTERM pour les mises à jour sans coupure. Ce socle tient sur n’importe quel serveur récent et reste lisible six mois plus tard, quand il faudra le reprendre. Il vaut aussi bien pour une API que pour une application rendue côté serveur, comme celles bâties sur les versions récentes de Node.js.

J’ai longtemps utilisé PM2, par habitude, avant de revenir à systemd sur mes propres serveurs. La bascule m’a fait gagner une dépendance en moins à maintenir et des journaux enfin unifiés avec le reste de la machine. Mon seul ajout systématique aujourd’hui, c’est une route /health triviale qui vérifie l’accès à la base : c’est elle qui me prévient d’un incident avant que le client ne le fasse. — Simon Janvier

Pour aller plus loin

Référence des directives de service sur la source primaire : documentation systemd.service.

Partager LinkedIn Bluesky Hacker News E-mail

À lire aussi