Vous avez développé une application avec Bun, elle tourne parfaitement en local, et maintenant il faut la mettre en production sur un VPS Linux. Deux approches dominent en 2026 : systemd (natif Linux, robuste, intégré aux logs et au monitoring système) ou PM2 (process manager Node.js historique, compatible avec Bun). Voici le guide pratique pour les deux, avec une recommandation claire selon le contexte.
Ce tutoriel s’inscrit dans notre série Bun. Pour les bases (installation, performance, écosystème), voir notre guide complet Bun en production 2026. Pour une alternative basée Coolify, voir le guide Coolify self-hosted.
systemd ou PM2 — quelle approche
- systemd est le système d’init standard de Linux moderne (Ubuntu, Debian, RHEL, Arch). Il gère natifvement les processus, journaux, redémarrage automatique, dépendances entre services, isolation. C’est l’approche la plus « Unix » et la plus légère.
- PM2 est un process manager écrit en Node.js, conçu initialement pour Node.js, qui apporte clustering, monitoring CLI, log rotation, et un dashboard web optionnel. Compatible avec Bun via l’option
--interpreter.
Recommandation : systemd pour les nouveaux projets et les VPS simples ; PM2 si vous gérez déjà une infra avec PM2 ou si vous voulez du clustering automatique sur plusieurs cœurs CPU sans modifier le code.
Prérequis
- VPS Linux (Ubuntu 22.04+, Debian 12+) avec accès root SSH
- Bun installé globalement (
/usr/local/bin/bun) - Une application Bun fonctionnelle (testée avec
bun run start) - Niveau attendu : intermédiaire
- Temps : 30 minutes
Approche 1 — systemd
Étape 1 — Préparer le serveur
# Créer un utilisateur dédié (sans shell)
useradd -r -s /bin/false -m -d /opt/myapp myapp
# Cloner / déployer le code
mkdir -p /opt/myapp/app
chown -R myapp:myapp /opt/myapp
cd /opt/myapp/app
git clone https://github.com/votre-org/myapp.git .
# Installer les dépendances en tant que user dédié
sudo -u myapp bun install --production
sudo -u myapp bun run build # si vous avez une étape de build
# Installer Bun globalement si pas déjà fait
curl -fsSL https://bun.sh/install | bash
mv ~/.bun/bin/bun /usr/local/bin/bun
chmod +x /usr/local/bin/bun
Étape 2 — Créer le service systemd
# /etc/systemd/system/myapp.service
[Unit]
Description=Mon API Bun
After=network.target postgresql.service
Requires=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp/app
EnvironmentFile=/opt/myapp/app/.env
ExecStart=/usr/local/bin/bun run start
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# Sécurité - hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/app/uploads
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
# Limites resources
MemoryMax=512M
CPUQuota=80%
[Install]
WantedBy=multi-user.target
Le bloc « hardening » est important : il restreint ce que le processus peut faire au cas où il serait compromis. Les valeurs ci-dessus conviennent à une API qui n’a besoin que de réseau et d’un répertoire d’uploads.
Étape 3 — Activer et lancer
systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
# Vérifier le statut
systemctl status myapp
# Voir les logs en direct
journalctl -u myapp -f
# Logs des dernières 100 lignes
journalctl -u myapp -n 100 --no-pager
# Redémarrer après une mise à jour
systemctl restart myapp
Étape 4 — Reverse proxy avec Caddy
Votre app Bun écoute typiquement sur 127.0.0.1:3000. Mettez Caddy ou Nginx devant pour HTTPS automatique. Avec Caddy, c’est trois lignes :
# /etc/caddy/Caddyfile
api.exemple.sn {
reverse_proxy 127.0.0.1:3000
encode gzip zstd
}
Caddy obtient automatiquement un certificat Let’s Encrypt et redirige HTTP vers HTTPS. systemctl reload caddy.
Étape 5 — Déploiements zero-downtime
Pour mettre à jour sans interruption visible, deux techniques avec systemd :
- Symlinks atomiques : déployer dans
/opt/myapp/releases/v123/, faire unln -sfnde/opt/myapp/currentvers la nouvelle release, puissystemctl restart myapp. Le redémarrage prend ~1 seconde grâce à Bun. - Socket activation : systemd peut tenir le socket TCP ouvert pendant qu’un nouvel processus prend le relais. Plus complexe, surtout utile pour des services à très haute disponibilité.
Pour la plupart des PME, un simple restart Bun (300-800 ms) est invisible si Caddy fait du failover. Sinon, deux instances Bun sur ports différents avec un round-robin Caddy fait l’affaire.
Approche 2 — PM2
Étape 1 — Installer PM2
# PM2 lui-même est en Node.js, donc il faut Node OU bun pour l'installer
npm install -g pm2
# Ou via Bun
bun install -g pm2
Étape 2 — Créer le fichier ecosystem
// ecosystem.config.js
module.exports = {
apps: [{
name: "myapp",
script: "./src/index.ts",
interpreter: "/usr/local/bin/bun",
interpreter_args: "run",
instances: 2, // ou "max" pour 1 par CPU
exec_mode: "cluster", // load balancing automatique
env: {
NODE_ENV: "production",
PORT: 3000,
},
max_memory_restart: "500M",
error_file: "./logs/err.log",
out_file: "./logs/out.log",
log_date_format: "YYYY-MM-DD HH:mm:ss",
merge_logs: true,
autorestart: true,
max_restarts: 10,
min_uptime: "10s",
}],
};
Étape 3 — Lancer et persister
# Lancer
pm2 start ecosystem.config.js
# Status
pm2 status
pm2 logs myapp
# Persister entre reboots (génère un service systemd PM2)
pm2 save
pm2 startup systemd -u myuser --hp /home/myuser
# Suivre l'instruction sudo qui s'affiche
# Reload zero-downtime
pm2 reload myapp
# Restart classique
pm2 restart myapp
# Stop / delete
pm2 stop myapp
pm2 delete myapp
Étape 4 — Logs et monitoring
PM2 inclut un dashboard CLI via pm2 monit, des métriques par app, et la rotation automatique des logs avec :
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 30
Comparaison concrète
| Critère | systemd | PM2 |
|---|---|---|
| Setup initial | 5-15 min (fichier .service) | 5 min (npm install + ecosystem) |
| Logs | journalctl natif, intégré | Fichiers + pm2 logs |
| Cluster mode | Manuel (multiples services) | Natif via instances + cluster |
| Hardening sécurité | Très avancé (NoNewPrivileges, ProtectSystem…) | Limité |
| Reload zero-downtime | Manuel (symlinks ou socket activation) | pm2 reload |
| Dépendance externe | Aucune (natif Linux) | Node.js ou Bun pour PM2 lui-même |
| Empreinte mémoire | ~0 (kernel) | ~50 Mo pour PM2 daemon |
Quel choix pour quel cas
VPS petit (1-2 Go RAM), une seule app → systemd. Plus léger, zero overhead.
Plusieurs apps Bun sur le même serveur, besoin de monitoring CLI → PM2. La vue d’ensemble pm2 status est pratique.
Besoin de cluster mode pour saturer un CPU multi-core → PM2 cluster, ou systemd avec plusieurs instances numérotées (myapp@1, myapp@2…) derrière un load balancer.
Sécurité critique, hardening fort → systemd avec les directives ProtectSystem, NoNewPrivileges, etc.
Adaptation Afrique de l’Ouest
Pour les VPS modestes des freelances et PME ouest-africaines (Hetzner CX22 4 Go RAM), systemd est le choix gagnant : il économise 50 Mo de RAM par rapport à PM2, ce qui permet d’héberger une app supplémentaire. La courbe d’apprentissage est légèrement plus haute mais ça vaut le coup pour le contrôle fin.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| systemd: status=203/EXEC | Chemin Bun incorrect dans ExecStart | which bun et adapter le chemin absolu |
| Permission denied sur uploads | ProtectSystem trop strict | Ajouter ReadWritePaths=/path/to/uploads |
| PM2 OOM | memory_restart trop bas | Augmenter max_memory_restart selon profil app |
| App ne démarre plus après reboot (PM2) | pm2 save oublié | Refaire pm2 save + pm2 startup |
| Logs PM2 saturent le disque | Pas de rotation | Installer pm2-logrotate |
Pour aller plus loin
- Guide complet Bun en production 2026
- Bun + Hono : API REST type-safe
- Bun + Drizzle ORM PostgreSQL
- Alternative : Coolify self-hosted
- Documentation systemd :
man systemd.service - Documentation PM2 : pm2.keymetrics.io/docs