Table des matières

Environement Docker au CREMI

L’utilisation de Docker n’est pas possible au CREMI car le démon docker nécessite des privilèges élevés, ce qui pose des problèmes de sécurité dans un environnement mutualisé. Nous allons utiliser à la place podman une alternative qui n’a pas cette contrainte. Pour simplifier son utilisation, définissez les alias suivant :

alias podman="podman --root=$TMPDIR/containers --storage-driver=overlay "
alias podman-compose="podman-compose --podman-args=\"--root=$TMPDIR/containers --storage-driver=overlay\" "
alias docker=podman
alias docker-compose=podman-compose

Step 1 : création d’une application minimaliste

Nous allons commencer par créer un serveur web minimaliste. Dans un répertoire vierge, créez un dossier src et le fichier index.js suivant.

import express from 'express';

const app = express();
const PORT = 80;

app.get('/', (req, res) => {
    res.send('Bonjour !');
});

app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

Initialiser la configuration npm avec npm init -y puis ajouter les lignes suivantes :

  "type": "module",
  "scripts": {
    "dev": "nodemon src/index.js",
    "start": "node src/index.js"
  }

Testez votre application en local. Ça ne marche pas! Normal, le port utilisé n’est pas possible. Nous allons simplifier la distrubution de notre application en la faisant tourner dans un container docker.

Step 2: contexte

Ce que propose Docker, c’est de packager l’application dans un conteneur. De loin un conteneur ressemble un peu à une Machine virtuelle. De plus près, un conteneur est différent, car il ne partage que son noyau avec celui de l’hôte et ne contient que le strict nécessaire au bon fonctionnement de l’application, à savoir l’application elle-même ainsi que tout son environnement (librairies, compilateur…).

L’avantage principal du conteneur est sa légèreté en termes d’espace disque (la plupart du temps moins de 1Go, voir moins de 100Mo), mais aussi en termes de ressources consommées: un conteneur n’a pas besoin de faire tourner son propre noyau et consomme très peu de RAM/CPU.

drawing drawing

L’autre avantage de conteneuriser son application - celui qui nous intéresse le plus ici - c’est qu’il est possible de packager l’application et son environnement d’exécution dans un conteneur. La création de ce conteneur est le plus souvent l’affaire du développeur: plus besoin, pour l’administrateur, de gérer des centaines de librairies possédant chacune plusieurs versions, le tout sur de nombreux serveurs. Il suffit d’installer Docker et de lancer les conteneurs!

Step 3: construisons notre première image

C’est exactement ce que nous allons faire ici: packager notre application. Pour ce faire, nous allons créer un fichier Dockerfile à la racine de l’application. Un Dockerfile est un fichier texte scripté utilisé par Docker pour automatiser le processus de création d’une image conteneur. Il contient une série d’instructions, comme FROM, RUN, COPY, et CMD, qui définissent la base de l’image, les dépendances à installer, les fichiers à copier, les variables d’environnement à définir, et le comportement par défaut du conteneur lorsqu’il est exécuté. Une fois écrit, le Dockerfile est utilisé avec la commande docker build pour créer une image Docker immuable, qui peut ensuite être exécutée comme un conteneur indépendant sur n’importe quelle plateforme compatible avec Docker.

FROM node:latest
WORKDIR /app
COPY ./package.json /app/package.json
RUN npm install
COPY . .
EXPOSE 80
CMD ["npm", "run", "dev"]

Détaillons son contenu:

FROM node:latest

La commande FROM initialise une nouvelle étape de construction et définit l’image de base à partir de laquelle on commence.

WORKDIR /app

WORKDIR définit le répertoire de travail pour les instructions à suivre.

COPY ./package.json /app/package.json
COPY . .

La commande COPY permet de copier des fichiers ou des répertoires depuis le système hôte vers le système de fichiers de l’image en cours de construction.

RUN npm install

La commande RUN permet d’exécuter des commandes dans une nouvelle couche au-dessus de l’image en cours de construction et de valider les résultats pour créer une nouvelle image.

CMD ["npm", "run", "dev"]

La commande CMD définit la commande exécutée par défaut lors du démarrage d’un conteneur à partir de l’image construite. Cette commande peut-être remplacée par l’utilisateur lors de l’exécution du conteneur.

Vous pouvez maintenant construire votre image Docker à partir du Dockerfile et lui attribuer un nom, comme par exemple web.

$ docker build -t web .

Step 4: lancement de notre conteneur

