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 (informations vérifiées en avril 2026, susceptibles d’évoluer) : 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 pratique 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 CX23 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 |
Lectures complémentaires
- guide pratique 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
Étape 1 : choisir entre systemd et PM2 pour superviser Bun
Bun, le runtime JavaScript ultra-rapide signé Oven, est désormais stable en production (version 1.2.x en 2026). Pour superviser un processus Bun sur un VPS Hetzner, OVH ou Contabo loué depuis Dakar, deux écoles s’opposent : systemd (natif Linux, sans dépendance, intégré au système) et PM2 (écosystème Node, monitoring intégré, multi-process).
Choisissez systemd si vous gérez 1 à 3 services Bun, si vous voulez zéro overhead et si vous êtes confortable avec les unit files. Choisissez PM2 si vous gérez plus de 5 processus, si vous voulez un dashboard en ligne, ou si vous mutualisez Bun et Node sur la même machine.
# Critères de décision rapide
systemd : 1-3 services, 0 dépendance, journald natif
PM2 : 5+ services, monitoring web, cluster mode
Indicateur que tout est en place : votre choix se fait en 2 minutes après ce filtre. Si vous hésitez, démarrez avec systemd : vous éviterez une dépendance et apprendrez un outil que vous retrouverez sur tout serveur Linux.
Étape 2 : installer Bun en production sur Ubuntu 24.04 LTS
Connectez-vous en SSH à votre VPS. Recommandation : Ubuntu 24.04 LTS, supportée jusqu’en 2029, idéale pour un site itskillscenter ou une API SaaS hébergée à Falkenstein ou Roubaix.
# Installation officielle Bun
curl -fsSL https://bun.sh/install | bash
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
bun --version
# Sortie attendue : 1.2.x (ou supérieur)
Pour une installation système (accessible à tous les utilisateurs), placez le binaire dans /usr/local/bin/bun et donnez les droits d’exécution. Cette étape est nécessaire si systemd lance Bun sous un utilisateur dédié sans accès au home d’installation.
# Installation système
sudo cp ~/.bun/bin/bun /usr/local/bin/bun
sudo chmod +x /usr/local/bin/bun
which bun
# Sortie attendue : /usr/local/bin/bun
Le marqueur de succès : bun --version retourne un numéro de version. Si la commande est introuvable, le PATH n’est pas chargé, relancez le shell ou exécutez source ~/.bashrc.
Étape 3 : créer un utilisateur dédié pour le service Bun
Ne lancez jamais un service Bun en root. Créez un utilisateur système sans shell, dédié au service. Cette pratique limite la casse en cas de compromission de l’application.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin bunapp
sudo mkdir -p /var/www/monapp
sudo chown -R bunapp:bunapp /var/www/monapp
# Vérification
id bunapp
# Sortie attendue : uid=999(bunapp) gid=999(bunapp) groups=999(bunapp)
Déposez le code de votre application dans /var/www/monapp. Pour un projet Bun classique avec un point d’entrée server.ts qui écoute sur le port 3000, l’arborescence ressemble à ceci.
/var/www/monapp/
├── package.json
├── bun.lockb
├── server.ts
├── src/
└── .env
Vous saurez que tout fonctionne quand : sudo -u bunapp bun --version fonctionne. Si vous obtenez une erreur de permissions, vérifiez que /usr/local/bin/bun est bien exécutable par tous.
Étape 4 : créer un service systemd pour Bun
Créez le fichier /etc/systemd/system/monapp.service avec le contenu suivant. Adaptez les chemins, l’utilisateur et la commande de démarrage à votre projet.
[Unit]
Description=Application Bun MonApp
After=network.target
[Service]
Type=simple
User=bunapp
Group=bunapp
WorkingDirectory=/var/www/monapp
EnvironmentFile=/var/www/monapp/.env
ExecStart=/usr/local/bin/bun run server.ts
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=monapp
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Activez et lancez le service. daemon-reload est obligatoire après toute modification d’un unit file.
sudo systemctl daemon-reload
sudo systemctl enable monapp
sudo systemctl start monapp
sudo systemctl status monapp
# Sortie attendue : Active: active (running)
La preuve que ça tourne : la sortie de status affiche « active (running) » en vert. Si vous voyez « failed », consultez les logs via journalctl -u monapp -n 50 pour trouver l’erreur.
Étape 5 : superviser et lire les logs avec journald
Avec systemd, tous les logs partent dans journald. Plus besoin de gérer la rotation manuelle, c’est natif. Quelques commandes essentielles à mémoriser.
# Logs en temps réel
sudo journalctl -u monapp -f
# 100 dernières lignes
sudo journalctl -u monapp -n 100
# Logs depuis 1 heure
sudo journalctl -u monapp --since "1 hour ago"
# Logs entre deux dates
sudo journalctl -u monapp --since "2026-05-04 09:00" --until "2026-05-04 18:00"
Pour limiter la taille des logs (utile sur un VPS avec 40 Go de disque), configurez /etc/systemd/journald.conf avec SystemMaxUse=500M. Redémarrez avec sudo systemctl restart systemd-journald.
La preuve que ça tourne : journalctl -u monapp -f affiche les logs en streaming dès que l’application écrit. Si rien n’apparaît, vérifiez que votre code Bun écrit bien sur stdout/stderr.
Étape 6 : installer et configurer PM2 pour Bun
Si vous avez choisi PM2, installez-le globalement via npm (PM2 reste un outil Node, mais il sait piloter un processus Bun).
sudo npm install -g pm2
pm2 --version
# Sortie attendue : 5.x.x ou supérieur
# Lancement du service Bun
cd /var/www/monapp
pm2 start "bun run server.ts" --name monapp --interpreter none
L’option --interpreter none est cruciale : elle dit à PM2 de lancer la commande telle quelle, sans tenter de la passer à Node.
# Sauvegarder la liste des processus
pm2 save
# Générer le service systemd qui relance PM2 au boot
pm2 startup systemd -u $USER --hp /home/$USER
# Copiez et exécutez la commande retournée (sudo env PATH=... pm2 ...)
Validation pratique : pm2 list affiche votre app en statut « online ». Si elle est en « errored », consultez pm2 logs monapp --lines 50.
Étape 7 : exploiter le mode cluster de PM2 pour scaler
PM2 propose un mode cluster qui lance plusieurs instances de votre app et répartit la charge. C’est utile sur un VPS avec 4 vCPU et plus, par exemple un Hetzner CX42 (8 vCPU, 32 Go RAM, environ 33 EUR/mois soit environ 21 650 FCFA).
# Lancer 4 instances en cluster
pm2 start "bun run server.ts" --name monapp -i 4 --interpreter none
pm2 list
# Sortie : 4 lignes monapp (id 0, 1, 2, 3) toutes en "online"
Bun gère nativement le mode cluster via SO_REUSEPORT, donc PM2 n’a qu’à lancer plusieurs instances sur le même port. Aucune modification du code n’est nécessaire.
Vous saurez que tout fonctionne quand : un test de charge avec autocannon -c 100 -d 30 http://localhost:3000 montre une montée en débit proportionnelle au nombre d’instances. Si la charge stagne, votre code a un goulot (DB, mutex, fichier).
Étape 8 : configurer un reverse proxy Nginx devant Bun
Bun écoute généralement sur 3000 ou 8080. Pour exposer en HTTPS sur le port 443 avec un certificat Let’s Encrypt, placez Nginx devant.
# /etc/nginx/sites-available/monapp.conf
server {
listen 443 ssl http2;
server_name api.monsite.sn;
ssl_certificate /etc/letsencrypt/live/api.monsite.sn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.monsite.sn/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Activez le site et générez le certificat avec Certbot.
sudo ln -s /etc/nginx/sites-available/monapp.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d api.monsite.sn
Voir aussi notre guide complet pour lancer un SaaS à Dakar en 2026 où le déploiement back-end est traité de bout en bout. Validation pratique : curl -I https://api.monsite.sn retourne un 200 OK avec en-têtes HSTS.
Étape 9 : mettre à jour Bun sans interruption de service
Bun publie une nouvelle version mineure chaque 2 à 3 semaines. Pour mettre à jour sans coupure, scriptez la procédure et utilisez le hot-reload de PM2 ou un rolling restart systemd.
# Avec PM2 (zero downtime grâce au cluster)
bun upgrade
pm2 reload monapp
# Avec systemd (downtime de 2-3 secondes)
sudo bun upgrade # ou cp depuis ~/.bun/bin/bun
sudo systemctl restart monapp
Pour systemd, vous pouvez approcher le zero-downtime en lançant deux unités sur deux ports différents (3000 et 3001), reverse proxy avec upstream Nginx, basculement progressif. Cette approche est plus complexe mais utile pour un site critique.
Comment vérifier le bon fonctionnement : un client HTTP en boucle (while true; do curl -s -o /dev/null -w "%{http_code}
" https://api.monsite.sn; done) ne voit aucun 502 pendant la mise à jour avec PM2.
Étape 10 : surveiller la production et alerter en cas d’incident
Mettez en place une surveillance externe gratuite (UptimeRobot, BetterStack 1 moniteur gratuit) qui pingue votre endpoint toutes les 5 minutes. Configurez l’alerte SMS via Twilio ou WhatsApp via WhatsApp Business API.
# Endpoint de healthcheck à exposer dans server.ts (Bun + Hono)
import { Hono } from 'hono';
const app = new Hono();
app.get('/health', (c) => c.json({
status: 'ok',
uptime: process.uptime(),
version: '1.2.0'
}));
export default { port: 3000, fetch: app.fetch };
Pour la métrologie système (CPU, RAM, disque), installez htop et vnstat. Pour le monitoring applicatif, exportez les métriques Prometheus via le package prom-client compatible Bun.
Découvrez aussi notre guide pour devenir freelance développeur au Sénégal où la gestion d’infra est valorisée comme service à part entière. Le marqueur de succès : vous recevez une alerte WhatsApp en moins de 5 minutes après une coupure simulée (sudo systemctl stop monapp). Si vous ne recevez rien, l’alerte est mal configurée, testez chaque maillon.