Optimisations système : Stack Strapi + Next.js avec PM2
ARCHITECTURE WEB
Optimisations système : Stack Strapi + Next.js avec PM2
Déployer une application découplée basée sur Strapi (CMS headless) et Next.js (framework Javascript frontend) sur un serveur avec des ressources limitées demande une configuration fine. Entre les métriques trompeuses de consommation mémoire et les particularités de chaque framework, il est facile de se perdre. Nos recommandations techniques dans cet article.
Les défis pour une architecture découplée NodeJs basée sur Strapi et NextJs
Voici les points-clés à appréhender pour offrir à votre stack un système optimisé.
Strapi gourmand en RAM : Base de données, uploads, cache applicatif
Next.js en cluster vs Strapi en fork : Deux approches différentes
Cache Linux : Le mystère des "98% de RAM utilisée"
max-old-space-size : Comment le calculer correctement
La configuration PM2 recommandée pour Next.js
Next.js est stateless et bénéficie grandement du cluster mode de PM2. Cela permet de distribuer la charge entre plusieurs instances et d'obtenir du load balancing automatique.
Les principes clés appliqués
✅ Cluster Mode : Utilise tous les CPU cores ✅ Multiple Instances : Load balancing automatique ✅ Zero-downtime : Reload progressif sans coupure ✅ Heap optimisé : 75-96% de max_memory_restart
Notre recommandation pour Next.js
Pour un serveur avec 2 CPU et 4 Go de RAM :
// ecosystem.config.js (Next.js)constpath = require('path');module.exports = {apps:[{name:'frontend',append_env_to_name:true,// ✅ CRUCIAL : Working directorycwd:path.join(__dirname,'next'),// ✅ Script direct (pas npm pour cluster mode)script:'./node_modules/.bin/next',args:'start',// CLUSTER MODEexec_mode:'cluster',instances:2,// 1 par CPU// MÉMOIREmax_memory_restart:'800M',node_args:['--max-old-space-size=768',// 800M × 0.96'--optimize_for_size','--gc-interval=100',],// STABILITÉmax_restarts:10,min_uptime:'10s',restart_delay:4000,exp_backoff_restart_delay:100,// MAINTENANCEcron_restart:'0 4 * * *',// Redémarrage quotidien à 4h// LOGSlog_date_format:'YYYY-MM-DD HH:mm:ss Z',error_file:'/var/log/pm2/frontend-error.log',out_file:'/var/log/pm2/frontend-out.log',merge_logs:true,// ENVIRONNEMENTSenv:{NODE_ENV:'production',PORT:3000,},env_production:{NODE_ENV:'production',PORT:3000,NEXT_TELEMETRY_DISABLED:1,},}]}
// ecosystem.config.js (Next.js)constpath = require('path');module.exports = {apps:[{name:'frontend',append_env_to_name:true,// ✅ CRUCIAL : Working directorycwd:path.join(__dirname,'next'),// ✅ Script direct (pas npm pour cluster mode)script:'./node_modules/.bin/next',args:'start',// CLUSTER MODEexec_mode:'cluster',instances:2,// 1 par CPU// MÉMOIREmax_memory_restart:'800M',node_args:['--max-old-space-size=768',// 800M × 0.96'--optimize_for_size','--gc-interval=100',],// STABILITÉmax_restarts:10,min_uptime:'10s',restart_delay:4000,exp_backoff_restart_delay:100,// MAINTENANCEcron_restart:'0 4 * * *',// Redémarrage quotidien à 4h// LOGSlog_date_format:'YYYY-MM-DD HH:mm:ss Z',error_file:'/var/log/pm2/frontend-error.log',out_file:'/var/log/pm2/frontend-out.log',merge_logs:true,// ENVIRONNEMENTSenv:{NODE_ENV:'production',PORT:3000,},env_production:{NODE_ENV:'production',PORT:3000,NEXT_TELEMETRY_DISABLED:1,},}]}
// ecosystem.config.js (Next.js)constpath = require('path');module.exports = {apps:[{name:'frontend',append_env_to_name:true,// ✅ CRUCIAL : Working directorycwd:path.join(__dirname,'next'),// ✅ Script direct (pas npm pour cluster mode)script:'./node_modules/.bin/next',args:'start',// CLUSTER MODEexec_mode:'cluster',instances:2,// 1 par CPU// MÉMOIREmax_memory_restart:'800M',node_args:['--max-old-space-size=768',// 800M × 0.96'--optimize_for_size','--gc-interval=100',],// STABILITÉmax_restarts:10,min_uptime:'10s',restart_delay:4000,exp_backoff_restart_delay:100,// MAINTENANCEcron_restart:'0 4 * * *',// Redémarrage quotidien à 4h// LOGSlog_date_format:'YYYY-MM-DD HH:mm:ss Z',error_file:'/var/log/pm2/frontend-error.log',out_file:'/var/log/pm2/frontend-out.log',merge_logs:true,// ENVIRONNEMENTSenv:{NODE_ENV:'production',PORT:3000,},env_production:{NODE_ENV:'production',PORT:3000,NEXT_TELEMETRY_DISABLED:1,},}]}
Pourquoi cette configuration ?
max-old-space-size = 768M : Calculé à 96% de 800M pour laisser ~4% à Node.js (code, stack, buffers). C'est un ratio agressif mais optimal pour Next.js qui a besoin de beaucoup de heap pour le rendering.
cron_restart à 4h : Nettoie les fuites mémoire potentielles pendant les heures creuses. Même si votre code est parfait, certaines bibliothèques tierces peuvent avoir des micro-fuites.
exp_backoff_restart_delay : Évite les boucles de crash infinies avec des délais progressifs (100ms, 200ms, 400ms, etc.).
La configuration PM2 recommandée pour Strapi
Strapi a des besoins très différents de Next.js. Il est stateful et ne supporte PAS le cluster mode.
Les spécificités du CMS Strapi
⚠️ Fork Mode obligatoire : Cluster incompatible (uploads, cache local) ⚠️ Plus de RAM : DB queries + médias + cache applicatif ⚠️ Démarrage lent : Timeouts plus élevés nécessaires ⚠️ Headers larges : Uploads nécessitent --max-http-header-size
Notre recommandation pour Strapi
// ecosystem.config.js (Strapi)module.exports = {apps:[{name:'strapi',append_env_to_name:true,script:'./node_modules/.bin/strapi',args:'start',// ⚠️ FORK MODE (important !)exec_mode:'fork',instances:1,// MÉMOIRE (plus que Next.js)max_memory_restart:'1G',node_args:['--max-old-space-size=896',// 1G × 0.90'--optimize-for-size','--gc-interval=100','--max-http-header-size=16384',// Pour uploads],// STABILITÉmax_restarts:10,min_uptime:'15s',// Strapi met plus de temps à démarrerrestart_delay:5000,exp_backoff_restart_delay:100,// TIMEOUTS ÉLEVÉSlisten_timeout:10000,// 10 secondeskill_timeout:5000,// MAINTENANCE (après Next.js)cron_restart:'0 5 * * *',// Redémarrage à 5h// LOGSlog_date_format:'YYYY-MM-DD HH:mm:ss Z',error_file:'/var/log/pm2/strapi-error.log',out_file:'/var/log/pm2/strapi-out.log',merge_logs:true,// ENVIRONNEMENTSenv:{NODE_ENV:'production',HOST:'0.0.0.0',PORT:1337,},env_production:{NODE_ENV:'production',HOST:'0.0.0.0',PORT:1337,STRAPI_TELEMETRY_DISABLED:true,STRAPI_LOG_LEVEL:'error',},}]}
// ecosystem.config.js (Strapi)module.exports = {apps:[{name:'strapi',append_env_to_name:true,script:'./node_modules/.bin/strapi',args:'start',// ⚠️ FORK MODE (important !)exec_mode:'fork',instances:1,// MÉMOIRE (plus que Next.js)max_memory_restart:'1G',node_args:['--max-old-space-size=896',// 1G × 0.90'--optimize-for-size','--gc-interval=100','--max-http-header-size=16384',// Pour uploads],// STABILITÉmax_restarts:10,min_uptime:'15s',// Strapi met plus de temps à démarrerrestart_delay:5000,exp_backoff_restart_delay:100,// TIMEOUTS ÉLEVÉSlisten_timeout:10000,// 10 secondeskill_timeout:5000,// MAINTENANCE (après Next.js)cron_restart:'0 5 * * *',// Redémarrage à 5h// LOGSlog_date_format:'YYYY-MM-DD HH:mm:ss Z',error_file:'/var/log/pm2/strapi-error.log',out_file:'/var/log/pm2/strapi-out.log',merge_logs:true,// ENVIRONNEMENTSenv:{NODE_ENV:'production',HOST:'0.0.0.0',PORT:1337,},env_production:{NODE_ENV:'production',HOST:'0.0.0.0',PORT:1337,STRAPI_TELEMETRY_DISABLED:true,STRAPI_LOG_LEVEL:'error',},}]}
// ecosystem.config.js (Strapi)module.exports = {apps:[{name:'strapi',append_env_to_name:true,script:'./node_modules/.bin/strapi',args:'start',// ⚠️ FORK MODE (important !)exec_mode:'fork',instances:1,// MÉMOIRE (plus que Next.js)max_memory_restart:'1G',node_args:['--max-old-space-size=896',// 1G × 0.90'--optimize-for-size','--gc-interval=100','--max-http-header-size=16384',// Pour uploads],// STABILITÉmax_restarts:10,min_uptime:'15s',// Strapi met plus de temps à démarrerrestart_delay:5000,exp_backoff_restart_delay:100,// TIMEOUTS ÉLEVÉSlisten_timeout:10000,// 10 secondeskill_timeout:5000,// MAINTENANCE (après Next.js)cron_restart:'0 5 * * *',// Redémarrage à 5h// LOGSlog_date_format:'YYYY-MM-DD HH:mm:ss Z',error_file:'/var/log/pm2/strapi-error.log',out_file:'/var/log/pm2/strapi-out.log',merge_logs:true,// ENVIRONNEMENTSenv:{NODE_ENV:'production',HOST:'0.0.0.0',PORT:1337,},env_production:{NODE_ENV:'production',HOST:'0.0.0.0',PORT:1337,STRAPI_TELEMETRY_DISABLED:true,STRAPI_LOG_LEVEL:'error',},}]}
Pourquoi Fork et pas Cluster ?
En cluster mode, Strapi rencontre des problèmes critiques :
❌ Uploads perdus : Fichiers sauvegardés sur instance 1, requête suivante arrive sur instance 2 ❌ Cache désynchronisé : Chaque instance a son propre cache en mémoire ❌ Locks de base de données : Conflits de transactions ❌ WebSockets cassés : L'admin panel utilise WebSocket et ne fonctionne pas en cluster
En fork mode, ces problèmes n'existent pas :
✅ Uploads cohérents ✅ Cache unifié ✅ Pas de conflit DB ✅ Admin panel stable
Calcul de max-old-space-size
Le paramètre --max-old-space-size définit la taille maximale du heap V8 (mémoire JavaScript) en mégaoctets.
Verdict : Pas de memory leak ! Les 98% incluent le cache Linux qui est libérable instantanément. Le working_set à ~46% montre que l'application consomme réellement 920 MB, laissant 1 GB de vraie marge avant un OOM kill.
Que contient le cache ?
Linux met en cache tous les fichiers fréquemment lus :
node_modules/ : ~1 GB (100% en cache)
.next/cache/ : ~388 MB (ISR cache)
.next/static/ : ~8 MB (JS/CSS bundles)
.next/server/ : ~1 GB / 2 GB (48% en cache)
Ce cache améliore considérablement les performances en évitant les lectures disque répétées.
Monitoring et scripts utiles
Commandes PM2 essentielles
# Dashboard temps réel CPU/RAMpm2 monit
# Voir les derniers logspm2 logs --lines 100
# Détails d'une instance
pm2 describe 0
# Redémarrage sans coupurepm2 reload all
# Sauvegarder la configuration actuellepm2 save
# Liste des instances avec statspm2 list
# Dashboard temps réel CPU/RAMpm2 monit
# Voir les derniers logspm2 logs --lines 100
# Détails d'une instance
pm2 describe 0
# Redémarrage sans coupurepm2 reload all
# Sauvegarder la configuration actuellepm2 save
# Liste des instances avec statspm2 list
# Dashboard temps réel CPU/RAMpm2 monit
# Voir les derniers logspm2 logs --lines 100
# Détails d'une instance
pm2 describe 0
# Redémarrage sans coupurepm2 reload all
# Sauvegarder la configuration actuellepm2 save
# Liste des instances avec statspm2 list
✅ memory.working_set : La vraie consommation (pas memory.usage !) ✅ restart_time : Nombre de restarts (anomalie si > 10/jour) ✅ cpu % : Alerte si > 80% constant ✅ heap used vs heap limit : Détecte les memory leaks
Alertes recommandées
# Alerter si RAM > 85%
if[$RAM_PERCENT -gt 85 ];then
# Envoyer notificationfi
# Alerter si restarts > 10en 1hRESTARTS=$(pm2 jlist | jq '[.[].pm2_env.restart_time] | add')if[$RESTARTS -gt 10 ];then
# Investiguer crashsfi
# Alerter si working_set > 80% de la limit
# (Kubernetes/Docker uniquement)
# Alerter si RAM > 85%
if[$RAM_PERCENT -gt 85 ];then
# Envoyer notificationfi
# Alerter si restarts > 10en 1hRESTARTS=$(pm2 jlist | jq '[.[].pm2_env.restart_time] | add')if[$RESTARTS -gt 10 ];then
# Investiguer crashsfi
# Alerter si working_set > 80% de la limit
# (Kubernetes/Docker uniquement)
# Alerter si RAM > 85%
if[$RAM_PERCENT -gt 85 ];then
# Envoyer notificationfi
# Alerter si restarts > 10en 1hRESTARTS=$(pm2 jlist | jq '[.[].pm2_env.restart_time] | add')if[$RESTARTS -gt 10 ];then
# Investiguer crashsfi
# Alerter si working_set > 80% de la limit
# (Kubernetes/Docker uniquement)
Rotation des logs
Sans rotation, les logs peuvent remplir le disque rapidement.
Installation pm2-logrotate
# Installer le modulepm2 install pm2-logrotate
# Configuration recommandéepm2 set pm2-logrotate:max_size 20Mpm2 set pm2-logrotate:retain 10 # 10jourspm2 set pm2-logrotate:compress truepm2 set pm2-logrotate:rotateInterval '0 0 * * *' # Minuit
# Installer le modulepm2 install pm2-logrotate
# Configuration recommandéepm2 set pm2-logrotate:max_size 20Mpm2 set pm2-logrotate:retain 10 # 10jourspm2 set pm2-logrotate:compress truepm2 set pm2-logrotate:rotateInterval '0 0 * * *' # Minuit
# Installer le modulepm2 install pm2-logrotate
# Configuration recommandéepm2 set pm2-logrotate:max_size 20Mpm2 set pm2-logrotate:retain 10 # 10jourspm2 set pm2-logrotate:compress truepm2 set pm2-logrotate:rotateInterval '0 0 * * *' # Minuit
--max-old-space-size = 75-90% de max_memory_restart
instances : 2+ pour Next.js, 1 pour Strapi
exec_mode : cluster pour Next.js, fork pour Strapi
cron_restart défini (4h pour Next.js, 5h pour Strapi)
Logs avec rotation activée
env_production défini
Monitoring
pm2-logrotate installé et configuré
Health check scripts configurés
Alertes sur memory.working_set (pas memory.usage)
pm2 save après chaque changement
pm2 startup pour auto-restart au boot
Post-déploiement
# Vérifier qu'il n'y a pas de restarts en bouclepm2 list #colonne restart doit être stable
# Vérifier la RAMfree -h #available doit être > 500M
# Vérifier les logspm2 logs --lines 100 # aucune erreur
# Sauvegarderpm2 save
# Vérifier qu'il n'y a pas de restarts en bouclepm2 list #colonne restart doit être stable
# Vérifier la RAMfree -h #available doit être > 500M
# Vérifier les logspm2 logs --lines 100 # aucune erreur
# Sauvegarderpm2 save
# Vérifier qu'il n'y a pas de restarts en bouclepm2 list #colonne restart doit être stable
# Vérifier la RAMfree -h #available doit être > 500M
# Vérifier les logspm2 logs --lines 100 # aucune erreur
# Sauvegarderpm2 save
Tableau récapitulatif
Paramètre
Next.js
Strapi
Explication
exec_mode
cluster
fork
Next.js = stateless, Strapi = stateful
instances
2 (= nb CPU)
1
Cluster pour Next.js uniquement
max_memory_restart
800M
1G
Strapi plus gourmand (DB + uploads)
--max-old-space-size
768
896
75-90% de max_memory_restart
listen_timeout
3000
10000
Strapi démarre lentement
cron_restart
4h
5h
Éviter restart simultané
Formules clés
Calcul max-old-space-size
max-old-space-size = max_memory_restart × 0.75
max-old-space-size = max_memory_restart × 0.75
max-old-space-size = max_memory_restart × 0.75
Exemple : 800M × 0.75 = 600M (ou × 0.96 pour être plus agressif = 768M)
L'optimisation d'une stack Strapi/Next.js en production nécessite une compréhension fine de :
La gestion mémoire Linux (buff/cache vs RAM réelle)
Les spécificités de PM2 (cluster vs fork, calcul heap)
Les limites de chaque framework (Next.js cluster-friendly, Strapi non)
Le monitoring proactif (working_set vs usage)
Les métriques peuvent être trompeuses : 98% de RAM utilisée ne signifie pas nécessairement un problème si la majorité est du cache Linux libérable.
L'optimisation est un processus itératif :
Déployer avec des valeurs conservatives
Monitorer pendant 1-2 semaines
Ajuster selon les données réelles
Documenter chaque changement
Avec les configurations et scripts fournis dans cet article, votre stack est maintenant production-ready et optimisée pour la performance et la stabilité.
Configuration testée avec Next.js 15, Strapi 4, PM2 5.x et Node.js 20+ sur serveur 4 Go RAM / 2 CPU