Conteneurisation d’une application Node.js pour le développement avec Docker Compose

introduction

Si vous développez activement une application, utilisez Docker pour simplifier votre flux de travail et le processus de déploiement de votre application en production. Travailler avec des conteneurs en développement offre les avantages suivants:

  • Les environnements sont cohérents, ce qui signifie que vous pouvez choisir les langues et les dépendances de votre projet sans vous soucier des conflits système.

  • Les environnements sont isolés, ce qui facilite la résolution des problèmes et l’intégration de nouveaux membres de l’équipe.

  • Les environnements sont portables, vous permettant d’emballer et de partager votre code avec d’autres.

Ce tutoriel vous montrera comment configurer un environnement de développement pour une application Node.js à l’aide de Docker. Vous allez créer deux conteneurs - un pour l’application Node et un autre pour la base de données MongoDB - avec Docker Compose. Comme cette application fonctionne avec Node et MongoDB, notre configuration procédera comme suit:

  • Synchronisez le code de l’application sur l’hôte avec le code du conteneur pour faciliter les modifications en cours de développement.

  • Assurez-vous que les modifications apportées au code de l’application fonctionnent sans redémarrage.

  • Créez une base de données utilisateur et protégée par mot de passe pour les données de l’application.

  • Persistez ces données.

À la fin de ce didacticiel, vous aurez une application d’information sur les requins fonctionnant sur des conteneurs Docker:

image: https: //assets.digitalocean.com/articles/node_docker_dev/persisted_data.png [Collection complète de requins]

Conditions préalables

Pour suivre ce tutoriel, vous aurez besoin de:

Étape 1 - Clonage du projet et modification des dépendances

La première étape de la construction de cette configuration consistera à cloner le code du projet et à modifier son fichier https://docs.npmjs.com/files/package.json [+ package.json +], qui inclut les dépendances du projet. Nous allons ajouter https://www.npmjs.com/package/nodemon [+ nodemon +] au projet https://docs.npmjs.com/files/package.json#devdependencies [+ devDependencies +], en précisant que nous allons l’utiliser pendant le développement. L’exécution de l’application avec + nodemon + garantit son redémarrage automatique chaque fois que vous modifiez votre code.

Tout d’abord, clonez le https://github.com/do-community/nodejs-mongo-mongoose [+ nodejs-mongo-mongoose + référentiel] à partir du DigitalOcean Community compte GitHub . Ce référentiel comprend le code de la configuration décrite dans Comment intégrer MongoDB à votre application de nœud, qui explique comment intégrer une base de données MongoDB à une application de noeud existante à l’aide de Mongoose.

Clonez le référentiel dans un répertoire appelé ++:

git clone https://github.com/do-community/nodejs-mongo-mongoose.git

Accédez au répertoire ++:

cd

Ouvrez le fichier + package.json du projet en utilisant` + nano` ou votre éditeur favori:

nano package.json

Sous les dépendances du projet et au-dessus de l’accolade fermante, créez un nouvel objet + devDependencies + qui inclut + nodemon +:

~ / node_project / package.json

...
"dependencies": {
   "ejs": "^2.6.1",
   "express": "^4.16.4",
   "mongoose": "^5.4.10"
 }



}

Enregistrez et fermez le fichier une fois l’édition terminée.

Une fois le code de projet en place et ses dépendances modifiés, vous pouvez procéder à une refactorisation du code pour un flux de travail conteneurisé.

Étape 2 - Configuration de votre application pour qu’elle fonctionne avec des conteneurs

Modifier votre application pour un flux de travail conteneurisé signifie rendre votre code plus modulaire. Les conteneurs offrent une portabilité entre les environnements. Notre code devrait en tenir compte en restant aussi découplés que possible du système d’exploitation sous-jacent. Pour ce faire, nous allons refactoriser notre code pour utiliser davantage la propriété process.env de Node, qui renvoie un objet contenant des informations sur votre environnement utilisateur au moment de l’exécution. Nous pouvons utiliser cet objet dans notre code pour affecter dynamiquement des informations de configuration lors de l’exécution à l’aide de variables d’environnement.

