Mettez en production votre application Node.js sur un VPS

14mn de lecture

Les gens supposent souvent que tout développeur doit savoir comment déployer une application sur un serveur distant. En réalité, beaucoup ne sont pas à l’aise avec la mise en production d’un site web.

Dans cet article, nous allons déployer une application AdonisJS sur un serveur de type VPS tournant sur Ubuntu 24.04. Il faut savoir que la démarche sera identique, peu importe le framework que vous utilisez.

Je ne recommande pas de déployer une application de cette manière. Aujourd’hui, un déploiement via Docker est largement préférable.

Cependant, il est toujours utile de comprendre d’où l’on vient pour mieux appréhender les technologies actuelles.

Le déploiement via Docker fera l’objet d’un futur article !

Créer un serveur

En premier lieu, il nous faut un serveur. Pour cela, je vais utiliser Hetzner, un fournisseur de serveurs qui propose de bonnes machines à un prix intéressant.

Si vous utilisez mon lien d'affiliation, vous recevrez un crédit de 20 $, idéal pour tester le déploiement de votre application sans frais.

Avant de lancer la création de votre serveur, vous devez ajouter une clé SSH à votre compte Hetzner. Cette clé vous permettra d’accéder à votre serveur, comme une clé physique vous permet d’entrer chez vous.

Lorsque vous vous connectez à un serveur via SSH, vous devez prouver votre identité. Plutôt que d’utiliser un mot de passe, nous allons utiliser une paire de clés SSH, un peu comme un système de serrure et de clés.

La clé privée est comme votre clé personnelle : elle doit rester sur votre machine et ne jamais être partagée. La clé publique, elle, est comme la serrure d’une porte : vous pouvez l’envoyer à n’importe quel serveur sur lequel vous souhaitez vous connecter.

Le serveur ne vous demandera pas de mot de passe, il vérifiera simplement si votre clé privée correspond à la clé publique enregistrée dans son système. Si elles correspondent, il vous laissera entrer, exactement comme si vous aviez inséré la bonne clé dans une serrure.

Créer une clé SSH

Pour générer une clé SSH, vous devez avoir OpenSSH installé sur votre machine, ce qui est normalement le cas sous Linux et macOS. Pour les machines sous Windows, vous pouvez passer par WSL ou utiliser un logiciel comme PuTTY.

L'utilitaire ssh-keygen sera utilisé pour générer une clé spécifique. Vous pouvez bien sûr sauter cette étape si vous avez déjà une clé existante.

ssh-keygen -t ed25519 -C "romain.lanz@slyos"

La commande prend comme argument -t, qui permet de spécifier le type de clé à générer. À une époque, les clés rsa étaient courantes, mais il est désormais conseillé d’utiliser l’algorithme plus robuste ed25519.

Le second paramètre permet d’ajouter un commentaire. Il s’agit d’un champ de texte libre qui vous aidera à repérer facilement votre clé parmi d’autres, si vous en possédez plusieurs.

Une fois cette commande exécutée, vous obtiendrez une nouvelle paire de clés SSH : une clé publique et une clé privée.

La clé privée ne doit jamais quitter votre machine. C’est elle qui vous permet d’ouvrir la porte. La clé publique, en revanche, est destinée à être partagée. Elle fonctionne comme la serrure de la porte que vous souhaitez ouvrir.

Imaginez que votre serveur est une grande porte sur laquelle vous pouvez ajouter autant de serrures que nécessaire.

Ajouter votre clé SSH publique sur Hetzner

Maintenant que vous avez créé votre clé publique et privée, vous devez l'ajouter à votre serveur, sans quoi vous ne pourrez pas vous y connecter.

Dans Hetzner, une fois un projet créé, vous pourrez accéder à l'onglet "Security". Depuis cet espace, copiez votre clé publique et cliquez sur "Add SSH Key".

Bien entendu, selon le fournisseur que vous utilisez, la démarche peut légèrement différer, mais reste globalement la même.

