~Blog / Jordane Takoukam
DevOpsNiveau Intermédiaire

Déployer une application sur un VPS, de zéro au CI/CD

La procédure complète que je rejoue à chaque nouveau projet : du VPS vierge à l'application en HTTPS, redéployée toute seule à chaque git push. Toutes les commandes sont prêtes à copier, dans l'ordre.

Auteur
Jordane Takoukam
Publié le
Lecture
21 min de lecture
#VPS#SSH#Sécurité#Nginx#Node.js#PM2#CI/CD

Déployer une application sur un VPS, ce n'est pas une seule tâche, mais une dizaine de petites étapes qui doivent s'enchaîner dans le bon ordre. Oubliez-en une et vous vous retrouvez avec un 502 Bad Gateway ou un Permission deniedsans savoir d'où il vient.

Ce guide déroule la procédure de bout en bout : on part d'un serveur fraîchement acheté et on termine sur une application servie en HTTPS, supervisée par PM2, et redéployée automatiquement à chaque git push grâce à GitHub Actions.

Ce dont vous avez besoin

  • Un VPS sous Ubuntu (22.04 ou 24.04), avec son adresse IP et le mot de passe rootfournis par l'hébergeur.
  • Un nom de domaine dont l'enregistrement DNS Apointe vers l'IP du VPS.
  • Un dépôt GitHub contenant le code du projet.
  • Un PC Windows avec OpenSSH (inclus par défaut sur Windows 10/11).
I

Créer un alias SSH pour se connecter facilement au serveur

Objectif : remplacer la saisie de l'IP et du mot de passe par une seule commande mémorisable (ssh mon-vps) et une connexion par clé, bien plus sûre.

1Se placer dans le dossier de configuration SSH

Sous Windows, le dossier .sshse trouve à la racine du profil utilisateur. On s'y rend depuis PowerShell.

PowerShell · Windows
cd C:\Users\Jordane\.ssh

S'il n'existe pas encore, créez-le avec mkdir C:\Users\Jordane\.ssh.

2Générer la paire de clés SSH

On génère une clé RSA de 4096 bits. L'option -C ajoute un commentaire (pratique pour identifier la clé plus tard) et -f fixe le nom du fichier de sortie.

PowerShell · Windows
ssh-keygen -t rsa -b 4096 -C "vps personnel" -f mon-vps

À la question passphrase, vous pouvez valider à vide (Entrée). Deux fichiers apparaissent : mon-vps (clé privée, à garder secrète) et mon-vps.pub (clé publique, à diffuser).

3Déclarer l'alias dans le fichier de configuration SSH

Toujours dans .ssh, on édite (ou on crée) le fichier config, sans extension. Il associe un nom court à une IP, un utilisateur et une clé.

C:\Users\Jordane\.ssh\config
# Mon VPS personnel
Host mon-vps
    HostName 147.x.x.x
    User root
    IdentityFile ~/.ssh/mon-vps

4Afficher la clé publique

On affiche le contenu de la clé publique pour le copier à l'étape suivante.

PowerShell · Windows
type mon-vps.pub

5Installer la clé publique sur le serveur

