Mettez en production votre application Node.js sur un VPS
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 symboliquereleases
: Les dernières versions de notre applicationlogs
: Les logs persistantsshared
: 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.