Achat du serveur

Il ne nous reste plus qu'à commander un serveur. Pour cela, allez dans "Servers" et cliquez sur "Add Server". À ce moment-là, vous devrez répondre à une série de questions pour configurer votre serveur.

Vous pouvez choisir sa localisation. Pour ma part, je prendrai "Falkenstein". Il vous faudra également sélectionner son système d’exploitation (Ubuntu 24.04) ainsi que sa puissance.

Pour ce tutoriel, nous n’allons pas choisir une machine trop puissante. J’opte pour le CX22, qui coûte 3,29 € par mois pour 2 vCPU et 4 Go de RAM. C’est largement suffisant pour notre besoin.

Les autres options ne sont pas nécessaires pour le moment. Il suffit simplement de sélectionner la clé SSH ajoutée précédemment afin de pouvoir se connecter au serveur une fois celui-ci créé.

Sécuriser le serveur

Par défaut, le serveur n’est pas accessible à tous, mais nous pouvons tout de même effectuer quelques actions pour le sécuriser davantage.

Mettre à jour le serveur

Tout d’abord, nous allons le mettre à jour. Cela peut paraître étrange, puisque nous venons de l’installer, mais il faut comprendre que les hébergeurs utilisent des images système qu’ils déploient lors de l’achat du serveur. Celles-ci ne sont pas forcément à jour.

Connectons-nous à notre serveur avec la commande suivante :

ssh root@<ip de votre serveur>

Cette commande devrait ouvrir directement un shell (terminal) sur votre serveur et vous connecter en tant qu’utilisateur root. Si vous rencontrez une erreur, assurez-vous que votre clé SSH privée est bien enregistrée.

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519 # Ça doit être le chemin vers votre clé privée

Une fois connecté, vous allez pouvoir lancer la commande :

apt update

Cette commande mettra à jour le registre des paquets afin d’obtenir la liste des dernières versions disponibles. Une fois cela fait, vous pouvez poursuivre avec la commande suivante :

apt upgrade

Cette commande mettra à jour les paquets déjà installés sur votre serveur.

Protéger son serveur avec fail2ban

fail2ban est un service qui analyse les journaux de votre serveur et bloque les adresses IP présentant une activité suspecte, comme un nombre élevé de tentatives de connexion échouées.

apt install fail2ban

Vous pouvez ensuite configurer fail2ban en modifiant le fichier /etc/fail2ban/jail.local.

