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 permettra d’installer Node.js spécifiquement pour l’utilisateur githubci.
La version est figée dans un fichier .nvmrc à la racine du projet (par exemple 22.14.0). À chaque déploiement, le script exécuté par la pipeline GitHub charge NVM, se place dans le dépôt, puis lance nvm install et nvm use. NVM installe alors exactement cette version. Le chemin du binaire reste stable, ce qui évite de casser le redémarrage PM2 au boot. PM2 est ensuite installé 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 à la main. Le script de déploiement s’en chargera au premier déploiement, à partir du fichier .nvmrc.
Configuration des clés 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és 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 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, .nvmrc et PM2
Nous allons ajouter trois fichiers au dépôt : le workflow qui décrit ce que fait la GitHub Action à chaque push sur main, le fichier .nvmrc qui fige la version de Node.js, et le fichier de configuration PM2 qui décrit comment lancer l’application sur le serveur.
Créer le fichier .nvmrc
À la racine du projet (à côté de package.json), créer le fichier .nvmrc. Il contient uniquement la version exacte de Node.js à installer sur le serveur. Le package-lock.json doit aussi être versionné : le script de déploiement installe les dépendances avec npm ci.
22.14.0
Remplacer 22.14.0 par la version souhaitée. Une version exacte évite qu’un déploiement installe un nouveau correctif et change le chemin du binaire utilisé par PM2 au démarrage du 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: Deploy to server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.SSH_HOST }}
username: githubci
key: ${{ secrets.SSH_PRIVATE_KEY }}
script_stop: true
script: |
echo "Starting deployment..."
# nvm.sh lit des variables non définies : pas de set -u ici
export NVM_DIR="$HOME/.nvm"
set +e
. "$NVM_DIR/nvm.sh"
set -eo pipefail
# Répertoires de code et de données
mkdir -p "$HOME/www" "$HOME/.todolist"
# Clé d'hôte de GitHub, sinon le premier git clone échoue sans TTY
mkdir -p "$HOME/.ssh"
touch "$HOME/.ssh/known_hosts"
grep -q "github.com" "$HOME/.ssh/known_hosts" || ssh-keyscan -t ed25519 github.com >> "$HOME/.ssh/known_hosts"
DEPLOY_DIR="$HOME/www/todolist"
REPO_URL="git@github.com:${{ github.repository }}.git"
echo "Deploy directory: $DEPLOY_DIR"
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
# Version de Node.js figée dans .nvmrc
nvm install
nvm use
echo "Node version: $(node -v)"
echo "NPM version: $(npm -v)"
echo "Installing dependencies..."
npm ci --omit=dev
echo "Managing app with PM2..."
npm install -g pm2
pm2 stop todolist || true
pm2 delete todolist || true
pm2 start ecosystem.config.js
pm2 save
pm2 show todolist
echo "Deployment completed!"
Ce workflow se déclenche à chaque push sur la branche main. Il 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). github.repository vient du contexte GitHub Actions : le job n’a pas besoin de checkout, le script s’exécute entièrement sur le serveur.
Le bloc script est exécuté sur le serveur. set -eo pipefail est activé après le chargement de NVM (nvm.sh lit des variables non définies, donc pas de set -u), et script_stop: true arrête aussi l’action à la première commande en erreur, pour qu’un simple echo final ne rende pas le workflow vert. Le script crée ~/www et ~/.todolist, enregistre la clé d’hôte de GitHub, clone ou met à jour le dépôt dans ~/www/todolist, installe la version de Node.js du fichier .nvmrc, installe les dépendances avec npm ci --omit=dev, puis relance l’application avec PM2 à partir de 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,
DATA_DIR: '/home/githubci/.todolist'
},
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). DATA_DIR pointe vers /home/githubci/.todolist/ : c’est là que l’application lit ses données hors dépôt (SQLite, etc.). Un fichier .env déposé dans ce dossier n’est pas chargé par PM2 ; l’application doit le lire elle-même, ou les variables sont déclarées dans env. 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 et .nvmrc : les trois doivent être versionnés dans le dépôt.
Premier déploiement
Une fois le workflow, le fichier .nvmrc et 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 .nvmrc
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’exécution du workflow « Deploy todolist » doit apparaître en cours, puis en succès lorsque l’exécution 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.
pm2 startup fige le PATH de Node.js tel qu’il est au moment de la commande. Après un changement de version dans .nvmrc, relancer pm2 startup, exécuter à nouveau la commande root affichée, puis pm2 save.
Bonus PM2 (utile en production)
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 livraison 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