On se connecte une première fois avec le mot de passedu VPS (celui fourni par l'hébergeur), puis on installe la clé publique.

PowerShell · Windows
ssh mon-vps

Une fois connecté au serveur, on exécute :

VPS · bash
# 1. Créer le dossier .ssh s'il n'existe pas
mkdir -p ~/.ssh

# 2. Ouvrir le fichier authorized_keys
nano ~/.ssh/authorized_keys
# -> coller le contenu de mon-vps.pub
# -> enregistrer avec Ctrl+O puis Entrée, quitter avec Ctrl+X

# 3. Sécuriser les permissions (SSH refuse des droits trop larges)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

6Vérifier la connexion sans mot de passe

On se déconnecte, puis on relance la connexion : elle doit désormais s'ouvrir sans demander de mot de passe.

PowerShell · Windows
exit
ssh mon-vps
II

Durcir la sécurité du serveur

Objectif : un VPS exposé sur Internet est scanné en permanence. On crée un utilisateur non-root, on coupe l'accès par mot de passe et on ferme tous les ports inutiles.

1Créer un utilisateur non-root avec droits sudo

Travailler en rootau quotidien est risqué : la moindre erreur s'exécute avec tous les pouvoirs. On crée un utilisateur dédié, ici deploy.

VPS · bash (en root)
# Créer l'utilisateur (un mot de passe sera demandé)
adduser deploy

# Lui donner les droits d'administration
usermod -aG sudo deploy

# Copier la clé SSH autorisée vers le nouvel utilisateur
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

Mettez à jour l'alias côté Windows pour utiliser ce compte (User deploy au lieu de User root dans .ssh/config), puis testez ssh mon-vps.

2Configurer le pare-feu UFW

UFW ne laisse passer que ce qu'on autorise explicitement : SSH, HTTP et HTTPS. Tout le reste est bloqué.

VPS · bash
# Autoriser SSH, HTTP et HTTPS
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'

# Activer le pare-feu
sudo ufw enable

# Vérifier les règles actives
sudo ufw status verbose

3Désactiver la connexion root et par mot de passe

On édite la configuration du serveur SSH pour n'autoriser que l'authentification par clé.

VPS · bash
sudo nano /etc/ssh/sshd_config

Repérez et ajustez ces trois directives :

/etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Puis on recharge le service SSH pour appliquer :

VPS · bash
sudo systemctl restart ssh

4Installer fail2ban (optionnel mais recommandé)

fail2ban surveille les journaux et bannit automatiquement les adresses IP qui multiplient les tentatives de connexion échouées.

VPS · bash
sudo apt install fail2ban -y

# Vérifier qu'il tourne
sudo systemctl status fail2ban
III

Préparer l'environnement applicatif

Objectif : installer la pile qui fera tourner et servira l'application. Système à jour, Nginx, Certbot, Node.js 20 et PM2.

1Mettre le serveur à jour

VPS · bash
# Mettre à jour la liste des paquets disponibles
sudo apt update

# Installer les mises à jour
sudo apt upgrade -y

# Nettoyer les paquets devenus inutiles
sudo apt autoremove -y
sudo apt autoclean

2Installer Nginx

Nginx jouera le rôle de reverse proxy: il reçoit le trafic web et le transmet à l'application Node.js (section VIII).

VPS · bash
sudo apt install nginx -y

# Vérifier que le service est actif
sudo systemctl status nginx

À ce stade, ouvrir http://147.x.x.xdans un navigateur doit afficher la page d'accueil par défaut de Nginx.

3Installer Certbot (certificats SSL gratuits)

Certbot obtient et renouvelle les certificats Let's Encrypt. Le paquet python3-certbot-nginxajoute l'intégration directe avec Nginx.

VPS · bash
sudo apt install certbot python3-certbot-nginx -y

# Vérifier l'installation
certbot --version

4Installer Node.js 20

Le dépôt officiel NodeSource fournit une version récente et maintenue, contrairement à celle des dépôts Ubuntu.

VPS · bash
# Ajouter le dépôt officiel NodeSource pour Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -

# Installer Node.js (npm est inclus)
sudo apt install -y nodejs

# Vérifier les versions
node -v
npm -v

5Installer et configurer PM2

PM2 est un gestionnaire de processus : il maintient l'application en vie, la relance si elle plante et la redémarre au boot du serveur.

VPS · bash
# Installer PM2 globalement
sudo npm install -g pm2

# Générer le script de démarrage automatique
pm2 startup

# Vérifier la version
pm2 -v
IV

Mettre en place l'architecture des dossiers

Objectif : un emplacement standard et prévisible pour le code, que le pipeline CI/CD retrouvera toujours au même endroit.

La convention sous Linux est de servir les sites depuis /var/www. On y crée un dossier pour le front-end et un pour le back-end.

VPS · bash
# Se placer dans le dossier des sites web
cd /var/www

# Créer les dossiers du projet
sudo mkdir -p frontend backend

# Donner la propriété à l'utilisateur de déploiement
sudo chown -R deploy:deploy /var/www/frontend /var/www/backend

L'arborescence obtenue :

Arborescence cible
/var/www
├── frontend/   # code du site / de l'interface
└── backend/    # code de l'API
V

Lier le VPS et GitHub par une clé SSH dédiée

Objectif : donner à GitHub Actions une clé propre pour se connecter au serveur, distincte de votre clé personnelle et révocable à tout moment.

1Générer une paire de clés sur le VPS

On crée une clé ed25519 (moderne et compacte) sans passphrase, car un pipeline ne peut pas en saisir une.

VPS · bash
ssh-keygen -t ed25519 -f ~/.ssh/github_actions -N ""

Deux fichiers sont créés :

Fichiers générés
~/.ssh/github_actions       # clé privée -> ira dans un secret GitHub
~/.ssh/github_actions.pub   # clé publique -> reste sur le serveur

2Autoriser cette clé sur le VPS

On ajoute la clé publique aux clés acceptées par le serveur, et on verrouille les permissions.

VPS · bash
# Ajouter la clé publique aux clés autorisées
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys

# Verrouiller les permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/github_actions

3Récupérer la clé privée

On affiche la clé privée : son contenu ira dans le secret GitHub SERVER_SSH_KEY à la section suivante.

VPS · bash
cat ~/.ssh/github_actions
VI

Renseigner les secrets et variables GitHub

Objectif : confier au dépôt GitHub les informations dont le pipeline a besoin, en séparant ce qui est sensible (secrets) de ce qui ne l'est pas (variables).

Dans le dépôt GitHub, ouvrez Settings → Secrets and variables → Actions. L'onglet propose deux sections.

Onglet « Secrets » : chiffrés, jamais réaffichés

  • SERVER_IP: l'adresse IP du VPS.
  • SERVER_USER : l'utilisateur SSH, soit deploy.
  • SERVER_SSH_KEY : la clé privée github_actions copiée à la section V.
  • SMTP_PASSWORD: le mot de passe du compte d'envoi d'e-mails de l'application.

Onglet « Variables » : en clair

  • SMTP_USERNAME: l'identifiant du compte SMTP.
  • SMTP_TO: l'adresse de réception des e-mails.
VII

Écrire le pipeline de déploiement continu

Objectif : à chaque git pushsur une branche dédiée, GitHub se connecte au VPS, récupère le code, installe les dépendances et redémarre l'application, sans intervention.

1Créer la branche de déploiement

Le pipeline ne se déclenchera que sur une branche précise. On crée production en local.

PowerShell · projet local
git checkout -b production
git push -u origin production

2Créer le fichier du workflow

GitHub Actions lit les workflows depuis un emplacement précis du dépôt :

Arborescence du dépôt
.github/
└── workflows/
    └── deploy.yml

3Définir le workflow de déploiement

Voici un deploy.yml complet et réutilisable. Il se connecte au serveur en SSH, met le code à jour, installe les dépendances, construit le projet et le (re)lance avec PM2. Adaptez le chemin, le nom du processus et les commandes de build à votre projet.

.github/workflows/deploy.yml
name: Déploiement sur le VPS

# Se déclenche à chaque push sur la branche production
on:
  push:
    branches: [production]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Récupérer le code
        uses: actions/checkout@v4

      - name: Déployer via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /var/www/backend
            git pull origin production
            npm ci
            npm run build
            pm2 reload ecosystem.config.js --update-env
            pm2 save

4Décrire l'application pour PM2

Le pipeline appelle ecosystem.config.js: ce fichier, à la racine du projet, décrit comment lancer l'application. On le versionne dans le dépôt.

ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "backend",
      script: "npm",
      args: "start",
      cwd: "/var/www/backend",
      env: {
        NODE_ENV: "production",
        PORT: 3000,
      },
    },
  ],
};