[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 3
bantime = 3600

Enfin, redémarrez le service pour appliquer la configuration.

systemctl restart fail2ban

Configurer un pare-feu avec UFW

Un pare-feu permet de bloquer certaines actions réseau sur votre machine. Il en existe sous forme de logiciels ou de dispositifs physiques. Il est notamment utilisé pour restreindre l’accès à certains ports en fonction de l’adresse IP, ou pour bannir des adresses suspectes.

Sur Hetzner, il est possible de configurer un pare-feu externe, ce que je recommande. Dans ce tutoriel, nous supposerons que notre hébergeur ne propose pas cette fonctionnalité, et nous définirons donc les règles de pare-feu directement sur notre serveur.

Pour cela, nous utiliserons ufw, une interface simple permettant de gérer les règles de pare-feu.

ufw default allow outgoing
ufw default deny incoming
ufw allow ssh/tcp
ufw allow http/tcp
ufw allow https/tcp

Ces règles sont assez simples. Tout d’abord, nous autorisons notre serveur à communiquer vers l’extérieur. Ensuite, nous bloquons toute connexion entrante par défaut. Enfin, nous définissons des règles spécifiques pour autoriser le SSH (port 22), le HTTP (port 80) et le HTTPS (port 443).

Selon les services que vous souhaitez installer sur votre serveur, pensez à ouvrir les ports nécessaires pour assurer leur bon fonctionnement.

Une fois cela fait, activez le pare-feu avec la commande suivante :

ufw enable

Créer un utilisateur non-root pour le déploiement

Actuellement, nous sommes connectés en root sur la machine. Cet utilisateur est l’administrateur et possède un accès total au système.

Comme nous allons déployer notre application directement sur la machine, il est important d’avoir un second utilisateur avec des privilèges limités. En cas de faille de sécurité dans l’application, un attaquant aurait ainsi des droits restreints sur le système.

Il est généralement recommandé d’avoir un utilisateur secondaire sur une machine, bien que cela soit moins nécessaire dans le cadre d’un déploiement via Docker.

Créons un nouvel utilisateur avec la commande suivante :

useradd -m -s /bin/bash -G sudo deploy

Attribuons-lui un mot de passe avec la commande suivante :

passwd deploy

Ajoutons ensuite notre clé SSH publique afin de pouvoir nous y connecter.

mkdir /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
vim /home/deploy/.ssh/authorized_keys # On ajoute notre clé SSH ici
chmod 400 /home/deploy/.ssh/authorized_keys
chown deploy:deploy /home/deploy -R

Après ces commandes, nous devrions pouvoir nous connecter avec notre nouvel utilisateur.

Dans un second terminal, lancez la commande suivante :

ssh deploy@<ip de votre serveur>

Si la connexion fonctionne, vérifions également que le mot de passe est bien pris en compte avec la commande suivante :

sudo whoami

La commande whoami permet d’afficher l’utilisateur exécutant la commande. Dans notre cas, nous avons ajouté sudo devant, ce qui signifie que la commande est lancée en tant que root. Nous devrions donc voir root affiché, bien que nous soyons connectés en tant que deploy.

Renforcer la sécurité SSH

Votre serveur possède une configuration de base qui définit les paramètres de connexion SSH. Selon votre système d’exploitation, cette configuration peut varier et inclure des options qui ne sont pas sécurisées.

Celle-ci se trouve dans le fichier /etc/ssh/sshd_config.

sudo vim /etc/ssh/sshd_config

Tout d’abord, nous allons vérifier que seules les connexions via clés SSH sont autorisées.

La ligne à vérifier dans cette configuration est PasswordAuthentication. Sa valeur doit être no.

Nous allons également interdire la connexion directe à root. Pour cela, recherchez la ligne PermitRootLogin, qui doit aussi être définie sur no.

PasswordAuthentication no
PermitRootLogin no

Enfin, redémarrez le service SSH pour appliquer la nouvelle configuration.

sudo systemctl restart ssh

Installer les dépendences

Pour exécuter notre application, nous avons besoin de plusieurs logiciels

Pour la suite de cet article, nous supposerons que vous êtes connecté en tant qu’utilisateur deploy.

Configurer un reverse proxy avec Nginx

Pour accéder à notre application via HTTP et HTTPS, celle-ci doit écouter sur les ports 80 et 443 mais un seul processus peut écouter sur un même port.

Cela pose un problème si vous souhaitez héberger plusieurs applications sur le même serveur, car elles ne pourront pas toutes écouter directement sur le port 80.

C’est précisément le rôle d’un reverse proxy : il reçoit toutes les requêtes HTTP et les redirige vers l’application appropriée en fonction du domaine ou du chemin d’accès.

Il existe plusieurs reverse proxies, comme Apache, Nginx, Caddy ou encore Traefik, largement utilisé avec Docker. Dans cet article, nous nous concentrerons sur Nginx, l’un des reverse proxies les plus utilisés dans le monde professionnel.

Commençons donc par installer Nginx.

sudo apt install nginx

Et voilà, c’est terminé. Normalement, si vous accédez à l’IP de votre serveur depuis votre navigateur, vous devriez voir la page "Welcome to Nginx".

Installer et configurer PostgreSQL

J’utilise principalement PostgreSQL comme gestionnaire de base de données, c’est donc celui que nous traiterons dans cet article. Si vous préférez MariaDB, MySQL ou un autre SGBD, vous pouvez bien entendu les utiliser.

L’installation de PostgreSQL est simple. Il suffit d’exécuter la commande suivante :

sudo apt install postgresql

Une fois installé, PostgreSQL crée un utilisateur système du même nom. Cet utilisateur sera utilisé pour se connecter à la base de données par défaut.

Nous pouvons donc nous y connecter en utilisant cet utilisateur.

sudo -i -u postgres psql

Après cette commande, vous devriez vous retrouver dans l’interface en ligne de commande de PostgreSQL, où vous pourrez exécuter des requêtes SQL.

Créons un utilisateur dédié à notre application :

CREATE USER app WITH CREATEDB ENCRYPTED PASSWORD '<votre mot de passe>';

Il ne reste plus qu’à modifier la configuration pour autoriser la connexion avec un mot de passe en éditant le fichier /etc/postgresql/XX/main/pg_hba.conf.

Il faut remplacer la ligne :

# "local" is for Unix domain socket connections only
local   all             all                                     peer

Par :

# "local" is for Unix domain socket connections only
local   all             all                                     md5

Et voilà. Vous devriez maintenant pouvoir vous connecter à PostgreSQL en tant qu’utilisateur app depuis votre compte deploy.

psql -U app -d postgres

Installer Node.js avec Volta

Node.js est l’environnement d’exécution qui permet d’exécuter notre serveur JavaScript. Pour l’installer, nous allons utiliser l’outil Volta.

curl https://get.volta.sh | bash

Pour utiliser la commande volta, vous devrez vous reconnecter à votre session SSH.

Maintenant, nous pouvons installer la dernière version LTS de Node.js avec la commande :

volta install node@lts

L’un des principaux avantages de Volta est qu’il permet de définir la version de Node.js par projet avec volta pin node@lts. Si vous le faites, votre serveur téléchargera automatiquement la bonne version.

Gérer l’application avec PM2

Lorsque nous lançons notre application Node.js, vous avez sûrement remarqué que le terminal reste actif. Il est donc impossible de simplement exécuter notre application dans un terminal, car une fois la connexion SSH coupée, elle s’arrêtera.

Pour résoudre ce problème simplement, nous allons utiliser pm2, un gestionnaire de processus pour Node.js.

npm install pm2@latest -g

Déployer une application Node.js sur le serveur

Nous disposons maintenant de tous les outils nécessaires pour déployer notre application.

Pour cela, nous allons utiliser git pour récupérer le code source de notre application.

Configurer une clé de déploiement GitHub

GitHub permet d’ajouter une clé de déploiement à un dépôt. Cette clé autorise le clonage du projet sans donner la possibilité de pousser des modifications.

Nous allons générer une nouvelle clé SSH, mais cette fois-ci depuis le serveur.

ssh-keygen -t ed25519 -C "deploy key"

Une fois la clé générée, ajoutez-la à votre dépôt GitHub. Dans les paramètres de votre projet, un menu "Deploy keys" vous permet d’ajouter votre clé SSH fraîchement créée.

Organiser les fichiers de l’application

Nous allons utiliser le répertoire /srv/www sur notre serveur pour y stocker les fichiers relatifs à notre application.

Ce répertoire contiendra différentes applications, organisées en sous-dossiers :

  • current : La version active de notre application avec un lien symbolique
  • releases : Les dernières versions de notre application
  • logs : Les logs persistants
  • shared : Les fichiers persistants

Créons le répertoire principal, le reste sera généré par un script de déploiement.

sudo mkdir -p /srv/www
sudo chown deploy:deploy /srv/www
sudo chmod 750 /srv/www

Automatiser le déploiement avec un script Bash

Pour simplifier le déploiement de l’application et éviter toute erreur, il est préférable de l’automatiser avec un script Bash.

Commençons par créer le dossier de notre application dans /srv/www.

mkdir -p /srv/www/mon-app

Nous allons maintenant utiliser le script suivant pour déployer notre application. Copiez-le dans le fichier /srv/www/mon-app/deploy.sh

vim /srv/www/mon-app/deploy.sh
#!/bin/bash

# Variables
APP_NAME="mon-app"                          # Nom du processus PM2
REPO="[email protected]:mon-org/mon-repo.git"  # Repo Git
DEPLOY_DIR="/srv/www/$APP_NAME/current"     # Lien symbolique vers la version active
RELEASES_DIR="/srv/www/$APP_NAME/releases"  # Dossier contenant toutes les versions
NEW_RELEASE="$RELEASES_DIR/$(date +%Y%m%d%H%M%S)"  # Dossier pour la nouvelle version
SHARED_DIR="/srv/www/$APP_NAME/shared"      # Dossier pour fichiers persistants
LOG_DIR="/srv/www/$APP_NAME/logs"           # Logs persistants
ENV_FILE="$SHARED_DIR/.env"                 # Emplacement du fichier .env
USER="deploy"                               # Utilisateur qui exécute l'application

# Étape 1 : Création des dossiers si nécessaire
echo "📂 Vérification des dossiers nécessaires..."
mkdir -p $RELEASES_DIR $SHARED_DIR $LOG_DIR

# Vérifier si "current" existe, sinon pointer vers un dossier vide temporaire
if [ ! -L "$DEPLOY_DIR" ]; then
    echo "🆕 Création du lien symbolique 'current' initial..."
    mkdir -p "$SHARED_DIR/empty"
    ln -sfn "$SHARED_DIR/empty" "$DEPLOY_DIR"
fi

# Étape 2 : Cloner et préparer la nouvelle version
echo "🚀 Clonage dans $NEW_RELEASE..."
git clone $REPO $NEW_RELEASE
cd $NEW_RELEASE

echo "📦 Installation des dépendances..."
npm ci  # Installation complète pour permettre le build

echo "🔨 Build de l'application..."
node ace build

echo "🗑 Suppression des fichiers inutiles sauf build/..."
# Supprime tous les fichiers et dossiers sauf "build"
find . -mindepth 1 -maxdepth 1 ! -name "build" -exec rm -rf {} +

echo "📦 Réinstallation des dépendances en mode production..."
cd build
npm ci --omit=dev

# Étape 3 : Copier le fichier .env (si existant)
if [ -f "$ENV_FILE" ]; then
    echo "🔗 Copie du fichier .env..."
    cp $ENV_FILE .env
fi

# Étape 4 : Lancer les migrations
echo "🔄 Exécution des migrations..."
node ace migration:run --force

# Étape 5 : Mettre à jour le lien symbolique "current"
echo "🔄 Bascule vers la nouvelle version..."
ln -sfn $NEW_RELEASE/build $DEPLOY_DIR  # Le dossier current pointe directement vers build

# Étape 6 : Vérifier si l'application existe déjà dans PM2
if pm2 list | grep -q "$APP_NAME"; then
    echo "♻️ Restart PM2 sans downtime..."
    pm2 reload $APP_NAME --update-env
else
    echo "🚀 Lancement initial de l'application avec PM2..."
    pm2 start $DEPLOY_DIR/bin/server.js --name $APP_NAME --log $LOG_DIR/output.log --error $LOG_DIR/error.log --update-env
    pm2 save  # Sauvegarde l'état pour que PM2 relance l'app au reboot
fi

# Étape 7 : Nettoyage des anciennes versions (optionnel, garder les 3 dernières)
echo "🧹 Nettoyage des anciennes versions..."
cd $RELEASES_DIR
ls -dt * | tail -n +4 | xargs rm -rf

echo "✅ Déploiement terminé avec succès !"

Adaptez-le évidemment à vos besoins, notamment en ajustant le nom de votre application et le dépôt que vous souhaitez récupérer.

Ce script va créer la structure de dossiers présentée plus haut, cloner votre application, installer les dépendances et compiler votre application en production. Il supprimera ensuite tout ce qui n’est pas nécessaire en production.

Une fois prête, la liaison symbolique sera mise à jour et pm2 relancera notre application.

Il ne reste plus qu’à lui attribuer les droits d’exécution.

chmod +x /srv/www/mon-app/deploy.sh

Vous pouvez exécuter le script ; vous devriez voir différentes actions s’exécuter. Une fois terminé, vérifiez si votre application a bien démarré avec la commande suivante :

pm2 list

Vous pouvez également vérifier l’affichage de votre page avec la commande suivante :

curl localhost:3333

Servir l’application avec Nginx

Nous avons maintenant notre application qui tourne en local et notre proxy qui écoute sur le port 80 depuis l’extérieur.

Nous devons maintenant relier les deux en configurant Nginx.

Tout d’abord, nous allons supprimer la configuration affichant le site par défaut lorsque vous accédez à l’IP de votre serveur depuis votre navigateur.

sudo rm /etc/nginx/sites-available/default
sudo rm /etc/nginx/sites-enabled/default

Avant de créer notre fichier de configuration, nous devons disposer d’un nom de domaine pointant sur notre serveur.

Pour ce tutoriel, nous allons utiliser le service sslip.io, qui fournit un domaine dynamique sans nécessiter d’ajout d’enregistrement DNS. Idéal pour tester !

Votre nom de domaine ressemblera à ceci http://<votre app>-<votre ip>.sslip.io. Par exemple http://mon-app-138.201.42.42.sslip.io.

Ensuite, nous allons créer notre fichier de configuration :

sudo vim /etc/nginx/sites-available/mon-app
server {
  listen 80 default_server;
  listen [::]:80 default_server;

  server_name mon-app-138.201.42.42.sslip.io;

  # Our Node.js application
  location / {
    proxy_pass http://localhost:3333;
    proxy_http_version 1.1;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

Vous l’avez peut-être remarqué, mais Nginx utilise deux dossiers : sites-available et sites-enabled. Vous pouvez définir autant de sites que vous le souhaitez dans sites-available, mais ils ne seront actifs que s’ils sont liés dans sites-enabled. Pour les activer, nous devons créer un lien symbolique vers notre fichier de configuration dans ce dossier.

sudo ln -s /etc/nginx/sites-available/mon-app /etc/nginx/sites-enabled/mon-app

Enfin, redémarrez Nginx pour appliquer la nouvelle configuration.

sudo service nginx restart

Sécuriser l’application avec un certificat SSL

La dernière étape consiste à générer un certificat SSL pour sécuriser notre site et le rendre accessible via https.

Grâce à Let's Encrypt, nous pouvons obtenir gratuitement des certificats SSL pour n’importe quel domaine.

Nous allons utiliser certbot pour générer ce certificat.

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Une fois installé, nous devons exécuter l’utilitaire avec l’option --nginx.

sudo certbot --nginx

L’outil vous demandera d’entrer une adresse e-mail afin de vous avertir en cas d’expiration du certificat. Il vous demandera ensuite d’accepter les conditions d’utilisation et vous proposera de partager votre adresse avec l’EFF.

Ensuite, une liste des sites configurés dans Nginx s’affichera. Sélectionnez l’application à laquelle vous souhaitez ajouter un certificat SSL.

Conclusion

Nous avons maintenant un serveur configuré avec un pare-feu, une base de données PostgreSQL et une application AdonisJS qui tourne derrière un reverse proxy Nginx avec un certificat SSL. Cette méthode de déploiement vous permet d’héberger votre application sur un VPS, mais elle peut rapidement devenir complexe à gérer à grande échelle.

Dans un prochain article, nous verrons comment simplifier le déploiement avec Docker, une solution aujourd’hui largement adoptée pour gérer efficacement les applications en production.