La configuration VPS qui fonctionne vraiment : Node.js, PM2, Nginx et déploiements sans interruption
La configuration VPS exacte que j'utilisé en production — sécurisation d'Ubuntu, PM2 en mode cluster, reverse proxy Nginx, SSL et un script de déploiement qui ne m'a jamais laissé tomber. Pas de théorie, juste ce qui marché.
Ce blog tourne sur un VPS a 10 $/mois. Pas Vercel, pas AWS, pas un cluster Kubernetes gere par une équipe de six personnes. Une seule machine Ubuntu avec Nginx, PM2 et un script bash qui deploie en moins de 30 secondes.
J'ai essaye les autres chemins. J'ai utilisé Vercel (genial jusqu'a ce que vous ayez besoin de cron jobs, de WebSockets persistants ou juste de contrôle). J'ai utilisé AWS (genial si vous aimez passer la moitie de votre journee dans les politiques IAM). Je finis toujours par revenir a un VPS.
Mais voici le problème : chaque tutoriel "déployer sur un VPS" sur internet s'arrêté au happy path. Ils vous montrent comment installer Node.js et executer node server.js et appellent ca de la production. Puis votre serveur se fait bruteforcer en SSH, votre processus meurt a 3h du matin parce que personne n'a configure de gestionnaire de processus, et votre certificat SSL a expire il y a trois mois.
Voici le guide que j'aurais aime avoir. Tout ici a été eprouve en conditions reelles — cette configuration exacte sert la page que vous etes en train de lire.
Commencez par la sécurité, pas le code#
Avant même de penser a Node.js, verrouillez la machine. Les instances VPS fraiches sont des cibles. Des bots automatises commencent a frapper votre port SSH quelques minutes après le provisionnement.
Creez un utilisateur non-root#
adduser deploy
usermod -aG sudo deployConfigurez l'authentification par cle SSH#
Sur votre machine locale :
ssh-copy-id deploy@your-server-ipPuis desactivez completement l'authentification par mot de passé :
sudo nano /etc/ssh/sshd_configPasswordAuthentication no
PermitRootLogin nosudo systemctl restart sshdSi vous sautez cette étape, vous verrez des milliers de tentatives de connexion echouees dans vos logs d'authentification en quelques jours. Ce n'est pas de la paranoia — c'est un mardi ordinaire sur l'internet public.
Pare-feu avec UFW#
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableC'est tout. Quatre regles. Seuls le trafic SSH et web passent.
Fail2Ban#
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.localEditez /etc/fail2ban/jail.local :
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600sudo systemctl enable fail2ban
sudo systemctl start fail2banTrois tentatives SSH echouees et vous etes banni pour une heure. J'ai regarde Fail2Ban bloquer des centaines d'IP en une seule journee. Ca fonctionne.
Mises a jour de sécurité automatiques#
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgradesVotre serveur va maintenant installer automatiquement les correctifs de sécurité. Une chose de moins a oublier.
Node.js : utilisez NVM, pas apt#
Je vois ca dans chaque tutoriel : sudo apt install nodejs. Ne faites pas ca.
Les depots de paquets d'Ubuntu livrent des versions anciennes de Node.js. Même le PPA NodeSource est en retard. Et quand vous devez basculer entre Node 20 et Node 22 pour differents projets, vous etes bloque.
NVM resout ce problème :
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm alias default lts/*Maintenant verifiez :
node -v # v22.x.x ou la version LTS actuelle
npm -vL'astuce non évidente : quand vous installez des paquets globaux avec NVM (comme PM2), ils sont lies a cette version de Node. Si vous changez de version avec nvm use, vos globaux disparaissent. Definissez votre version par défaut et gardez-la sur le serveur :
nvm alias default 22Ca m'a mordu exactement une fois. Une fois a suffi.
PM2 : le gestionnaire de processus qui vaut le coup#
PM2 est la difference entre "déployé" et "prêt pour la production". Il gere la gestion des processus, le clustering, la rotation des logs, le redemarrage automatique en cas de crash et les scripts de demarrage. Gratuitement.
Installation et configuration#
npm install -g pm2La configuration ecosystem#
Ne demarrez pas les applications avec des flags CLI. Utilisez un fichier ecosystem.config.js. Il est versionne, reproductible et auto-documente.
// ecosystem.config.js
module.exports = {
apps: [
{
name: "akousa",
script: "node_modules/.bin/next",
args: "start -p 3002",
cwd: "/var/www/akousa.net",
instances: 2,
exec_mode: "cluster",
max_memory_restart: "500M",
env: {
NODE_ENV: "production",
PORT: 3002,
},
// Arret gracieux
kill_timeout: 5000,
listen_timeout: 10000,
wait_ready: false,
// Logging
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
error_file: "/var/log/pm2/akousa-error.log",
out_file: "/var/log/pm2/akousa-out.log",
merge_logs: true,
// Redemarrage automatique en cas d'echec
autorestart: true,
max_restarts: 10,
min_uptime: "10s",
// Ne pas surveiller en production
watch: false,
},
],
};Laissez-moi expliquer les choix qui comptent :
instances: 2 au lieu de "max" : Sur un petit VPS avec 1-2 coeurs, "max" semble intelligent mais ca va creer des processus qui se battent pour les ressources pendant les builds. Deux instances vous donné des rechargements sans interruption tout en gardant de la marge. Sur une machine 4+ coeurs, oui, utilisez "max".
exec_mode: "cluster" : C'est ce qui active les rechargements sans interruption. Sans le mode cluster, pm2 reload n'est qu'un redemarrage deguise. Avec le mode cluster, PM2 redemarre les instances une par une — votre application n'est jamais completement hors ligne.
max_memory_restart: "500M" : Votre application Next.js a une fuite mémoire ? PM2 la redemarre avant qu'elle ne provoque un OOM-kill sur votre serveur. Ca m'a sauve des alertes a 2h du matin plus d'une fois.
kill_timeout: 5000 : Donné a votre application 5 secondes pour terminer les requêtes en cours avant que PM2 ne la tue de force. La valeur par défaut (1600 ms) est trop agressive pour les applications avec des connexions a la base de donnees.
watch: false : J'ai vu des gens laisser watch: true en production. PM2 redemarrait alors l'application a chaque modification d'un fichier de log. Votre application entre dans une boucle de redemarrage. Ne faites pas ca.
Script de demarrage#
Faites en sorte que PM2 survive aux redemarrages :
pm2 startup systemd
# Copiez et executez la commande qu'il affiche
pm2 saveCela généré un service systemd. Après un redemarrage du serveur, votre application revient automatiquement. Testez-le — redemarrez votre serveur et verifiez. Ne supposez pas.
Rotation des logs#
Les logs finiront par manger votre disque. Installez le module de rotation :
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true50 Mo max par fichier, garder 7 fichiers tournes, compresser les anciens. Sans ca, j'ai vu /var/log remplir un disque de 25 Go en trois semaines sur une application moderement frequentee.
Nginx : le reverse proxy qui fait plus que vous ne pensez#
"Pourquoi ne pas simplement exposer Node.js directement sur le port 80 ?"
Parce que Nginx gere des choses pour lesquelles Node.js ne devrait pas gaspiller de cycles : la terminaison SSL, le service de fichiers statiques, la compression gzip, le buffering des requêtes, les limites de connexions et la gestion gracieuse des clients lents. Il est ecrit en C et concu specifiquement pour ca.
Installation#
sudo apt install nginx -yLa configuration#
# /etc/nginx/sites-available/akousa.net
upstream node_app {
server 127.0.0.1:3002;
keepalive 64;
}
server {
listen 80;
listen [::]:80;
server_name akousa.net www.akousa.net;
# Rediriger tout le HTTP vers HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name akousa.net www.akousa.net;
# SSL (gere par Certbot — ces lignes sont ajoutees automatiquement)
ssl_certificate /etc/letsencrypt/live/akousa.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/akousa.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# En-tetes de securite
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Compression gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
application/wasm;
# Parametres du proxy
location / {
proxy_pass http://node_app;
proxy_http_version 1.1;
# En-tetes
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 WebSocket (au cas ou vous en auriez besoin)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts — genereux mais pas infinis
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering — laissez Nginx gerer les clients lents
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
}
# Assets statiques Next.js — laissez Nginx les servir directement
location /_next/static/ {
alias /var/www/akousa.net/.next/static/;
expires 365d;
access_log off;
add_header Cache-Control "public, immutable";
}
# Fichiers statiques publics
location /static/ {
alias /var/www/akousa.net/public/static/;
expires 30d;
access_log off;
}
# Bloquer l'acces aux fichiers caches (dotfiles)
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}Activez-le :
sudo ln -s /etc/nginx/sites-available/akousa.net /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxExecutez toujours nginx -t avant de recharger. J'ai un jour pousse une configuration cassee et mis le site hors ligne parce que j'ai saute la verification de syntaxe. Les cinq caracteres nginx -t m'auraient fait economiser trente minutes de débogage panique.
Ce que la plupart des tutoriels oublient dans cette configuration :
Bloc upstream avec keepalive 64 : Nginx reutilise les connexions vers votre backend Node.js au lieu d'ouvrir une nouvelle connexion TCP a chaque requête. Ca compte sous charge.
proxy_buffering on : Nginx lit la réponse entière de Node.js en mémoire, puis l'envoie au client a la vitesse que le client peut gérer. Sans ca, un client lent sur une connexion 3G monopolise votre worker Node.js.
Servir _next/static/ directement : Ce sont des assets haches et immuables. Laissez Nginx les servir depuis le disque avec un header de cache de 365 jours. Vos processus Node.js ne devraient pas perdre de temps la-dessus.
SSL en cinq minutes#
Let's Encrypt a resolu le SSL. Si vous payez encore pour des certificats en 2026, arretez.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d akousa.net -d www.akousa.netCertbot va demander votre email, accepter les conditions d'utilisation et modifier automatiquement votre configuration Nginx pour inclure les directives SSL. C'est tout.
Vérifier le renouvellement automatique#
Certbot installe un timer systemd qui vérifié deux fois par jour et renouvelle les certificats dans les 30 jours avant l'expiration :
sudo systemctl list-timers | grep certbotTestez que le renouvellement fonctionne :
sudo certbot renew --dry-runSi le test a sec passé, vous ne penserez plus jamais au SSL. S'il echoue, c'est généralement parce que le port 80 est bloque (verifiez vos regles UFW) ou que Nginx ne tourne pas.
Un point qui m'a piege : si vous configurez Nginx avant d'executer Certbot, assurez-vous que votre bloc server ecoute sur le port 80 sans la redirection HTTPS d'abord. Certbot a besoin d'atteindre le port 80 pour le challenge HTTP-01. Après que Certbot a reussi, la vous ajoutez la redirection.
Le script de déploiement#
Voici le script qui s'execute chaque fois que je pousse en production. Pas de plateforme CI/CD, pas de GitHub Actions. Juste SSH et bash.
#!/bin/bash
# deploy.sh — deploiement quasi sans interruption
set -euo pipefail
APP_DIR="/var/www/akousa.net"
APP_NAME="akousa"
LOG_FILE="/var/log/deploy.log"
HEALTH_URL="http://localhost:3002"
MAX_RETRIES=10
RETRY_INTERVAL=3
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "=== Deploy started ==="
cd "$APP_DIR"
# Recuperer le dernier code
log "Pulling latest changes..."
git pull origin main 2>&1 | tee -a "$LOG_FILE"
# Installer les dependances
log "Installing dependencies..."
npm install --legacy-peer-deps 2>&1 | tee -a "$LOG_FILE"
# Build
log "Building application..."
rm -rf .next
npm run build 2>&1 | tee -a "$LOG_FILE"
if [ $? -ne 0 ]; then
log "ERROR: Build failed. Aborting deploy."
exit 1
fi
# Recharger PM2 (sans interruption en mode cluster)
log "Reloading PM2..."
pm2 reload "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
pm2 save 2>&1 | tee -a "$LOG_FILE"
# Verification de sante avec retries
log "Running health check..."
for i in $(seq 1 $MAX_RETRIES); do
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
log "Health check passed (HTTP $HTTP_CODE)"
log "=== Deploy completed successfully ==="
exit 0
fi
log "Health check attempt $i/$MAX_RETRIES (HTTP $HTTP_CODE). Retrying in ${RETRY_INTERVAL}s..."
sleep $RETRY_INTERVAL
done
log "ERROR: Health check failed after $MAX_RETRIES attempts"
log "Rolling back to previous PM2 state..."
pm2 restart "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
exit 1Rendez-le executable :
chmod +x deploy.shDeployez depuis votre machine locale :
ssh root@your-server-ip "bash /var/www/akousa.net/deploy.sh"Decisions cles dans ce script :
set -euo pipefail : Le script s'arrêté immédiatement a la moindre erreur. Sans ca, un npm install en echec continue silencieusement vers l'étape de build, et vous obtenez une erreur cryptique qui vous fait perdre 20 minutes a debugger.
rm -rf .next avant le build : Next.js a un cache de build qui produit occasionnellement des sorties obsoletes. Ca m'a mordu une fois — une page affichait du vieux contenu malgre la mise a jour du code source. Supprimer le répertoire de build ajoute peut-être 15 secondes au build mais garantit une sortie fraiche.
pm2 reload au lieu de pm2 restart : C'est la partie sans interruption. En mode cluster, reload effectue un redemarrage progressif — il demarre de nouvelles instances avec le code mis a jour, attend qu'elles soient pretes, puis arrêté gracieusement les anciennes. A aucun moment zero instance ne tourne.
Verification de sante avec retries : Next.js met quelques secondes a se rechauffer après le redemarrage. Le script attend jusqu'a 30 secondes (10 retries x 3 secondes), verifiant si l'application repond avec un HTTP 200. Si ce n'est pas le cas, quelque chose ne va pas et vous devez le savoir immédiatement — pas le decouvrir par un utilisateur.
Rollback en cas d'echec : Si la verification de sante echoue après tous les retries, le script redemarre PM2 (qui charge le dernier état sauvegarde). Ce n'est pas un rollback parfait, mais c'est mieux que de laisser le serveur dans un état casse.
Quand ca casse a 2h du matin#
Voici ce que j'ai réellement debugge sur cette configuration exacte :
"Le site est down"#
Premieres commandes a executer :
pm2 status
pm2 logs akousa --lines 50
sudo systemctl status nginx
sudo tail -50 /var/log/nginx/error.logNeuf fois sur dix, pm2 logs vous dit immédiatement ce qui s'est passé. Une variable d'environnement manquante, une connexion a la base de donnees echouee ou une promesse non geree.
"La mémoire ne cesse d'augmenter"#
pm2 monitCela vous donné un tableau de bord en temps reel du CPU et de la mémoire par processus. Si la mémoire grimpe régulièrement sans se stabiliser, vous avez une fuite. Le parametre max_memory_restart dans votre configuration ecosystem est votre filet de sécurité — PM2 redemarre le processus avant qu'il ne fasse tomber le serveur.
Pour une investigation plus approfondie :
pm2 describe akousaCela montre le temps de fonctionnement, le nombre de redemarrages et des instantanes mémoire. Si vous voyez 47 redemarrages dans les dernieres 24 heures, c'est votre indice.
"Le certificat SSL a expire"#
sudo certbot certificatesListe tous les certificats avec leurs dates d'expiration. Si le renouvellement automatique a echoue :
sudo certbot renew --force-renewal
sudo systemctl reload nginx"Le disque est plein"#
df -h
du -sh /var/log/*
pm2 flushpm2 flush vide immédiatement tous les fichiers de log PM2. Si vous n'avez pas configure la rotation des logs (je vous l'avais dit), c'est la que vous payez le prix.
La commande que je lancé chaque matin#
ssh deploy@akousa.net "pm2 status && df -h / && uptime"Trois choses en une ligne : est-ce que mes processus tournent, est-ce que mon disque va bien, est-ce que le serveur est surcharge. Ca prend deux secondes. Ca detecte les problèmes avant les utilisateurs.
Ce que la plupart des guides ne vous diront pas#
Votre étape de build est votre plus grande vulnérabilité. Sur un VPS avec 1 Go de RAM, npm run build pour une application Next.js peut consommer plus de 800 Mo de mémoire. Si PM2 fait tourner votre application en deux instances pendant le build, vous allez provoquer un OOM. Solutions : utilisez un fichier swap (au moins 2 Go), ou arretez l'application pendant les builds et acceptez quelques secondes d'interruption. J'utilisé le swap.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab--legacy-peer-deps dans votre commande d'installation est un symptome, pas une solution. Je l'utilisé parce que certains paquets dans mon arbre de dépendances n'ont pas mis a jour leurs plages de peer dependencies. Tous les quelques mois j'essaie de le retirer. Un jour ca marchera. En attendant, je livre.
Testez votre script de déploiement de zero. Clonez votre depot sur un serveur vierge et executez chaque étape manuellement. Le nombre de problèmes "ca marché sur ma machine" qui se cachent dans les scripts de déploiement est embarrassant. J'ai trouve trois problèmes dans le mien quand j'ai fait ca — des paquets globaux manquants, des permissions de fichiers incorrectes et un chemin qui n'existait que grace a une configuration manuelle precedente.
Mettez l'IP de votre serveur dans votre configuration SSH. Arretez de taper des adresses IP :
# ~/.ssh/config
Host akousa
HostName 69.62.66.94
User deploy
IdentityFile ~/.ssh/id_ed25519Maintenant ssh akousa est tout ce dont vous avez besoin. Les petites choses s'accumulent.
La checklist complete#
Avant de considerer que c'est termine :
- Utilisateur non-root avec accès sudo
- Authentification SSH par cle uniquement, authentification par mot de passé desactivee
- UFW active avec seulement les ports nécessaires ouverts
- Fail2Ban protege SSH
- Mises a jour de sécurité automatiques activees
- Node.js installe via NVM
- PM2 fait tourner votre application en mode cluster
- Script de demarrage PM2 configure (survit au redemarrage)
- Rotation des logs PM2 installee
- Reverse proxy Nginx avec les bons en-tetes
- SSL via Let's Encrypt avec renouvellement automatique
- Script de déploiement avec verifications de sante
- Fichier swap configure (pour la marge du build)
- Teste : redémarrer le serveur et vérifier que tout revient
Ce dernier point est celui que les gens sautent. Ne soyez pas cette personne. Redemarrez le serveur, attendez 60 secondes, et verifiez si votre application est en ligne. Si elle ne l'est pas, vos scripts de demarrage sont mal configures et vous le decouvrirez au pire moment.
Est-ce "enterprise-grade" ?#
Non. Et c'est justement l'idee.
Cette configuration sert ce blog de manière fiable pour moins de 10 $/mois. Le déploiement prend 30 secondes avec une seule commande. Je comprends chaque piece du puzzle. Quand quelque chose casse, je sais exactement ou regarder.
Est-ce que je pourrais utiliser Docker ? Bien sur. Est-ce que je pourrais utiliser Kubernetes ? Techniquement. Est-ce que je pourrais mettre en place un pipeline CI/CD complet avec des environnements de staging et des déploiements canary ? Absolument.
Mais j'ai appris que la meilleure infrastructure est celle que vous comprenez réellement, que vous pouvez debugger a 2h du matin et qui ne coute pas plus que ce que le projet rapporte. Pour un site personnel, un MVP SaaS ou une petite startup — c'est cette configuration.
Livrez d'abord. Mettez a l'échelle quand c'est nécessaire. Et toujours, toujours, testez votre script de déploiement sur un serveur vierge.