Au tout premier déploiement, l'application n'est pas encore connue de PM2 : lancez-la une fois à la main sur le serveur, puis figez la liste.

VPS · bash (premier lancement)
cd /var/www/backend
pm2 start ecosystem.config.js
pm2 save

# Suivre les journaux de l'application
pm2 logs backend
VIII

Configurer Nginx en reverse proxy et activer le HTTPS

Objectif : exposer l'application au monde via le nom de domaine, et chiffrer le trafic avec un certificat SSL gratuit.

1Créer le fichier de configuration du domaine

Les configurations de sites vivent dans /etc/nginx/sites-available. On crée un fichier portant le nom du domaine.

VPS · bash
sudo nano /etc/nginx/sites-available/api.mon-domaine.com

Cette configuration reçoit le trafic HTTP et le transmet à l'application Node.js, qui écoute en local sur le port 3000.

/etc/nginx/sites-available/api.mon-domaine.com
server {
    listen 80;
    server_name api.mon-domaine.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
    }
}

2Activer le site

Nginx ne sert que les configurations présentes dans sites-enabled. On y crée un lien symbolique vers notre fichier, on vérifie la syntaxe, puis on recharge.

VPS · bash
# Activer le site
sudo ln -s /etc/nginx/sites-available/api.mon-domaine.com \
  /etc/nginx/sites-enabled/