Commençons par + app.js +, notre principal point d’entrée de l’application. Ouvrez le fichier:

nano app.js

À l’intérieur, vous verrez une définition d’un + port + constant, ainsi qu’un https: / /expressjs.com/en/4x/api.html#app.listen [+ listen + function] qui utilise cette constante pour spécifier le port sur lequel l’application écoutera:

~ / home / node_project / app.js

...
const port = 8080;
...
app.listen(port, function () {
 console.log('Example app listening on port 8080!');
});

Redéfinissons la constante + port + pour permettre l’affectation dynamique à l’exécution à l’aide de l’objet + process.env +. Apportez les modifications suivantes à la définition de la constante et à la fonction + listen +:

~ / home / node_project / app.js

...

...
app.listen(port, function () {
 console.log();
});

Notre nouvelle définition de constante attribue dynamiquement + port + en utilisant la valeur transmise à l’exécution ou + 8080 +. De même, nous avons réécrit la fonction + listen + pour utiliser un https://www.digitalocean.com/community/tutorials/how-to-work-with-strings-in-javascript#string-literals-and-string -values ​​[template literal], qui interpolera la valeur du port lors de l’écoute de connexions. Comme nous allons cartographier nos ports ailleurs, ces révisions nous éviteront de devoir réviser ce fichier en permanence à mesure que notre environnement change.

Lorsque vous avez terminé, enregistrez et fermez le fichier.

Ensuite, nous modifierons les informations de connexion à la base de données pour supprimer les informations d’identification de configuration. Ouvrez le fichier + db.js +, qui contient ces informations:

nano db.js

Actuellement, le fichier fait les choses suivantes:

  • Importe Mongoose, le Object Document Mapper (ODM) que nous utilisons pour créer des schémas et des modèles pour nos données d’application.

  • Définit les informations d’identification de la base de données comme constantes, y compris le nom d’utilisateur et le mot de passe.

  • Se connecte à la base de données à l’aide de la méthode https://mongoosejs.com/docs/api.html#connection_Connection [+ mongoose.connect +].

Notre première étape dans la modification du fichier consistera à redéfinir les constantes qui incluent des informations sensibles. Actuellement, ces constantes ressemblent à ceci:

~ / node_project / db.js

...
const MONGO_USERNAME = '';
const MONGO_PASSWORD = '';
const MONGO_HOSTNAME = '127.0.0.1';
const MONGO_PORT = '27017';
const MONGO_DB = '';
...

Au lieu de coder en dur ces informations, vous pouvez utiliser l’objet + process.env + pour capturer les valeurs d’exécution de ces constantes. Modifiez le bloc pour qu’il ressemble à ceci:

~ / node_project / db.js

...
const {
 MONGO_USERNAME,
 MONGO_PASSWORD,
 MONGO_HOSTNAME,
 MONGO_PORT,
 MONGO_DB
} = process.env;
...

Enregistrez et fermez le fichier une fois l’édition terminée.

À ce stade, vous avez modifié + db.js + pour fonctionner avec les variables d’environnement de votre application, mais vous avez toujours besoin d’un moyen de les transmettre à votre application. Créons un fichier + .env + avec des valeurs que vous pouvez transmettre à votre application au moment de l’exécution.

Ouvrez le fichier:

nano .env

Ce fichier comprendra les informations que vous avez supprimées de + db.js +: le nom d’utilisateur et le mot de passe de la base de données de votre application, ainsi que le paramètre de port et le nom de la base de données. N’oubliez pas de mettre à jour le nom d’utilisateur, le mot de passe et le nom de la base de données répertoriés ici avec vos propres informations:

~ / node_project / .env

MONGO_USERNAME=
MONGO_PASSWORD=
MONGO_PORT=27017
MONGO_DB=

Notez que nous avons * supprimé * le paramétrage de l’hôte apparu à l’origine dans + db.js. Nous allons maintenant définir notre hôte au niveau du fichier Docker Compose, ainsi que d’autres informations sur nos services et nos conteneurs.

Enregistrez et fermez ce fichier une fois l’édition terminée.