La commande docker run est utilisée pour exécuter un conteneur Docker à partir d’une image spécifiée, démarrant ainsi une instance du conteneur avec les paramètres et les options spécifiés. Utiliser la commande suivante pour lancer le conteneur en arrière plan et mapper un port entre le système hôte (votre machine locale) et le conteneur Docker. Le mapping de port est indispensable pour que les services dans le conteneur soient accessibles depuis l’extérieur.

docker run -d -p 8090:80 web

Vous pouvez maintenant ouvrir un navigateur web et accéder à l’URL http://localhost:8090/. Pour lister les conteneurs en cours d’exécution, utilisez docker ps, et pour arrêter un conteneur spécifique, utilisez docker stop <ID_DU_CONTENEUR>.

Step 5: modification de l’application

Maintenant que l’application est déployée et fonctionnelle, nous allons lui apporter une petite modification, en l’occurence modifier le texte Bonjour! en Hello!.

Le code a été modifié, il faut donc:

  • recréer l’image
  • la relancer.

Le message d’erreur signale que le port 8090 est déjà en cours d’utilisation, car notre conteneur continue de fonctionner. Pour résoudre cela, vous pouvez afficher la liste de tous les conteneurs en cours d’exécution en utilisant la commande docker ps --all, puis arrêter et supprimer le conteneur en utilisant les commandes docker stop <NOMDUCONTENEUR> et docker rm ,<NOMDUCONTENEUR>. Assurez-vous de vérifier avec docker ps --all que le conteneur a été correctement supprimé, puis créez un nouveau conteneur à partir de la nouvelle image pour vérifier que vos modifications ont été prises en compte.

Step 6: partager votre application

Solution 1: via un dépôt d’images

Un dépôt d’images Docker, tel que Docker Hub, est un service en ligne qui offre la possibilité de stocker, partager et distribuer des images Docker. L’utilisation de docker pull vous permet de télécharger des images depuis ce dépôt vers votre machine locale, ce qui est particulièrement utile pour obtenir des images prêtes à l’emploi. D’autre part, docker push vous permet d’envoyer vos propres images Docker depuis votre machine locale vers le dépôt, ce qui facilite le partage et la distribution de vos applications et services dans un environnement conteneurisé.

L’introduction de tags lors de l’utilisation de docker pull ou docker push est essentielle pour la gestion des versions. Les tags vous permettent de spécifier des versions ou des variantes spécifiques de vos images. Par exemple, en ajoutant des tags comme v1.0, v2.0, ou latest à vos images, vous pouvez clairement indiquer la version de l’image que vous souhaitez utiliser. Cela simplifie la gestion des mises à jour et permet aux utilisateurs de choisir la version appropriée de l’image en fonction de leurs besoins, ce qui est essentiel pour garantir la cohérence et la fiabilité des déploiements de conteneurs.

Utilisez la commande docker pull pour télécharger l’image Alpine depuis Docker Hub :

docker pull alpine

Une fois l’image Alpine téléchargée, vous pouvez exécuter un conteneur interactif en utilisant les options -it. Par exemple, exécutez un shell interactif dans un conteneur Alpine nouvellement créé :

docker run -it --rm alpine /bin/sh

Vous serez maintenant connecté à un shell interactif dans le conteneur Alpine, ce qui vous permettra d’interagir avec le système de fichiers de l’image Alpine et d’exécuter des commandes à l’intérieur du conteneur. Une fois que vous avez terminé, vous pouvez quitter le shell interactif en utilisant exit, et le conteneur sera automatiquement supprimé en raison de l’option --rm.

Solution 2: en distribuant le code source

Une autre approche, tout aussi fréquemment utilisée que la précédente, consiste à placer à la fois le code source de votre application et le Dockerfile correspondant dans un dépôt Git par exemple.

Step 7: gestion des données

Démarrez le conteneur précédent, puis, une fois qu’il est en cours d’exécution, accédez à son shell interactif et modifiez le fichier src/index.js en utilisant un éditeur de ligne de commande tel que nano, vi, ou emacs -nw pour changer le texte affiché. Comme le conteneur précédent utilise “modemon”, la page devrait être mise à jour automatiquement pour refléter les modifications que vous apportez, simplifiant ainsi le processus de développement en vous permettant de voir les résultats en temps réel.

Lorsque vous arrêtez et redémarrez votre conteneur, toutes les modifications apportées sont effacées. Pour remédier à cette situation, la solution consiste à configurer notre conteneur de manière à ce qu’il stocke ses données en dehors du conteneur, sur la machine hôte.

Solution 1: les bind mounts

Les bind mounts sont une méthode simple pour stocker des données en dehors d’un conteneur Docker. Par exemple, en utilisant -v /chemin/local:/chemin/conteneur lors du démarrage d’un conteneur, tout ce qui est stocké dans /chemin/local sur votre machine hôte est accessible depuis le conteneur via /chemin/conteneur. Cela assure que les données sont persistantes même si le conteneur est arrêté ou supprimé.