# Vérifier que la configuration est valide
sudo nginx -t

# Recharger Nginx pour appliquer
sudo systemctl reload nginx

3Générer le certificat SSL avec Certbot

Certbot obtient le certificat pour le domaine et modifie automatiquement la configuration Nginx pour activer le HTTPS et rediriger le HTTP vers le HTTPS.

VPS · bash
# Obtenir et installer le certificat
sudo certbot --nginx -d api.mon-domaine.com

# Vérifier que le renouvellement automatique fonctionne
sudo certbot renew --dry-run
IX

Dépanner les erreurs les plus courantes

Trois erreurs reviennent presque à chaque déploiement. Voici comment les diagnostiquer rapidement.

502 Bad Gateway

Nginx fonctionne mais ne joint pas l'application : elle est arrêtée, a planté, ou n'écoute pas sur le port attendu. On vérifie l'état du processus et ses journaux.

VPS · bash
# L'application tourne-t-elle ?
pm2 status

# Que disent ses journaux ?
pm2 logs backend --lines 50

Permission denied (publickey)

Le serveur refuse la clé. Causes fréquentes : permissions trop larges sur ~/.ssh, mauvais utilisateur dans l'alias, ou clé absente de authorized_keys. On relance la connexion en mode bavard pour voir où elle bloque.

PowerShell · Windows
ssh -v mon-vps

EADDRINUSE : port déjà utilisé

Une ancienne instance occupe encore le port 3000. On identifie le processus fautif, puis on demande à PM2 de repartir proprement.

VPS · bash
# Quel processus occupe le port 3000 ?
sudo lsof -i :3000

# Repartir d'une base saine
pm2 delete backend
pm2 start ecosystem.config.js
pm2 save

Checklist finale

Une fois toutes les sections suivies, voici ce qui doit être vrai. Si une case n'est pas cochée, reprenez la section correspondante.

  • ssh mon-vps connecte au serveur sans mot de passe, sur le compte deploy.
  • La connexion root et par mot de passe est désactivée ; UFW est actif.
  • Node.js 20, Nginx, Certbot et PM2 sont installés et fonctionnels.
  • Le code vit dans /var/www et appartient à deploy.
  • Les secrets et variables sont renseignés dans GitHub ; deploy.yml et ecosystem.config.js sont versionnés.
  • Un git push origin production déclenche le déploiement automatiquement.
  • Le domaine répond en https:// avec un certificat valide.
Retour au blogHaut de page