Configurer Node.js pour le serveur
Node est génial. Quand on commence, on fait joujou avec et on lance toutes nos commandes en CLI à base de node server.js
ou node index.js
sans se poser plus de questions. Puis vient le jour où on veut mettre en ligne notre belle application codée avec amour, et là, on ne sait pas comment faire tourner Node comme un vrai serveur, indépendamment du shell. Voyons ça de plus près.
Par défaut, Node ne tourne pas en tant que dæmon. Cependant pas d’inquiétude, nous allons mettre tout cela en place, ce n’est pas très compliqué. D’ailleurs, comme souvent avec Node, ce que nous faisons manuellement est l’occasion d’en apprendre un peu plus sur le fonctionnement sous-jacent des technos que nous utilisons.
Node en tant que daemon
Encore récemment, j’utilisais Node en direct avec systemD. Il fallait donc utiliser le module cluster
de Node afin de tirer parti du multi-cœur ainsi que les domain
pour gérer les erreurs. Cependant, ce dernier module est déprécié. Nous allons donc utiliser PM2 afin de gérer tout cela pour nous.
systemD va s’assurer de lancer PM2 qui lancera notre ou nos applications et les relancera en cas d’échec. PM2 gère aussi les logs et fournit des statistiques d’usage (cpu, ram, uptime…).
Installation de pm2
PM2 est un module nodejs, on commence donc par l’installer globalement avec un petit coup de npm install -g pm2
. Jusque là, tout va bien. PM2 a besoin d’une home pour sauvegarder sa configuration, son état etc. Comme j’ai pour habitude de placer mes sites dans /var/www
, c’est tout naturellement là que PM2 prendra racine.
cd /var/www
mkdir .pm2
chown www-data:www-data .pm2
Le propriétaire des services web est en général www-data
ou www
, ça dépend des ditributions, il est donc logique que PM2 tourne aussi sous cet utilisateur. On ne lui fera pas lancer nos services Node en root !
Il est maintenant temps d’installer le service systemD qui aura la lourde tâche de lancer PM2 et de s’assurer qu’il tourne toujours. Nous sommes chanceux car PM2 possède une commande qui automatise l’installation du service systemD.
pm2 startup -u www-data --hp /var/www
Nous avons donc généré un script pour systemD, PM2 sera lancé automatiquement au boot de la machine. On a précisé l’utilisateur avec lequel lancer PM2 ainsi que sa home. Cette commande a généré le fichier /etc/systemd/system/pm2-www-data.service
.
Nous pouvons maintenant utiliser la commande service
pour stoper et relancer PM2. Nous ferons également souvent appel à la commande pm2
directement. Afin qu’elle tourne avec le bon utilisateur sans avoir à le spécifier à chaque fois, nous pouvons installer un alias dans notre .bashrc
. Si vous vous connectez à votre serveur en root, ce sera /root/.bashrc
, sinon ce sera /home/username/.bashrc
. On ajoute donc la ligne suivante en bas du fichier :
alias pm2='sudo -su www-data PM2_HOME=/var/www/.pm2 pm2'
Maintenant, lorsque nous ferons appel à la commande pm2
, elle sera toujours exécutée en tant que www-data
.
Utilisation de PM2
Tout est en place, il ne reste plus qu’à configurer notre service pour qu’il soit lancé et maintenu en ligne avec PM2. Cela est très simple avec la ligne de commande.
# lance une instance de notre app
pm2 start app.js
# arrête l'instance mais conserve l'app dans la liste des process managés
pm2 stop app.js
# affiche ladite liste
pm2 ls
# affiche les logs
pm2 logs
# affiche les stats d'utilisation
pm2 monit
# redémarre l'app
pm2 restart app.js
# arrête l'app et l'efface de la liste
pm2 delete app.js
# lance l'app en mode cluster avec -i instances
pm2 start app.is -i 8
# mode cluster avec une instance par thread cpu
pm2 start -i max
Ce sont les principales commandes à retenir. Seulement, pour automatiser la chose, rien ne vaut un fichier de configuration que PM2 pourra lire et interpréter sans que vous soyiez derrière votre clavier.
Vous pouvez générer un fichier de config avec la commande pm2 init
. Vous pouvez spécifier plusieurs services dans le fichier, ainsi un pm2 start
démarrera l’ensemble. Vous avez par exemple l’api et un service qui écoute sur les websockets.
{
"apps" : [{
"name" : "api",
"script" : "./server.js",
"instances" : "max",
"exec_mode" : "cluster",
"env" : {
"NODE_ENV": "production"
}
},
{
"name" : "socket",
"script" : "./socket.js",
"env" : {
"NODE_ENV": "production"
}
}]
}
Il s’agit ici des options les plus basiques, un service tourne en mode cluster tandis que le second n’a qu’une seule instance car sa charge est bien moindre. Pour un aperçu de toutes les possibilités, le mieux est de vous référer directement à la doc.
Lorsque vous avez lancé vos process avec pm2 start
, il peut s’avérer fort utile d’exécuter un petit pm2 save
. Cela permettra à pm2 de relancer exactement la même chose lors du redémarrage du serveur.
Node sur le port 80
Si vous tentez de démarrer node sur le port 80, vous aurez sans doute une erreur EACCES
si vous n’êtes pas root. Sous Linux, seul les ports supérieurs à 1024 sont accessibles aux utilisateurs autres que root. Cependant, comme expliqué précédemment, c’est une très mauvaise idée de démarrer son serveur en tant que root. En effet, dans le cas où un attaquant arriverait à gagner l’accès au serveur web, si ce dernier est root, c’est l’accès à l’ensemble de votre machine que vous lui offrez !
Il existe plusieurs manières de permettre à Node de binder sur le port 80 et/ou 443. Cependant, Node n’est pas vraiment dans son domaine d’excellence pour servir des fichiers statiques, ni gérer les connexions TLS.
La meilleure option est bien entendu de la placer derrière en reverse proxy. Ce dernier gèrera la terminaison TLS [en] et servira directement les fichiers statiques. Node n’aura qu’à écouter sur un port non privilégié en ne se souciant ni de chiffrer les requêtes, ni du CORS, ni des fichiers statiques.
Deux solutions sont principalement utilisées pour cette tâche, vous connaissez certainement déjà leur nom : Apache2 et Nginx. C’est pas ce dernier que je vais commencer mais je vais vous montrer comment configurer les deux.
La configuration peut légèrement varier selon ce que vous faites avec node. Très souvent, nous avons un frontend d’un côté avec des fichiers statiques et une api (nodejs) de l’autre. Le plus simple et efficace est à mon avis de séparer ces deux éléments en sous-domaines différents.
Prenons en exemple mon service de conversion d’archives email : pstconverter. Le front est très simple, tous les assets sont statiques et le JavaScript s’occupe du reste. Ce sont ces éléments que voit l’utilisateur, c’est donc le domaine principal pstconverter.net
.
L’api en revanche, est gérée par Node, elle est joignable sur api.pstconverter.net
et c’est le JS du front qui interragit avec elle. Ce découpage permet une configuration très simple :
- Si la requête arrive sur le domaine principal, on sert des fichiers statiques,
- Si la requête arrive sur le sous-domaine api, on transmet le tout à node.
Dans les deux cas, nous partirons du principe que nous utilisons du HTTPS et que vous savez vous servir du serveur car nous ne nous attarderons pas sur leur installation, la création et l’activation de vhosts etc. D’ailleurs, nous ne verrons même pas comment créer le host pour le site statique, c’est quelque chose que vous maîtrisez normalement. Sinon, pour Apache, j’ai écrit un article à ce sujet.
Reverse avec Nginx
# on redirige le http vers le https
server {
listen [::]:80;
listen 80;
server_name api.pstconverter.net;
return 301 https://api.pstconverter.net$request_uri;
}
server {
listen [::]:443;
listen 443;
server_name api.pstconverter.net;
root /var/www/api;
charset utf-8;
location / {
# on ajoute les infos du client afin qu'elles soient accessibles à node
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# comme le front est sur un domaine différent, une requête preflight (OPTIONS) sera envoyée
# avant toute requêtes PUT/PATCH/DELETE ou contenant des headers spécifiques
if ($request_method = OPTIONS) {
# dans ce cas on répond donc que le domaine d'origine est autorisé
add_header Access-Control-Allow-Origin 'https://pstconverter.net';
# que les méthodes POST et OPTIONS sont autorisées (à moduler selon vos besoins)
add_header Access-Control-Allow-Methods 'POST, OPTIONS';
# et la liste des headers autorisés (à moduler selon vos besoins, ici j'ai des headers spécifiques)
add_header Access-Control-Allow-Headers 'Origin, X-Requested-With, Content-Type, Accept, Authorization, uploader-chunk-number, uploader-chunks-total, uploader-file-id';
# on précise aussi une durée de validité afin de ne pas refaire une preflight à chaque fois
add_header Access-Control-Max-Age 86400;
add_header Content-Length 0;
add_header Content-Type text/plain;
return 204;
}
# pour toute autre type de requête, quel que soit le response code (always), on envoie notre politique CORS
add_header Access-Control-Allow-Origin 'https://pstconverter.net' always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
# ici on spécifie de passer la requête au serveur en écoute sur le port local 8080
# et on passe le header content type de la requête (tous les autres sont automatiquement supprimés)
proxy_pass http://localhost:8080;
proxy_pass_header Content-type;
}
# ensuite il s'agit de la conf TLS
# sauf mention contraire, la communication avec le backend se fait en HTTP
include conf.d/ssl.conf;
ssl_certificate /etc/letsencrypt/live/api.pstconverter.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.pstconverter.net/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/api.pstconverter.net/chain.pem;
}
Il n’y a plus qu’à activer cet hôte avec un lien symbolique depuis sites-available
vers sites-enabled
et le tour est joué. J’imagine (et j’espère) que vous utilisez un fichier par hôte, sinon, honte à vous !
Reverse avec Apache2
C’est moins à la mode mais Apache est tout aussi efficace. De plus, il tourne déjà peut-être sur votre serveur pour d’autres raisons, dans ce cas, autant l’utiliser ! En plus, si vous êtes déjà familier d’Apache, alors ce sera parfaît pour vous.
# redirection http vers https
<VirtualHost api.pstconverter.net:80>
ServerName api.pstconverter.net
ServerAdmin mon@mail.net
Redirect permanent / https://api.pstconverter.net/
</VirtualHost>
<VirtualHost api.pstconverter.net:443>
Protocols h2 http/1.1
ServerName api.pstconverter.net
ServerAdmin mon@mail.net
# nos paramètres de bases
Header always set Access-Control-Allow-Origin "https://pstconverter.net"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Max-Age 86400
Header always set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization, uploader-chunk-number, uploader-chunks-total, uploader-file-id"
# pour les preflight, il faut bien répondre avec un statut success (ici 204)
# apache répond en 404 par défaut
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
# conf TLS,
# sauf mention contraire, la communication avec le backend se fait en HTTP
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/api.pstconverter.net/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/api.pstconverter.net/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/api.pstconverter.net/chain.pem
# on transmet ensuite les requêtes au backend sur le port local 8080
ProxyPass "/" "http://localhost:8080/"
</VirtualHost>
Apache ajoute par défaut les informations des en-têtes du client, nous n’avons donc rien à faire. Dans les deux cas cependant, il existe un nouvel en-tête standard, Forwarded
qui remplace les en-têtes non standards [en] que sont : X-Real-IP
, X-Forwarded-For
et X-Forwarded-Proto
. Cependant, ce dernier n’est pas encore supporté par défaut par ces deux serveurs, il faudra attendre une adoption un peu plus large.
Votre application est maintenant prête pour affronter son succès !
N’hésitez pas à faire part de vos expériences. Avec quelles technos couplez-vous Node ? L’utilisez-vous pour de l’API ou même pour directement générer du HTML ? Comme on dit en anglais: what’s your stack like?
Commentaires
Rejoignez la discussion !