Étant donné que votre fichier + .env + contient des informations sensibles, vous devez vous assurer qu’il est inclus dans les fichiers + .dockerignore + et + .gitignore + de votre projet afin qu’il ne soit pas copié dans votre contrôle de version ou vos conteneurs.

Ouvrez votre fichier + .dockerignore +:

nano .dockerignore

Ajoutez la ligne suivante au bas du fichier:

~ / node_project / .dockerignore

...
.gitignore

Enregistrez et fermez le fichier une fois l’édition terminée.

Le fichier + .gitignore + de ce référentiel inclut déjà + .env +, mais n’hésitez pas à vérifier qu’il se trouve bien ici:

nano .gitignore

~~ / node_project / .gitignore

...
.env
...

À ce stade, vous avez correctement extrait les informations sensibles de votre code de projet et pris des mesures pour contrôler comment et où ces informations sont copiées. Vous pouvez maintenant ajouter plus de robustesse à votre code de connexion à la base de données afin de l’optimiser pour un flux de travail conteneurisé.

Étape 3 - Modification des paramètres de connexion à la base de données

Notre prochaine étape consistera à rendre notre méthode de connexion à la base de données plus robuste en ajoutant du code permettant de gérer les cas où notre application ne parvient pas à se connecter à notre base de données. L’introduction de ce niveau de résilience au code de votre application est une pratique recommended lorsque vous travaillez avec des conteneurs en utilisant Compose.

Ouvrez + db.js + pour l’édition:

nano db.js

Vous verrez le code que nous avons ajouté précédemment, ainsi que la constante + url + pour l’URI de connexion de Mongo et la méthode Mongoose + connect +:

~ / node_project / db.js

...
const {
 MONGO_USERNAME,
 MONGO_PASSWORD,
 MONGO_HOSTNAME,
 MONGO_PORT,
 MONGO_DB
} = process.env;

const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;

mongoose.connect(url, {useNewUrlParser: true});

Actuellement, notre méthode + connect + accepte une option qui indique à Mongoose d’utiliser son new URL parser. Ajoutons quelques options supplémentaires à cette méthode pour définir les paramètres des tentatives de reconnexion. Nous pouvons le faire en créant une constante + options + qui inclut les informations pertinentes, en plus de la nouvelle option d’analyse URL. Sous vos constantes Mongo, ajoutez la définition suivante pour une constante + options +:

~ / node_project / db.js

...
const {
 MONGO_USERNAME,
 MONGO_PASSWORD,
 MONGO_HOSTNAME,
 MONGO_PORT,
 MONGO_DB
} = process.env;







...

L’option + reconnectTries + indique à Mongoose de continuer à essayer de se connecter indéfiniment, tandis que + reconnectInterval + définit la période entre les tentatives de connexion en millisecondes. + connectTimeoutMS + définit 10 secondes comme la période d’attente du pilote Mongo avant l’échec de la tentative de connexion.

Nous pouvons maintenant utiliser la nouvelle constante + options + dans la méthode Mongoose + connect + pour affiner nos paramètres de connexion Mongoose. Nous allons également ajouter un promise pour gérer les erreurs de connexion potentielles.

Actuellement, la méthode Mongoose + connect + ressemble à ceci:

~ / node_project / db.js

...
mongoose.connect(url, {useNewUrlParser: true});

Supprimez la méthode + connect + existante et remplacez-la par le code suivant, qui inclut la constante + options + et une promesse:

~ / node_project / db.js

...

En cas de connexion réussie, notre fonction enregistre un message approprié; sinon, il https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch [+ catch +] et enregistre l’erreur, ce qui nous permet de résoudre le problème.

Le fichier fini ressemblera à ceci:

~ / node_project / db.js

const mongoose = require('mongoose');

const {
 MONGO_USERNAME,
 MONGO_PASSWORD,
 MONGO_HOSTNAME,
 MONGO_PORT,
 MONGO_DB
} = process.env;

const options = {
 useNewUrlParser: true,
 reconnectTries: Number.MAX_VALUE,
 reconnectInterval: 500,
 connectTimeoutMS: 10000,
};