Modifiez l’application précédente afin que le répertoire src à l’intérieur du conteneur corresponde au répertoire src sur votre machine hôte. Vous devriez maintenant pouvoir modifier vos fichiers dans votre éditeur préféré sur votre machine hôte.

Solution 2: les volumes

Les volumes dans Docker sont une autre façon de gérer la persistance des données. Contrairement aux bind mounts qui lient des répertoires de la machine hôte aux conteneurs, les volumes sont des systèmes de fichiers gérés par Docker lui-même, offrant une gestion plus flexible et des avantages importants.

Lorsque vous créez un volume Docker, Docker se charge de sa gestion et de sa persistance, ce qui signifie que les données stockées dans ces volumes ne sont pas liées à un chemin spécifique sur la machine hôte. Cela rend les volumes portables, ce qui vous permet de les utiliser sur différents hôtes Docker sans avoir à vous soucier de la localisation des données.

Les volumes peuvent être créés avec la commande docker volume create et montés dans des conteneurs en utilisant l’option -v lors du démarrage. Par exemple, vous pouvez créer un volume nommé “mon_volume” et le monter dans un conteneur avec la commande :

docker run -v mon_volume:/chemin/conteneur mon_image

Cette approche permet de garantir la persistance des données même lorsque les conteneurs sont arrêtés, redémarrés ou déplacés entre différents environnements Docker.

Voici quelques commandes couramment utilisées pour gérer les volumes Docker :

Step 8: applications multi-conteneurs

Notre application fonctionne actuellement dans un seul conteneur, mais en suivant les bonnes pratiques des conteneurs, il est préférable de diviser les tâches en plusieurs conteneurs distincts. Cela apporte plusieurs avantages : cela évite que le problème dans un service ne perturbe les autres, facilite les mises à jour et les redémarrages indépendants, permet de mettre à jour un service sans affecter les autres, évite la concurrence pour les ressources, et simplifie le déploiement pièce par pièce.

Nous allons commencer par modifier notre application pour qu’elle soit un peu plus compliqué, mais pas trop. Nous allons faire une application qui affiche sous forme de liste les contacts contenus dans une base de donnée Mongo. Nous allons utiliser l’API Random User Generator pour générer des utilisateurs fictifs et remplir la base.

Pour simplifier le fonctionnement, nous aurons plusieurs conteneurs à déployer :

Mise en réseau des conteneurs

Voici un exemple de Dockerfile pour générer une image qui inclut Node et Mongo.

# Use Ubuntu 22.04 as base image
FROM ubuntu:22.04

# Install NodeJS
# Update and install required packages
RUN apt-get update && apt-get install -y ca-certificates curl gnupg lsb-release
RUN curl -sSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/nodesource.gpg
RUN NODE_MAJOR=20 && \
    echo "deb [signed-by=/etc/apt/trusted.gpg.d/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get install -y nodejs



# Install MongoDB
RUN apt-get update && apt-get install -y gnupg curl
RUN curl -fsSL https://pgp.mongodb.com/server-7.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg
RUN echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu $(lsb_release -cs)/mongodb-org/7.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list
RUN apt-get update && apt-get install -y mongodb-org jq
RUN mkdir -p /data/db && chown -R mongodb:mongodb /data/db

# Setup our app
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .

EXPOSE 80

# Use a script to start mongod and node
COPY start.sh /start.sh
RUN chmod +x /start.sh
CMD ["/start.sh"]

avec le script de démarrage

#!/bin/bash
mongod &
npm run dev

Tout comme nous avons crée un named volume tout à l’heure, Docker nous permet aussi de créer des réseaux nommés. La règle est simple: deux conteneurs sur un même réseau pourront se parler, deux conteneurs sur deux réseaux différents ne pourront pas. Simple et efficace.

Commencez par créer le réseau:

docker network create contacts-net

Ensuite, créez un second conteneur pour MySQL:

docker run -d --network contacts-net --network-alias mongo -v db-data:/data/db mongo

La commande est assez longue, prenons le temps de la décortiquer:

Step 9: docker compose

Docker Compose est un outil qui simplifie la gestion d’applications multi-conteneurs en permettant de définir leur configuration dans un fichier YAML, puis de les déployer avec une seule commande. Il facilite la création, la gestion des réseaux et des volumes, ainsi que la configuration des variables d’environnement pour chaque service de l’application. C’est un moyen efficace de gérer des applications complexes basées sur Docker.