19 déc. 2025

ARCHITECTURE WEB

Optimisations système : Stack Strapi + Next.js avec PM2

ARCHITECTURE WEB

Optimisations système : Stack Strapi + Next.js avec PM2

PM2 avec Next, Strapi
PM2 avec Next, Strapi

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)
const path = require('path');

module.exports = {
  apps: [{
    name: 'frontend',
    append_env_to_name: true,
    
    // ✅ CRUCIAL : Working directory
    cwd: path.join(__dirname, 'next'),
    
    // ✅ Script direct (pas npm pour cluster mode)
    script: './node_modules/.bin/next',
    args: 'start',
    
    // CLUSTER MODE
    exec_mode: 'cluster',
    instances: 2,  // 1 par CPU
    
    // MÉMOIRE
    max_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,
    
    // MAINTENANCE
    cron_restart: '0 4 * * *',  // Redémarrage quotidien à 4h
    
    // LOGS
    log_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,
    
    // ENVIRONNEMENTS
    env: {
      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émarrer
    restart_delay: 5000,
    exp_backoff_restart_delay: 100,
    
    // TIMEOUTS ÉLEVÉS
    listen_timeout: 10000,  // 10 secondes
    kill_timeout: 5000,
    
    // MAINTENANCE (après Next.js)
    cron_restart: '0 5 * * *',  // Redémarrage à 5h
    
    // LOGS
    log_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,
    
    // ENVIRONNEMENTS
    env: {
      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.

La formule

max-old-space-size = max_memory_restart × 0.75 à 0.90

Pourquoi pas 100% ?

Node.js utilise de la mémoire en dehors du heap :

Mémoire totale Node.js = Heap + Code + Stack + Buffers
                         
                         └─ 20-50 Mo
                         └────────── 10-20 Mo
                         └───────────────── 50-100 Mo
                         └──────────── max-old-space-size

Tableau de référence

RAM instance

max-old-space-size

Calcul

Usage

256M

192

256 × 0.75

Micro instance

512M

384

512 × 0.75

Small

800M

768

800 × 0.96

Next.js (recommandé)

1G

896

1024 × 0.90

Strapi (recommandé)

2G

1536

2048 × 0.75

Large instance

Script de calcul

#!/bin/bash
# calculate-heap-size.sh

RAM_INSTANCE=$1

if [ -z "$RAM_INSTANCE" ]; then
    echo "Usage: $0 <RAM_par_instance_en_Mo>"
    echo "Exemple: $0 800"
    exit 1
fi

CONSERVATIVE=$((RAM_INSTANCE * 70 / 100))
RECOMMENDED=$((RAM_INSTANCE * 75 / 100))
AGGRESSIVE=$((RAM_INSTANCE * 85 / 100))

echo "RAM par instance: ${RAM_INSTANCE} Mo"
echo ""
echo "Options max-old-space-size:"
echo "  Conservateur (70%): ${CONSERVATIVE}"
echo "  Recommandé (75%):   ${RECOMMENDED}  ⭐"
echo "  Agressif (85%):     ${AGGRESSIVE}"
echo ""
echo "Configuration suggérée:"
echo "node_args: ['--max-old-space-size=${RECOMMENDED}']"

Utilisation :

chmod +x calculate-heap-size.sh
./calculate-heap-size.sh 800

Répartition RAM (VPS 4 Go)

Voici la répartition optimale pour un serveur avec 4 Go de RAM et 2 CPU :

Système d'exploitation : ~500 Mo
Next.js (2 instances)   : 2 × 800 Mo = 1.6 Go
Strapi (1 instance)     : 1 Go
PostgreSQL              : ~400 Mo
Nginx + autres          : ~100 Mo
────────────────────────────────────────────
Total utilisé           : ~3.6 Go
Marge disponible        : ~400 Mo 

Cette configuration laisse une marge de sécurité confortable tout en maximisant les performances.

Comprendre la vraie consommation mémoire

Le mystère des 98% de RAM

Vous lancez free -m et voyez :

              total        used        free      shared  buff/cache   available
Mem:          15802        3059         789           6       12297       12742
               
            Total      3GB réels                         12.3GB CACHE    12.7GB dispo

Question : Si 3 Go sont utilisés, pourquoi Kubernetes/Docker affiche-t-il 98% de RAM utilisée ?

Réponse : Le cache Linux (buff / cache)

Les trois métriques essentielles

1. RSS (Resident Set Size) - La vraie mémoire utilisée

Mémoire physique réellement allouée aux processus (heap + stack + code). C'est la consommation réelle de vos applications.

Exemple pour Next.js : 317 MB

2. Cache Linux (buff/cache) - Mémoire "reclaimable"

Linux met en cache les fichiers lus pour accélérer les I/O futures. Ce cache est automatiquement libéré si une application a besoin de mémoire.

Exemple : 1.3 GB (node_modules + .next/ + fichiers statiques)

3. Working Set - Ce qui compte vraiment

RSS + cache actif uniquement. C'est la métrique utilisée par Kubernetes pour décider des OOM kills.

Exemple : ~1.2 GB

L'équation Kubernetes

memory.usage (ce que vous voyez)
= RSS (720 MB) + Cache (1.3 GB)
= 2.0 GB (98.6% de la limit)

memory.working_set (la réalité)
= RSS + cache actif uniquement
= 720 MB + 200 MB
= 920 MB (46% de la limit)

Répartition détaillée d'un pod Next.js

Next.js processes (RSS)         317 MB
PM2 + npm                       150 MB
System overhead                 100 MB
────────────────────────────────────────
Total RSS (processus)           ~720 MB
Cache Linux (buff/cache)       ~1.3 GB
────────────────────────────────────────
Kubernetes memory.usage        ~2.0 GB (98%)

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/RAM
pm2 monit

# Voir les derniers logs
pm2 logs --lines 100

# Détails d'une instance
pm2 describe 0

# Redémarrage sans coupure
pm2 reload all

# Sauvegarder la configuration actuelle
pm2 save

# Liste des instances avec stats
pm2 list

Script de vérification santé

#!/bin/bash
# health-check.sh

echo "╔════════════════════════════════════════════╗"
echo "║     HEALTH CHECK - Strapi + Next.js        ║"
echo "╚════════════════════════════════════════════╝"

# RAM serveur
RAM_PERCENT=$(free | grep Mem | awk '{print ($3/$2) * 100.0}' | cut -d. -f1)
echo "RAM Serveur: ${RAM_PERCENT}%"

# PM2 stats
echo ""
echo "=== PM2 Instances ==="
pm2 jlist | jq -r '.[] | 
  "\(.name): \(.monit.memory/1024/1024 | floor)M RAM - \(.monit.cpu)% CPU - \(.pm2_env.restart_time) restarts"'

# Alertes
if [ $RAM_PERCENT -gt 85 ]; then
    echo "⚠️  ALERTE: RAM > 85%"
fi

# Test API Next.js
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000)
if [ "$HTTP_CODE" = "200" ]; then
    echo "✅ Next.js: Online"
else
    echo "❌ Next.js: Offline (HTTP $HTTP_CODE)"
fi

# Test API Strapi
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:1337/_health)
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then
    echo "✅ Strapi: Online"
else
    echo "❌ Strapi: Offline (HTTP $HTTP_CODE)"
fi

Installation :

chmod +x health-check.sh
./health-check.sh

Script de surveillance mémoire

#!/bin/bash
# monitor-memory.sh - Surveillance continue 5 minutes

echo "Monitoring mémoire pendant 5 minutes..."
echo "Time,Total,Next.js,Strapi" > /tmp/memory-log.csv

for i in {1..60}; do
    NEXT=$(pm2 jlist | jq -r '.[] | select(.name | contains("frontend")) | .monit.memory/1024/1024 | floor')
    STRAPI=$(pm2 jlist | jq -r '.[] | select(.name | contains("strapi")) | .monit.memory/1024/1024 | floor')
    TOTAL=$((NEXT + STRAPI))
    TIMESTAMP=$(date +%H:%M:%S)
    
    echo "$TIMESTAMP,$TOTAL,$NEXT,$STRAPI" >> /tmp/memory-log.csv
    echo "[$i/60] $TIMESTAMP - Total: ${TOTAL}M (Next: ${NEXT}M, Strapi: ${STRAPI}M)"
    
    sleep 5
done

echo ""
echo "=== ANALYSE ==="
AVG=$(awk -F, 'NR>1 {sum+=$2; count++} END {print int(sum/count)}' /tmp/memory-log.csv)
MAX=$(awk -F, 'NR>1 {if($2>max) max=$2} END {print max}' /tmp/memory-log.csv)

echo "Moyenne: ${AVG}M"
echo "Maximum: ${MAX}M"
echo "Rapport détaillé: /tmp/memory-log.csv"

Les métriques à surveiller

En production

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 notification
fi

# Alerter si restarts > 10 en 1h
RESTARTS=$(pm2 jlist | jq '[.[].pm2_env.restart_time] | add')
if [ $RESTARTS -gt 10 ]; then
    # Investiguer crashs
fi

# 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 module
pm2 install pm2-logrotate

# Configuration recommandée
pm2 set pm2-logrotate:max_size 20M
pm2 set pm2-logrotate:retain 10         # 10 jours
pm2 set pm2-logrotate:compress true
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'  # Minuit

Logs centralisés

# Créer le dossier
sudo mkdir -p /var/log/pm2
sudo chown -R $USER:$USER /var/log/pm2

Dans vos fichiers ecosystem.config.js :

{
  error_file: '/var/log/pm2/app-error.log',
  out_file: '/var/log/pm2/app-out.log',
  log_file: '/var/log/pm2/app-combined.log',
  merge_logs: true,
}

Checklist de déploiement

Configuration PM2

  • cwd correctement défini avec path.join(__dirname)

  • max_memory_restart adapté à votre RAM

  • --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 boucle
pm2 list  # colonne restart doit être stable

# Vérifier la RAM
free -h  # available doit être > 500M

# Vérifier les logs
pm2 logs --lines 100  # aucune erreur

# Sauvegarder
pm2 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

Exemple : 800M × 0.75 = 600M (ou × 0.96 pour être plus agressif = 768M)

Répartition RAM idéale (4 Go)

Next.js (2 × 800M) = 1.6 GB
Strapi (1 × 1G)    = 1.0 GB
Système + DB       = 1.0 GB
Marge              = 0.4 GB 

Vraie consommation mémoire

memory.usage = RSS + Cache (98% - trompeur)
memory.working_set = RSS + Cache actif (60% - réalité)

Surveillez working_set, pas usage !

Conclusion

L'optimisation d'une stack Strapi/Next.js en production nécessite une compréhension fine de :

  1. La gestion mémoire Linux (buff/cache vs RAM réelle)

  2. Les spécificités de PM2 (cluster vs fork, calcul heap)

  3. Les limites de chaque framework (Next.js cluster-friendly, Strapi non)

  4. 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

Envie d'en savoir plus ?

Un avis à partager, un projet, une question...