jules@halfnil:~$ cat ~/blog/heberger-nextjs-sur-un-vps.md

Héberger Next.js sur un VPS en 2026 (oui, sans Vercel).

pm2, GitHub Actions en SSH, et les deux pièges qui m'ont réellement cassé un déploiement — le Node fantôme et le script qui se met à jour lui-même.

12 juillet 2026 · 2 min ·

Tous mes projets tournent sur mon propre VPS. Pas par idéologie anti-cloud : parce que déployer soi-même est le meilleur cours d'infra qu'on puisse suivre, et qu'un VPS à quelques euros héberge sans effort un portfolio, un dashboard et ce qui viendra ensuite.

## L'architecture, en une ligne

GitHub Actions se connecte en SSH au VPS et lance un script : git pull, npm i, prisma db push, next build, pm2 restart. C'est tout. Pas de Docker, pas de Kubernetes — un process Node géré par pm2 derrière un reverse proxy.

yaml
# .github/workflows/deploy.yml — l'essentiel
on:
  push:
    branches: [main]
jobs:
  deploy:
    steps:
      - uses: appleboy/ssh-action@v1.2.0
        with:
          script: bash $HOME/portfolio-v2/scripts/server-deploy.sh

## Piège n°1 : le Node fantôme

Mon premier déploiement a explosé avec des erreurs incompréhensibles. La cause : la session SSH non-interactive de GitHub Actions ne charge pas nvm, et retombait sur le Node système — un antique v12, alors que Next 15 exige ≥ 18.18. En interactif tout marchait, ce qui rendait le bug invisible.

Le fix, dans le script de déploiement :

bash
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use 20
# garde-fou : refuser de continuer avec un Node trop vieux
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
[ "$NODE_MAJOR" -ge 18 ] || exit 1

## Piège n°2 : le script qui se met à jour lui-même

Plus vicieux : le script de déploiement fait git pull… qui met à jour le script en train de tourner. Le run en cours continue d'exécuter l'ancienne version (bash garde son descripteur sur l'ancien fichier). Concrètement : le jour où j'ai ajouté prisma db push au script, le déploiement qui apportait ce changement ne l'a pas exécuté — il a fallu relancer le workflow une deuxième fois.

À retenir : toute modification du script de déploiement ne prend effet qu'au run suivant. Prévoyez un déclenchement manuel (workflow_dispatch) pour ces cas-là.

## Ce que je retiens

  • Un garde-fou qui échoue bruyamment (version de Node) vaut mieux qu'un échec mystérieux trois étapes plus loin.
  • Le déploiement est du code : il a ses bugs, ses versions, et mérite la même rigueur.
  • Posséder la chaîne entière — du CSS au reverse proxy — change la façon dont on écrit l'application elle-même.

## Pour aller plus loin