const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;

mongoose.connect(url, options).then( function() {
 console.log('MongoDB is connected');
})
 .catch( function(err) {
 console.log(err);
});

Enregistrez et fermez le fichier une fois l’édition terminée.

Vous avez maintenant ajouté de la résilience à votre code d’application pour gérer les cas où votre application pourrait ne pas réussir à se connecter à votre base de données. Avec ce code en place, vous pouvez définir vos services avec Compose.

Étape 4 - Définition des services avec Docker Compose

Avec votre code refactorisé, vous êtes prêt à écrire le fichier + docker-compose.yml + avec vos définitions de service. Un service dans Compose est un conteneur en cours d’exécution, ainsi que les définitions de service - que vous allez inclure dans votre fichier + docker-compose.yml + - - contiennent des informations sur le fonctionnement de chaque image de conteneur. L’outil Compose vous permet de définir plusieurs services pour créer des applications multi-conteneurs.

Avant de définir nos services, toutefois, nous allons ajouter un outil à notre projet appelé https://github.com/Eficode/wait-for [+ wait-for +] pour que notre application ne tente de se connecter qu’une seule fois à notre base de données. les tâches de démarrage de la base de données sont terminées. Ce script d’encapsulation utilise `+. netcat + ` pour savoir si un hôte et un port spécifiques acceptent les connexions TCP. Son utilisation vous permet de contrôler les tentatives de votre application pour se connecter à votre base de données en vérifiant si la base de données est prête à accepter les connexions.

Bien que Compose vous permette de spécifier des dépendances entre des services à l’aide de l’option https://docs.docker.com/compose/compose-file/#depends_on [+ depend_on +], cette commande dépend du fait que le conteneur est exécuté ou non. que sa disponibilité. L’utilisation de + depend_on + ne sera pas optimale pour notre configuration, car nous souhaitons que notre application ne se connecte que lorsque les tâches de démarrage de la base de données, y compris l’ajout d’un utilisateur et d’un mot de passe à la base d’authentification + admin +, sont terminées. Pour plus d’informations sur l’utilisation de + wait-for + et d’autres outils pour contrôler l’ordre de démarrage, veuillez vous reporter à la recommendations correspondante dans la documentation Compose.

Ouvrez un fichier nommé + wait-for.sh +:

nano wait-for.sh

Collez le code suivant dans le fichier pour créer la fonction d’interrogation:

~ / node_project / app / wait-for.sh

#!/bin/sh

# original script: https://github.com/eficode/wait-for/blob/master/wait-for

TIMEOUT=15
QUIET=0

echoerr() {
 if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}

usage() {
 exitcode="$1"
 cat << USAGE >&2
Usage:
 $cmdname host:port [-t timeout] [-- command args]
 -q | --quiet                        Do not output any status messages
 -t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
 -- COMMAND ARGS                     Execute command with args after the test finishes
USAGE
 exit "$exitcode"
}

wait_for() {
 for i in `seq $TIMEOUT` ; do
   nc -z "$HOST" "$PORT" > /dev/null 2>&1

   result=$?
   if [ $result -eq 0 ] ; then
     if [ $# -gt 0 ] ; then
       exec "$@"
     fi
     exit 0
   fi
   sleep 1
 done
 echo "Operation timed out" >&2
 exit 1
}

while [ $# -gt 0 ]
do
 case "$1" in
   *:* )
   HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
   PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
   shift 1
   ;;
   -q | --quiet)
   QUIET=1
   shift 1
   ;;
   -t)
   TIMEOUT="$2"
   if [ "$TIMEOUT" = "" ]; then break; fi
   shift 2
   ;;
   --timeout=*)
   TIMEOUT="${1#*=}"
   shift 1
   ;;
   --)
   shift
   break
   ;;
   --help)
   usage 0
   ;;
   *)
   echoerr "Unknown argument: $1"
   usage 1
   ;;
 esac
done

if [ "$HOST" = "" -o "$PORT" = "" ]; then
 echoerr "Error: you need to provide a host and port to test."
 usage 2
