Comment automatiser le déploiement d’une application Node.js en production sur un VPS Debian ? L’idée est simple : à chaque push sur main, GitHub Actions se connecte en SSH au serveur, met à jour le code, installe les dépendances et (re)lance l’app via PM2.
Créer un utilisateur dédié pour le déploiement
Comme nous allons déployer l’application directement sur le serveur et la faire tourner avec PM2, il est préférable d’avoir un utilisateur dédié avec des privilèges limités. En cas de faille dans l’application, un attaquant sera limité sans les droits administrateur.
Créons un utilisateur nommé githubci, qui recevra les connexions SSH déclenchées par GitHub Actions et hébergera le code de l’application dans son répertoire personnel.
adduser githubci
Organiser les fichiers de l’application
Nous allons placer le code de l’application dans le répertoire personnel de githubci, sous un dossier www. Chaque projet aura son sous-dossier. Par exemple pour une app todo, nous utiliserons /home/githubci/www/todolist/.
Ce répertoire sera cloné depuis GitHub au premier déploiement, puis mis à jour à chaque push sur main grâce au script exécuté par la pipeline GitHub.
Les données qui ne doivent pas être versionnées (fichier SQLite, fichier .env, etc.) seront stockées dans un répertoire dédié. Pour l’app todo, ce sera .todolist, soit /home/githubci/.todolist/.
Inutile de créer ces dossiers à la main : le script de déploiement s’en chargera au premier passage. Retenons simplement la structure :
| Rôle | Chemin |
|---|---|
| Code (clone Git) | /home/githubci/www/todolist/ |
| Données / config | /home/githubci/.todolist/ |
Installer NVM sur le serveur
Nous allons utiliser NVM (Node Version Manager). Cela permetra d’installer node spécifiquement pour le user githubci.
A chaque déploiement, le script exécuté par la pipeline GitHub chargera NVM, lancera nvm install --lts et nvm use --lts, puis installera PM2 en global.
Pour commencer il faut se connecter au serveur en SSH, et passer sur le compte de l’utilisateur githubci :
su githubci
Télécharger et exécuter le script d’installation officiel de NVM. Consulter la documentation NVM pour la dernière version
Exemple avec une version fixe :
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
Le script clone NVM dans ~/.nvm et ajoute quelques lignes dans ~/.bashrc pour charger NVM à chaque ouverture de session. Recharger la configuration du shell :
source ~/.bashrc
Vérifier que NVM répond :
nvm --version
À ce stade, ne pas lancer nvm install --lts à la main. Le script de déploiement s’en chargera à chaque déploiement.
Configuration des clées SSH pour GitHub
Afin que GitHub puisse se connecter au VPS en tant que githubci, nous devons disposer d’une paire de clés SSH :
- La clé privée est stockée dans les secrets GitHub (elle ne doit jamais être committée).
- La clé publique est enregistrée :
- sur le VPS, dans
~/.ssh/authorized_keys(pour autoriser la connexion), - sur GitHub, en Deploy key (pour autoriser le
git clone git@github.com:...depuis le VPS).
- sur le VPS, dans
La clé privée est comme une clé personnelle : elle doit rester secrète. Ici, on l’enregistre dans GitHub Secrets (chiffré) pour que la pipeline puisse se connecter au VPS. La clé publique, elle, peut être partagée.
Générer une paire de clés sur le serveur, en tant que githubci :
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
La commande crée deux fichiers : ~/.ssh/id_ed25519 (clé privée) et ~/.ssh/id_ed25519.pub (clé publique).
Pour que les connexions SSH vers githubci fonctionnent, nous devons ajouter la clé publique dans le fichier des clés autorisées :
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Nous aurons besoin dans les prochaines étapes :
- du contenu de la clé privée (pour le secret GitHub),
- du contenu de la clé publique (pour la Deploy key).
Le contenu des clées peut-être affiché avec :
cat ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
Configurer le dépôt GitHub
Nous allons maintenant indiquer à GitHub comment se connecter au serveur et où trouver l’adresse du VPS. Tout se configure dans les paramètres du dépôt.
Ouvrir le dépôt sur GitHub, puis aller dans Settings → Secrets and variables → Actions.
Ajouter le secret contenant la clé privée SSH
Cliquer sur New repository secret. Donner le nom SSH_PRIVATE_KEY et coller le contenu complet du fichier ~/.ssh/id_ed25519 (la clé privée de l’utilisateur githubci). Enregistrer.
Cette clé permettra à la GitHub Action de se connecter au VPS en SSH sans mot de passe.
Ajouter la variable contenant l’adresse du VPS
Aller dans l’onglet Variables (toujours dans Secrets and variables → Actions). Cliquer sur New repository variable. Donner le nom SSH_HOST et la valeur : l’adresse IP ou le nom d’hôte de votre VPS (par ex. vps.example.com). Enregistrer.
Ajouter la clé publique en Deploy key
Afin que le serveur puisse cloner le dépôt avec git clone git@github.com:votre-org/votre-repo.git, GitHub doit accepter la clé publique de githubci. Pour cela, nous devons l’ajoutons comme Deploy key du dépôt.
Aller dans Settings → Deploy keys. Cliquer sur Add deploy key. Donner un titre (par ex. « VPS githubci ») et coller le contenu de ~/.ssh/id_ed25519.pub (la clé publique). Cocher Allow write access uniquement si le script doit pousser des modifications (pour notre cas, la lecture suffit). Enregistrer.
Une fois ces trois éléments en place (secret SSH_PRIVATE_KEY, variable SSH_HOST, Deploy key avec la clé publique), le workflow pourra se connecter au VPS et le serveur pourra cloner le dépôt.
Ajouter le workflow GitHub Actions et le fichier PM2
Nous allons ajouter deux fichiers au dépôt : le workflow qui décrit ce que fait la GitHub Action à chaque push sur main, et le fichier de configuration PM2 qui décrit comment lancer l’application sur le serveur.
Créer le fichier de workflow
À la racine du dépôt, créer le dossier .github/workflows s’il n’existe pas, puis le fichier .github/workflows/deploy.yml avec le contenu suivant. Adapter le nom todolist et le chemin si le projet a un autre nom :
name: Deploy todolist
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.SSH_HOST }}
username: githubci
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
echo "Starting deployment..."
# Charger NVM et utiliser Node.js
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install --lts
nvm use --lts
echo "Node version: $(node -v)"
echo "NPM version: $(npm -v)"
# Définir le répertoire de déploiement
DEPLOY_DIR="$HOME/www/todolist"
REPO_URL="git@github.com:${{ github.repository }}.git"
echo "Deploy directory: $DEPLOY_DIR"
# Cloner ou mettre à jour le dépôt
if [ ! -d "$DEPLOY_DIR/.git" ]; then
echo "Cloning repository..."
git clone "$REPO_URL" "$DEPLOY_DIR"
fi
cd "$DEPLOY_DIR"
git fetch origin
git reset --hard origin/main
# Installer les dépendances
echo "Installing dependencies..."
npm install --production
# Gérer l'application avec PM2
echo "Managing app with PM2..."
npm install -g pm2
pm2 stop todolist || true
pm2 delete todolist || true
NODE_ENV=production pm2 start ecosystem.config.js --name todolist
pm2 save
pm2 show todolist
echo "Deployment completed!"
Ce workflow se déclenche à chaque push sur la branche main. Il fait un checkout du dépôt (nécessaire pour que github.repository soit disponible), puis lance l’action appleboy/ssh-action qui se connecte au VPS avec : l’hôte (SSH_HOST), l’utilisateur githubci et la clé privée (SSH_PRIVATE_KEY).
Le bloc script est exécuté sur le serveur : il charge NVM, installe ou active node LTS, clone ou met à jour le dépôt dans ~/www/todolist, installe les dépendances, puis relance l’application avec PM2 en utilisant le fichier ecosystem.config.js.
Créer le fichier ecosystem.config.js
À la racine du projet (à côté de package.json), créer le fichier ecosystem.config.js. PM2 s’en sert pour savoir quel fichier lancer, dans quel répertoire et avec quelles variables d’environnement. Adapter le nom du script (index.js ou server.js) et le chemin cwd si besoin :
module.exports = {
apps: [{
name: 'todolist',
script: 'index.js',
cwd: '/home/githubci/www/todolist',
watch: false,
env: {
NODE_ENV: 'production',
PORT: 3002
},
instances: 1,
autorestart: true,
}],
};
Le champ name est le nom du processus dans PM2 (pour pm2 logs todolist, pm2 restart todolist, etc.). Le champ script est le point d’entrée de l’application. Le champ cwd est le répertoire de travail sur le serveur. Le champ env définit les variables d’environnement passées à l’application (par ex. PORT dans le cas où on exposerait plusieurs applications). instances à 1 suffit pour une petite app ; autorestart permet à PM2 de relancer l’app en cas de crash.
Enregistrer le fichier et le commiter avec le workflow : les deux doivent être versionnés dans le dépôt.
Premier déploiement
Une fois le workflow et le fichier ecosystem.config.js ajoutés au dépôt, pousser les modifications sur la branche main :
git add .github/workflows/deploy.yml ecosystem.config.js
git commit -m "Add GitHub Actions deploy workflow and PM2 config"
git push origin main
Ouvrir l’onglet Actions du dépôt sur GitHub. L’execution du workflow « Deploy todolist » doit apparaître en cours, puis en succès lorsque l’execution de la pipeline est terminée et OK.
En cas d’échec, consulter les logs de l’exécution : ils affichent la sortie du script sur le serveur. Vérifier que le secret SSH_PRIVATE_KEY, la variable SSH_HOST et la Deploy key sont correctement configurés, et que NVM est bien installé pour le compte githubci.
Pour vérifier côté serveur que l’application tourne, se connecter en SSH en tant que githubci et lancer :
pm2 list
pm2 logs todolist
Ces commandes permettent d’afficher le processus todolist et les logs de l’application.
Faire redémarrer l’application après un reboot du VPS
Par défaut, les processus gérés par PM2 ne redémarrent pas seuls après un reboot du serveur. L’application sera de nouveau up lors du prochain déploiement (le workflow relance pm2 start) ou si l’application est relancée manuellement.
Pour que l’application redémarre automatiquement au boot du serveur, il est nécessaire de configurer le démarrage système de PM2. En tant que githubci, lancer :
pm2 startup
PM2 affiche une commande à exécuter en root (du type sudo env PATH=... pm2 startup systemd -u githubci --hp /home/githubci). Exécuter cette commande sur le serveur, puis en tant que githubci :
pm2 save
Après cela, à chaque reboot du serveur, les processus enregistrés avec pm2 save redémarreront automatiquement.
Bonus PM2 (utile en produdction)
Rotation des logs
Par défaut, les logs PM2 peuvent grossir indéfiniment. Pour éviter de remplir le disque :
pm2 install pm2-logrotate
Quelques commandes à connaître
pm2 list
pm2 logs todolist
pm2 show todolist
pm2 restart todolist
pm2 reload todolist
Conclusion
Nous avons mis en place un déploiement automatique d’une application node sur un VPS Debian : un utilisateur dédié githubci, NVM pour node, une paire de clés SSH (clé privée dans les secrets GitHub, clé publique en Deploy key), un workflow GitHub Actions qui se connecte en SSH et exécute un script de déploiement (git, npm, pm2), et PM2 pour faire tourner l’application en arrière-plan et la relancer en cas de crash.
À chaque push sur main, la relivraison se fait sans intervention manuelle.
Pour aller plus loin :
- Reverse proxy : voir l’article
Installer transmission(configuration de l’interface web derrière Apache) : Installer transmission - HTTPS / certificats : voir l’article
Guide de survie Let's Encrypt: Guide de survie Let’s Encrypt