Laisser un commentaire

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 :

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 !

Vous pouvez utiliser Markdown pour les liens [ancre de lien](url), la mise en *italique* et en **gras**. Enfin pour le code, vous pouvez utiliser la syntaxe `inline` et la syntaxe bloc

```
ceci est un bloc
de code
```