backend / nodejs
Déploiement en production — PM2 et logs structurés
Explication
Ce que vous allez apprendre
- Superviser un processus Node.js en production avec PM2 (redémarrage automatique, mode cluster)
- Déployer une mise à jour sans coupure de service grâce au reload zero-downtime
- Produire des logs structurés en JSON avec Pino, exploitables par des outils d'agrégation
- Implémenter un arrêt propre (graceful shutdown) qui ne coupe jamais une requête en cours
- Exposer un endpoint
/healthpour permettre à un orchestrateur de détecter une instance défaillante
Dans quel contexte ?
Une API tourne en production lancée simplement avec node src/index.js dans un terminal screen. Le jour où le processus plante à 3h du matin suite à une exception non gérée, personne ne s'en aperçoit avant le lendemain matin, et les logs en texte libre ne permettent pas de savoir quelles requêtes ont échoué. Cette dernière leçon du cours couvre exactement l'outillage (PM2, logs structurés, arrêt propre, healthcheck) qui rend une application réellement prête pour la production.
Ce qui change quand une application tourne "pour de vrai"
Lancer une application avec node src/index.js suffit en développement. Mais ça soulève plusieurs problèmes en production : que se passe-t-il si le processus plante à 3h du matin ?
Comment le redémarrer automatiquement, exploiter tous les cœurs du serveur, ou déployer une mise à jour sans couper les utilisateurs ? Cette dernière leçon du cours répond à ces questions concrètes d'exploitation.
Commençons par l'outil qui répond à la plupart de ces problèmes : PM2, un superviseur de processus. Il relance automatiquement un processus qui plante, sans intervention humaine.
Il répartit aussi la charge sur tous les cœurs CPU disponibles. Le mode cluster de PM2 réutilise en coulisses le module cluster vu dans la leçon sur la performance.
Il permet enfin un "reload" sans coupure de service. Les anciens processus continuent de traiter les requêtes en cours pendant que les nouveaux démarrent, avant que les anciens ne soient définitivement arrêtés.
| Commande PM2 | Effet |
|---|---|
pm2 start app.js -i max | démarre en mode cluster, 1 processus par cœur CPU |
pm2 reload api | met à jour sans coupure de service (zero-downtime) |
pm2 logs / pm2 monit | logs et tableau de bord CPU/mémoire en temps réel |
pm2 save && pm2 startup | relance automatique après un reboot du serveur |
Une fois le processus supervisé, une autre question se pose : comment comprendre ce qui se passe à l'intérieur ? Un console.log("Utilisateur connecté") est lisible par un humain, mais difficile à interroger à grande échelle.
Des logs structurés en JSON, via Pino, résolvent ce problème. Ils permettent à des outils d'agrégation comme ELK ou Datadog de filtrer, compter et alerter sur des champs précis.
Une fois les logs exploitables, il reste un point souvent négligé : comment arrêter le processus proprement ? Arrêter sans précaution peut couper des requêtes en cours ou laisser des connexions ouvertes.
Un arrêt propre suit un ordre précis. Cesser d'accepter de NOUVELLES connexions, laisser les requêtes en cours se terminer, fermer les connexions externes, puis seulement quitter le processus.
Un timeout de sécurité force l'arrêt si cette séquence traîne trop longtemps. Ça évite qu'un arrêt bloqué immobilise le déploiement.
Bonne pratique
Implémentez toujours un arrêt propre (process.on("SIGTERM", ...)) qui cesse d'accepter de nouvelles connexions AVANT de fermer les connexions existantes (base de données, Redis). Sans cela, un redéploiement ou un scaling automatique peut couper des requêtes utilisateur en plein traitement.
Pour finir, un dernier outil permet à l'infrastructure de s'auto-réparer : le healthcheck. Un endpoint /health permet à un orchestrateur de détecter automatiquement qu'une instance est défaillante et de la remplacer.
Et la suite ? Cette leçon conclut le cours en reliant toutes les précédentes : c'est la somme du code applicatif, de la sécurité, des tests et de cette supervision opérationnelle qui rend une application vraiment prête pour la production.
Commandes & code
Déploiement en production — PM2 et logs structurés
npm install -g pm2
pm2 start src/index.js --name api -i max # -i max = 1 process par cœur CPU (cluster mode)
pm2 list
pm2 logs api
pm2 monit # tableau de bord CPU/mémoire en temps réel
pm2 reload api # zero-downtime reload
pm2 save && pm2 startup # relance automatique au reboot du serveur// ecosystem.config.js — configuration PM2 déclarative
module.exports = {
apps: [
{
name: "api",
script: "src/index.js",
instances: "max",
exec_mode: "cluster",
max_memory_restart: "500M", // redémarre le worker s'il dépasse 500 Mo
env_production: {
NODE_ENV: "production",
PORT: 3000,
},
error_file: "./logs/error.log",
out_file: "./logs/out.log",
merge_logs: true,
log_date_format: "YYYY-MM-DD HH:mm:ss",
},
],
};// Logs structurés (JSON) avec Pino — bien plus exploitable qu'un console.log en production
import pino from "pino";
const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
formatters: {
level: (label) => ({ level: label }), // "info" au lieu de 30
},
timestamp: pino.stdTimeFunctions.isoTime,
});
logger.info({ userId: 42, action: "login" }, "Connexion utilisateur réussie");
logger.error({ err, orderId: order.id }, "Échec du traitement de la commande");
// Middleware Express pour logger chaque requête avec un contexte structuré
app.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
logger.info(
{
method: req.method,
path: req.path,
status: res.statusCode,
durationMs: Date.now() - start,
},
"Requête traitée"
);
});
next();
});// Graceful shutdown — indispensable pour un reload PM2 sans requête coupée
const server = app.listen(config.port);
async function shutdown(signal) {
logger.info(`${signal} reçu, arrêt en cours...`);
server.close(async () => {
logger.info("Serveur HTTP fermé, aucune nouvelle connexion acceptée");
await pool.end(); // ferme proprement le pool de connexions DB
await redisClient.quit(); // ferme la connexion Redis
logger.info("Arrêt propre terminé");
process.exit(0);
});
// sécurité : forcer l'arrêt si le close traîne trop longtemps
setTimeout(() => {
logger.error("Arrêt forcé après timeout");
process.exit(1);
}, 10_000).unref();
}
process.on("SIGTERM", () => shutdown("SIGTERM")); // envoyé par PM2/Docker/K8s
process.on("SIGINT", () => shutdown("SIGINT")); // Ctrl+C# healthcheck exposé pour l'orchestrateur (load balancer, Docker, Kubernetes)
# app.get("/health", (req, res) => res.json({ status: "ok", uptime: process.uptime() }))
# docker-compose.yml
services:
api:
build: .
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
deploy:
resources:
limits:
memory: 512MRésumé
- PM2 en mode cluster exploite tous les cœurs CPU et permet un reload sans coupure (
pm2 reload). - Les logs structurés (JSON via Pino) sont indispensables pour l'agrégation et la recherche en production (ELK, Datadog).
- Le graceful shutdown ferme le serveur HTTP puis les connexions externes (DB, Redis) avant de quitter le process.
- Un endpoint
/healthpermet à l'orchestrateur de détecter et remplacer automatiquement une instance défaillante.
Exercices pratiques
Mission : des requêtes coupées en pleine nuit à chaque déploiement
Objectif : Remplacer un lancement fragile en terminal screen par une supervision PM2 en mode cluster avec reload zero-downtime, un arrêt propre et des logs structurés exploitables.
Contexte
L'API tourne actuellement via node src/index.js lancé dans un terminal screen sur le serveur. Chaque déploiement se fait en tuant le processus (Ctrl+C) puis en relançant la commande, ce qui coupe net toutes les requêtes en cours de traitement à ce moment précis. Cette nuit, le processus a planté suite à une exception non gérée à 3h12 et personne ne l'a su avant 9h, l'API étant restée injoignable pendant près de 6 heures. Les logs actuels sont de simples console.log("Utilisateur connecté") en texte libre, impossibles à filtrer ou agréger.