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
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.
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.
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!
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"]
💡 Le nom Dockerfile
est une convention courante, mais il n’est pas strictement nécessaire; cependant, Docker le reconnaîtra automatiquement s’il porte ce nom dans le répertoire de construction. Si vous choisissez de lui donner un autre nom, vous devrez préciser ce nom en utilisant le paramètre -f
ou --file
lors de la commande docker build
pour indiquer le chemin vers le fichier de configuration.
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.
🔥 Une installation classique de Docker définit des alias pour les noms des images standards. L’installation podman du CREMI est plus légère et il est souvent recommandé d’utiliser des noms explicites pour les images. Préférez donc docker.io/library/node:latest
pour préciser sur quel dépôt aller chercher l’image.
WORKDIR /app
WORKDIR
définit le répertoire de travail pour les instructions à suivre.
💡 En règle générale, le répertoire par défaut est défini sur /app
`, cependant, vous pouvez utiliser le répertoire de votre choix, tant que vous maintenez la cohérence dans votre configuration.
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 .
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>
.
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!.
❓ Vous avez modifié le code, quelles sont les étapes à suivre pour déployer à nouveau l’application?
Le code a été modifié, il faut donc:
❓ Vous avez une erreur lors du second docker run
? C’est normal. Lisez bien le message d’erreur, il y a une étape qui a été omise. Essayez de deviner ce qu’il faut faire.
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.
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
-it
: Ces options permettent d’ouvrir une session interactive avec le conteneur.--rm
: Cette option indique à Docker de supprimer le conteneur une fois qu’il est arrêté.alpine
: C’est le nom de l’image que vous souhaitez exécuter./bin/sh
: C’est la commande que vous souhaitez exécuter dans le conteneur.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
.
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.
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.
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.
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 :
Créer un volume : Utilisez docker volume create
pour créer un nouveau volume.
Lister les volumes : Affichez la liste des volumes avec docker volume ls
.
Supprimer un volume : Supprimez un volume avec docker volume rm
.
Monter un volume dans un conteneur : Utilisez -v
lors du démarrage d’un conteneur pour monter un volume.
Inspecter un volume : Obtenez des informations détaillées sur un volume avec docker volume inspect
.
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 :
api
pour le backfront
mongodb
🔥 L’installation podman du CREMI ne permet pas pour le moment de tester la gestion multi-conteneurs. Vous pouvez créer une image Linux de base et installer dedans le front, votre api et votre base de données.
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.
💡 Et les IPs alors? C’est simple, Docker se charge de tout. Pour accéder à un conteneur à partir d’un autre, il suffira d’utiliser son alias sur le réseau.
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:
--network
permet de préciser à quel réseau nous allons connecter notre conteneur.--network-alias
permet de préciser le nom qu’aura la machine sur le réseau.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.