fi

wait_for "$@"

Enregistrez et fermez le fichier lorsque vous avez terminé d’ajouter le code.

Rendre le script exécutable:

chmod +x wait-for.sh

Ensuite, ouvrez le fichier + docker-compose.yml +:

nano docker-compose.yml

Tout d’abord, définissez le service d’application + nodejs + en ajoutant le code suivant au fichier:

~ / node_project / docker-compose.yml

version: '3'

services:
 nodejs:
   build:
     context: .
     dockerfile: Dockerfile
   image: nodejs
   container_name: nodejs
   restart: unless-stopped
   env_file: .env
   environment:
     - MONGO_USERNAME=$MONGO_USERNAME
     - MONGO_PASSWORD=$MONGO_PASSWORD
     - MONGO_HOSTNAME=db
     - MONGO_PORT=$MONGO_PORT
     - MONGO_DB=$MONGO_DB
   ports:
     - "80:8080"
   volumes:
     - .:/home/node/app
     - node_modules:/home/node/app/node_modules
   networks:
     - app-network
   command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js

La définition du service + nodejs + comprend les options suivantes:

  • + build +: Ceci définit les options de configuration, y compris les + context et` + dockerfile`, qui seront appliquées lorsque Compose construit l’image de l’application. Si vous souhaitez utiliser une image existante d’un registre tel que Docker Hub, vous pouvez utiliser le https://docs.docker.com/compose/compose-file/#image [ + image + instruction] à la place, avec des informations sur votre nom d’utilisateur, votre référentiel et la balise image.

  • + context +: Ceci définit le contexte de construction de l’image - dans ce cas, le répertoire du projet en cours.

  • + dockerfile +: Ceci spécifie le + Dockerfile + dans le répertoire de votre projet actuel, car le fichier que Compose utilisera pour créer l’image de l’application. Pour plus d’informations sur ce fichier, veuillez consulter Comment créer une application Node.js avec Docker.

  • + image +, + container_name +: ces noms s’appliquent à l’image et au conteneur.

  • + restart +: Ceci définit la politique de redémarrage. La valeur par défaut est + no +, mais nous avons configuré le conteneur pour qu’il redémarre sauf s’il est arrêté.

  • + env_file +: Ceci indique à Compose que nous aimerions ajouter des variables d’environnement à partir d’un fichier appelé + .env +, situé dans notre contexte de construction.

  • + environment +: l’utilisation de cette option vous permet d’ajouter les paramètres de connexion Mongo définis dans le fichier + .env +. Notez que nous ne configurons pas + NODE_ENV + sur + développement +, car il s’agit de Express https://github.com/expressjs/express/blob/dc538f6e810bd462c98ee7e6aae24a624c64d4b1da/application. js # L71 [comportement par défaut] si + NODE_ENV + n’est pas défini. Lorsque vous passez en production, vous pouvez définir ceci comme suit: + production +: enable afficher la mise en cache et éviter les erreurs plus prolixes messages. Notez également que nous avons spécifié le conteneur de base de données + db + en tant qu’hôte, comme indiqué dans la page https://www.digitalocean.com/community/tutorials/containerizing-a-node-js-application-for-development-with- docker-composer # step-2-% E2% 80% 94 - configurer votre application pour travailler avec des conteneurs [Étape 2].

  • + ports +: Ceci mappe le port + 80 + de l’hôte sur le port + 8080 + du conteneur.

  • + volumes +: Nous incluons ici deux types de montures:

  • Le premier est un bind mount qui monte notre code d’application sur l’hôte dans le répertoire + / home / node / app + du conteneur. Cela facilitera le développement rapide, car toute modification apportée à votre code hôte sera immédiatement renseignée dans le conteneur.

  • Le second est nommé volume, + node_modules +. Lorsque Docker exécute l’instruction + npm install + répertoriée dans l’application + Dockerfile +, + npm + crée un nouveau +node_modules+ ` sur le conteneur qui contient les packages nécessaires à l’exécution de l’application. Le montage de liaison que nous venons de créer masquera toutefois ce répertoire `+ node_modules +