Prise en main de Docker Généralités Docker est une solution de virtualisation alternative par rapport aux hyperviseurs de type 1 (→ Hyper-V, VMware ESXi, Citrix XenServer…) ou de type 2 (→ VMware Workstation, _Oracle VirtualBox, …). De par son architecture, cette solution de virtualisation est plus légère que celle basée sur les hyperviseurs de type 2. Cependant, elle isole moins les applications les unes par rapport aux autres (⇒ plus de risques de faille de sécurité). Docker est une application client-serveur. Elle s’appuie sur les notions d'images et de containers. Un container contient tout ce qui est nécessaire à l’exécution d’une application. Un container est créé à partir d’une image. On peut assimiler une image Docker au fichier iso d’installation d’un système d’exploitation et le container au système d’exploitation lu-même, après installation Une présentation de Docker en vidéo : 💻 Travail n° 1 Site web statique Pour vous rendre compte de la relative simplicité de mise en œuvre de Docker, vous allez créer un container embarquant un serveur web qui va servir un site web simpliste. Récupérer et lancer la VM Virtual Box fournie par l’enseignant. Celle-ci contient : un système Debian Linux minimal en mode console dans lequel l’application Docker a été installée (serveur + client) des images Docker récupérées au préalable sur le Docker Hub pour éviter d’avoir à les télécharger depuis internet durant l’activité Une fois lancée, s’y connecter avec l’identifiant “john” et le mot de passe “john” également. Récupérer l’adresse ip de la VM avec la commande ip addr Minimiser la VM et exécuter l’application Windows nommée PuTTY (l’installer depuis le NAS si besoin est). Cette application est un client SSH Se connecter à la VM via PuTTY en fournissant son adresse ip récupérée précédemment Le fait d’utiliser PuTTY va permettre de faire des copier-coller de manière aisée et d’avoir une console facilement configurable (taille de la police de caractères par exemple) Lister les images Docker disponibles avec la commande : docker images Résultat : john@docker-lab:~$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE php apache-bookworm aa1a7b18157c 12 days ago 503MB httpd bookworm 359570977af2 3 weeks ago 168MB mysql latest c138801544a9 2 months ago 577MB hello-world latest 9c7a54a9a43c 5 months ago 13.3kB Lancer un container à partir de l'image httpd-bookworm qui contient un serveur web Apache prêt à l’emploi. Les éditeurs de logiciels libres proposent effectivement des images de leurs produits sur le Docker Hub afin d’en faciliter la mise en œuvre+ docker run -d --name apache-server -p 8080:80 httpd:bookworm (1) 1 On lance (→ run) un container depuis l’image httpd:bookworm auquel on donne le nom apache-server (→ option --name) qui s’exécutera en tâche de fond (→ option -d) et qui exposera son port 80 sur le port 8080 de notre VM Debian (→ option -p 8080:80) Résultat : john@docker-lab:~$ docker run -d --name apache-server -p 8080:80 httpd:bookworm a8ab349c84cf2b9423061c2624a04c8feb3743ba27a8fbe9c5a0e20feec4d5c0 Le nombre renvoyé suite à l' exécution de la commande (→ a8ab349c84c…) identifie de manière unique le container (→ container id). Celui qui vous sera renvoyé sera différent de celui affiché ici. Ouvrir dans un navigateur internet l’URL http://<ip-vm-debian>:8080 Celle-ci doit mener à l’affichage d’un message “It works” ⇒ Ceci indique que le serveur web est opérationnel. C’est génial, non ? 😉 Lister les containers actifs avec une des 2 commandes suivantes qui sont équivalentes : docker ps # ou docker container ls Résultat : john@docker-lab:~$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a8ab349c84cf httpd:bookworm "httpd-foreground" 3 minutes ago Up 3 minutes 0.0.0.0:8080->80/tcp, :::8080->80/tcp apache-server On retrouve : l’identifiant du container l'image Docker ayant servi pour ce container la mise en correspondance du port 80 du container sur le port 8080 de l’hôte le nom attribué au container (→ apache-server) On va maintenant ouvrir un shell dans le container afin d’exécuter une commande qui va modifier légèrement le contenu de la page web par défaut du serveur Apache docker exec -it apache-server bash (1) cat htdocs/index.html | tr [:lower:] [:upper:] | tee ./htdocs/index.html (2) 1 On lance un shell (→ bash) intéractif (→ option -i) dans le container apache-server avec lequel on va intergair avec un terminal (→ option -t). On est placé automatiquement dans le répertoire /usr/local/apache2 où se trouve le contenu du site web 2 On convertit en majuscules tout le contenu du fichier index.html et on affiche aussi le résultat à l’écran Résultat : john@docker-lab:~$ docker exec -it apache-server bash (1) root@a8ab349c84cf:/usr/local/apache2# cat htdocs/index.html | tr [:lower:] [:upper:] | tee ./htdocs/index.html (2) <HTML><BODY><H1>IT WORKS!</H1></BODY></HTML> root@a8ab349c84cf:/usr/local/apache2# 1 On ouvre un shell dans le container 2 On saisit une séquence de commandes dans le shell du container pour convertir le contenu de index.html en majuscules (noter l’invite de commande qui indique l’utilisateur et l’identifiant du container) En rafraichissant la page web dans le navigateur, vous devez voir le message “IT WORKS” (plutôt que le “It works” du départ) Quitter le shell du container en tapant soit Ctrl+D soit la commande exit Stopper le container avec la commande docker stop <container-id> # ou docker stop <container-name> Dans notre cas : docker stop apache-server Résultat : john@docker-lab:~$ docker stop apache-server apache-server Lister tous les containers qu’il soient en cours d’exécution ou arrêtés : docker ps -a # ou docker container ls -a Résultat john@docker-lab:~$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a8ab349c84cf httpd:bookworm "httpd-foreground" 23 minutes ago Exited (0) About a minute ago apache-server (1) 1 On remarque dans la colonne STATUS que le container est “Exited” donc arrêté On va, à présent qu’il est arrêté, détruire le container que l’on a créé avec : docker rm apache-server (1) 1 On peut aussi fournir l’identifiant du container à la place de son nom Résultat : john@docker-lab:~$ docker rm apache-server apache-server Pour détruire tous les containers arrêtés sans avoir à donner leur nom, on peut saisir la commande : docker container prune On va à présent créer un volume Docker c’est-à-dire un emplacement sur la VM Debian hôte qui sera “mappé” dans le système de fichiers du container Ceci va nous permettre de mettre le code source d’un site web statique sur la VM Debian qui sera servi par le serveur web du container. Dans la VM Debian, créer un répertoire de travail my-static-website/ qui contiendra un sous-répertoire nommé par exemple htdocs/ (mais il peut porter un autre nom) mkdir -p my-static-website my-static-website/htdocs cd my-static-website Résultat john@docker-lab:~$ mkdir -p my-static-website my-static-website/htdocs john@docker-lab:~$ cd my-static-website john@docker-lab:~/my-static-website$ Cloner un exemple de site web statique glané sur Github dans le répertoire htdocs/ de la VM Debian, git clone https://github.com/devedium/SimpleClock.git htdocs/ Résultat john@docker-lab:~/my-static-website$ git clone https://github.com/devedium/SimpleClock.git htdocs/ Clonage dans 'htdocs'... remote: Enumerating objects: 6, done. remote: Counting objects: 100% (6/6), done. remote: Compressing objects: 100% (6/6), done. remote: Total 6 (delta 0), reused 6 (delta 0), pack-reused 0 Réception d'objets: 100% (6/6), fait. Relancer le container httpd:bookworm mais en mappant cette fois-ci le répertoire htdocs/ de la VM Debian sur le répertoire /usr/local/apache2/htdocs du container docker run -d --name apache-server -p 8080:80 -v ./htdocs/:/usr/local/apache2/htdocs/ httpd:bookworm (1) 1 C’est l’option -v qui permet de mapper les 2 répertoires l’un sur l’autre Résultat john@docker-lab:~/my-static-website$ docker run -d --name apache-server -p 8080:80 -v ./htdocs/:/usr/local/apache2/htdocs/ httpd:bookworm 048f36cd3a4153dc59ee3e609817aca649494f619c7bf92771a41b69ac0e1ee0 Rafraichir dans le navigateur la page web servie par le serveur web du container ⇒ Vous devez à présent voir une horloge qui se met à jour toutes les secondes Suite au “mappage” des volumes, le serveur web du container sert effectivement le site web contenu dans la VM Debian (→ /home/john/my-static-website/htdocs) et non celui présent à l’origine dans le container (→ /usr/local/apache2/htdocs) Stopper et détruire le container docker stop apache-server docker rm apache-server Résultat : john@docker-lab:~$ docker stop apache-server apache-server john@docker-lab:~$ docker rm apache-server apache-server 💻 Travail n° 2 Personnalisation d’un container Docker offre la possibilité de créer ses propres images par personnalisation d'images existantes. Vous allez à présent construire une nouvelle image Docker qui intègrera d’origine le site web statique qui affiche l’horloge plutôt que de devoir le “mapper” à travers un volume. Se déplacer dans le répertoire my-static-website de la VM Debian et y créer un fichier Dockerfile (en respectant la casse) Résultat : john@docker-lab:~$ cd my-static-website/;touch Dockerfile (1) 1 enchaînement de commandes qui se déplace dans le répertoire my-static-website puis crée le fichier Dockerfile L’ouvrir avec l’éditeur de texte nano et y copier le contenu suivant : FROM httpd:bookworm (1) COPY ./htdocs /usr/local/apache2/htdocs (2) 1 On spécifie l'image de départ 2 On indique les commandes à exécuter pour personnaliser l'image. Ici, on se contente de copier le contenu du site web statique présent dans le répertoire ./htdocs de la VM Debian vers le répertoire /usr/local/apache2/htdocs du container Construire la nouvelle image : docker build -t my-httpd . (1) 1 la commande s’appuie sur le fichier Dockerfile présent dans le répertoire courant pour créer une image qui s’appellera my-httpd (→ option -t, signifiant tag) Ne pas oublier le ‘.’ à la fin de la commande. Celui-ci spécifie le chemin où se trouve le Dockerfile ( ‘.’ signifie “répertoire courant”) Résultat : john@docker-lab:~/my-static-website$ cat Dockerfile FROM httpd:bookworm COPY ./htdocs /usr/local/apache2/htdocs john@docker-lab:~/my-static-website$ docker build -t my-httpd . [+] Building 0.3s (7/7) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 98B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/httpd:bookworm 0.0s => [internal] load build context 0.1s => => transferring context: 31.68kB 0.0s => CACHED [1/2] FROM docker.io/library/httpd:bookworm 0.0s => [2/2] COPY ./htdocs /usr/local/apache2/htdocs 0.1s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:9f850c7081bd546d70bd8f75a2eccac19a2bcb972b0440485ef539c56b71bf40 0.0s => => naming to docker.io/library/my-httpd Constater la présence d’une nouvelle image avec : docker images Résultat : john@docker-lab:~/my-static-website$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE my-httpd latest 9f850c7081bd 51 seconds ago 168MB (1) php apache-bookworm aa1a7b18157c 12 days ago 503MB httpd bookworm 359570977af2 3 weeks ago 168MB mysql latest c138801544a9 2 months ago 577MB hello-world latest 9c7a54a9a43c 5 months ago 13.3kB 1 La nouvelle image créée apparait bien dans la liste des images disponibles Lancer le container , sans mettre en place de volume, et constater que l’horloge s’affiche bien dans le navigateur après avoir rafraîchi la page : docker run -d --name my-static-website -p 8080:80 my-httpd Ouvrir un terminal dans le container et s’assurer que le code source du site web de l’horloge figure bien dans le répertoire /usr/local/apache2/htdocs docker exec -it my-static-website bash ls htdocs Résultat : john@docker-lab:~/my-static-website$ docker run -d --name my-static-website -p 8080:80 my-httpd b7d06abcbd7a8ad42c6985bb2979446310c3363a9d2eaf6d3b7243e6e2dc0c63 john@docker-lab:~/my-static-website$ docker exec -it my-static-website bash root@b7d06abcbd7a:/usr/local/apache2# ls htdocs/ README.md index.html site.css site.js (1) 1 le répertoire /usr/local/apache2/htdocs/ du container contient bien les fichiers du site web qui affiche l’horloge Quitter le shell du container avec Ctrl+D ou la commande exit Stopper le container, le détruire ainsi que l'image personnalisée à partir de laquelle il a été créé docker stop my-static-website docker rm my-static-website docker rmi my-httpd (1) 1 C’est cette commande qui détruit l’image personnalisée Résultat john@docker-lab:~$ docker stop my-static-website my-static-website john@docker-lab:~$ docker rm my-static-website my-static-website john@docker-lab:~$ docker rmi my-httpd Untagged: my-httpd:latest Deleted: sha256:9f850c7081bd546d70bd8f75a2eccac19a2bcb972b0440485ef539c56b71bf40 john@docker-lab:~$ docker images (1) REPOSITORY TAG IMAGE ID CREATED SIZE php apache-bookworm aa1a7b18157c 12 days ago 503MB httpd bookworm 359570977af2 3 weeks ago 168MB mysql latest c138801544a9 2 months ago 577MB hello-world latest 9c7a54a9a43c 5 months ago 13.3kB 1 L'image Docker my-httpd n’est effectivement plus présente 💻 Travail n° 3 Site web dynamique Contrairement au 💻 Travail n° 1 Site web statique, vous allez ici mettre en œuvre une solution non pas avec 1 mais 2 containers Docker : le 1er container contiendra un serveur web Apache disposant d’un moteur PHP lui permettant d’interpréter les pages web incluant des instructions en langage PHP le 2ème proposera un serveur de base de données MySQL qui hébergera une base de données dans laquelle le site web viendra piocher ou écrire des données. L'“orchestration” de ces 2 containers se fera depuis l’outil Docker Compose qui constitue une solution “simple” de mise en place d’application multi-containers L’application multi-containers envisagée dans ce travail se résumera à une simple interface web de connexion à un site web fictif. Le code source de cette application sera récupérée sur Github (→ Registration-Login-and-Crud-in-PHP Public ) L’application propose de s’inscrire sur le site web ou de s’y connecter. Ces 2 opérations occasionneront une consultation de la base de données (→ procédure de connexion) et une écriture dans celle-ci (→ procédure d’inscription). Créer dans la VM un répertoire my-lamp-website et s’y déplacer. Ce répertoire contiendra lui-même les sous-répertoires app/, build/, build/mysql/ et build/php/ Résultat : john@docker-lab:~$ mkdir my-lamp-website; cd $_ (1) john@docker-lab:~/my-lamp-website$ mkdir -p app/ build/mysql build/php john@docker-lab:~/my-lamp-website$ 1 Noter l’astuce qui consiste à utiliser la variable $_ pour se rendre dans le répertoire qui a été créé par la commande précédente L’abréviation lamp présente dans le nom du répertoire de travail est couramment utilisée pour désigner un système Linux / Apache / MySQL / PHP. Créer un fichier docker-compose.yml et y ajouter le contenu suivant : docker-compose.yml version: "3.8" services: php: ports: - "8080:80" build: context: './build/php' container_name: 'my-php-server' volumes: - ./app:/var/www/html mysql: ports: - "3306:3306" build: context: './build/mysql' container_name: 'my-mysql-server' environment: MYSQL_ROOT_PASSWORD: "yesUcan" MYSQL_DATABASE: "regdb" volumes: - ./app/db:/tmp/app-db volumes: app: regdb: Ce fichier décrit les containers (→ services) à mettre en place : ports, nom, volumes… Personnaliser l'image Docker qui servira pour créer le container contenant le serveur Apache et le moteur PHP en créant un fichier Dockerfile dans ./build/php/. Y ajouter le contenu suivant : ./build/php/Dockerfile FROM php:apache-bookworm RUN apt-get update && \ docker-php-ext-install mysqli pdo pdo_mysql (1) 1 télécharger et installer les modules PHP permettant d’interagir avec les bases de données MySQL Personnaliser l'image Docker qui servira pour créer le container contenant le serveur MySQL en créant un fichier Dockerfile dans ./build/mysql/. Y ajouter le contenu suivant : ./build/mysql/Dockerfile FROM mysql:latest USER root RUN chmod 755 /var/lib/mysql (1) 1 Changer les droits de /var/lib/mysql pour permettre à PHP de s’y connecter Lancer les containers avec la commande docker compose up La commande va construire les images Docker et les personnaliser. Ceci va mener au téléchargement de quelques fichiers et une procédure de lancement qui occasionne l’affichage d’un ensemble de messages y compris d’avertissements. Ne pas en tenir compte. Résultat john@docker-lab:~/my-lamp-website$ docker compose up [+] Building 0.0s (4/4) FINISHED docker:default => [php internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 2B 0.0s => [php internal] load .dockerignore 0.0s => => transferring context: 2B [...] (1) my-mysql-server | 2023-10-12T13:21:02.251098Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock my-mysql-server | 2023-10-12T13:21:02.251199Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.1.0' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL. Dupliquer PuTTY afin d’avoir une 2ème console dans laquelle vous allez pouvoir saisir des commandes : la 1ère console ne vous a effectivement pas rendu la main car elle affiche des messages d’informations sur les services des containers. Vous allez dans un 1er temps vous assurer que le container Apache/PHP fonctionne correctement en vérifiant qu’il arrive à servir une page web simplissime construite avec PHP. Créer dans le répertoire ./app de la VM Debian un fichier index.php avec le contenu suivant : ./app/index.php <?php echo 'Bonjour à toi, jeune padawan !'; Constater que la page web s’affiche lorsqu’on se rend sur l’URL du site (→ http://<adresse-ip-vm-debian>:8080) Détruire le répertoire app/ pour le remplacer par le code source PHP/HTML/JS/CSS du site proposant l’interface d’inscription/connexion à un site web. Ce code source est récupéré d’un dépôt Github rm -rf app/ git clone https://github.com/mohibulkhan786/Registration-Login-and-Crud-in-PHP.git ./app Résultat : john@docker-lab:~/my-lamp-website$ rm -Rf app/ john@docker-lab:~/my-lamp-website$ git clone https://github.com/mohibulkhan786/Registration-Login-and-Crud-in-PHP.git ./app Clonage dans './app'... remote: Enumerating objects: 53, done. remote: Counting objects: 100% (53/53), done. remote: Compressing objects: 100% (47/47), done. remote: Total 53 (delta 3), reused 53 (delta 3), pack-reused 0 Réception d'objets: 100% (53/53), 1.12 Mio | 2.19 Mio/s, fait. Résolution des deltas: 100% (3/3), fait. Se connecter au serveur MySQL depuis un client lancé depuis le shell du container puis : vérifier la présence d’une base de données nommée regdb (créée à partir des instructions du fichier docker-compose.yml → MYSQL_DATABASE) créer les tables de cette base de données et les renseigner avec quelques données de départ grâce aux instructions SQL se trouvant dans le fichier ./app/db/db_php_crud.sql docker exec -ti my-mysql-server bash (1) mysql -uroot -pyesUcan (2) 1 Lancer un shell dans le container 2 lancer le client mysql en fournissant les identifiants renseignés dans le fichier docker-compose.yml (→ MYSQL_ROOT_PASSWORD) Dans le client mysql, saisir ensuite les commandes : show databases; (1) use regdb (2) source /tmp/app-db/db_php_crud.sql (3) 1 liste les bases de données gérées par MySQL 2 sélectionne la base de données regdb 3 joue les commandes SQL présentes dans /tmp/app-db/db_php_crud.sql pour intialiser et pré-remplir les tables de la base de données regdb Résultat : john@docker-lab:~/my-lamp-website$ docker exec -ti my-mysql-server bash bash-4.4# mysql -uroot -pyesUcan mysql: [Warning] Using a password on the command line interface can be insecure. [...] Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show databases; (1) +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | regdb | (2) | sys | +--------------------+ 5 rows in set (0.01 sec) mysql> use regdb (3) Database changed mysql> source /tmp/app-db/db_php_crud.sql (4) Query OK, 0 rows affected (0.00 sec) Query OK, 0 rows affected (0.00 sec) [...] Query OK, 0 rows affected (0.00 sec) mysql> exit (5) Bye bash-4.4# exit john@docker-lab:~/my-lamp-website$ 1 On liste les bases de données 2 La base de données regdb est bien présente 3 On sélectionne la base de données regdb 4 On crée les tables de la bdd et on les pré-remplit 5 On quitte le client mysql Modifier enfin le fichier ./app/config.php pour y mettre à jour les informations de connexion à la base de données Remplacer : $con=mysqli_connect("localhost", "root", "", "db_php_crud"); Par : $con=mysqli_connect("my-mysql-server", "root", "yesUcan", "regdb"); Rafraîchir la page du navigateur et constater qu’un formulaire d’inscription s’affiche et qu’il est possible de s’inscrire puis de se connecter au site On peut alors vérifier que les informations saisies se retrouvent dans la base de données regdb Résultat : john@docker-lab:~/my-lamp-website$ docker exec -it my-mysql-server bash (1) bash-4.4# mysql -uroot -pyesUcan (2) [...] mysql> use regdb (3) Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed mysql> show tables; (4) +-----------------+ | Tables_in_regdb | +-----------------+ | tbluser | | tblusers | +-----------------+ 2 rows in set (0.00 sec) mysql> SELECT * FROM tbluser; (5) +----+-----------------+--------------+--------------------------+----------------------------------+---------------------+ | ID | FullName | MobileNumber | Email | Password | RegDate | +----+-----------------+--------------+--------------------------+----------------------------------+---------------------+ | 5 | Armaan khan | 7007192298 | armaan@gmail.com | bc11f06afb9b27070673471a23ecc6a9 | 2021-04-24 15:58:30 | | 6 | mkhan | 7007192297 | mkhan15992@gmail.com | bc11f06afb9b27070673471a23ecc6a9 | 2021-04-24 17:30:03 | | 7 | aaaaa | 1111111111 | a@gmail.cm | 3dbe00a167653a1aaee01d93e77e730e | 2021-04-25 18:20:59 | | 8 | John DOE | 612345678 | john.doe@acme.com | 527bd5b5d689e2c32ae974c6229ff785 | 2023-10-12 15:11:27 | (6) | 9 | Gérard MANVUSSA | 687654321 | gerard.manvussa@acme.com | 64d8be661d8a79416eb6662db51e7118 | 2023-10-12 15:17:37 | (6) +----+-----------------+--------------+--------------------------+----------------------------------+---------------------+ 5 rows in set (0.00 sec) mysql> exit (7) Bye bash-4.4# exit (8) exit john@docker-lab:~/my-lamp-website$ 1 On lance un shell dans le container MySQL 2 On exécute le client MySQL 3 On sélectionne la base de données regdb 4 On liste les tables de de la base de données regdb 5 On exécute une requête SQL qui affiche le contenu de la table tbluser 6 On vérifie de bien retrouver les utilisateurs que l’on a inscrits 7 On quitte le client mysql 8 On quitte le shell du container Conclusion Dans cette activité, vous avez déployé des containers Docker pour mettre en place : un site web statique ne nécessitant qu’un seul container un site web dynamique reposant sur l’utilisation de 2 containers Par le biais des personnalisations des images Docker, le site web dynamique aurait très bien pu ne reposer que sur un seul container. Sur le terrain, ce choix doit être pris par l’architecte de l’application en fonction des contraintes qui lui sont imposées par le client (performance, occupation mémoire, sécurité…). Cette activité a illustré mais n’est pas entré dans les détails de la communication entre containers. Il y aurait eu trop de choses à dire … Docker est outil puissant dont vous n’avez survolé que quelques possibilités. Docker n’est pas la seule application à proposer la “conteneurisation” d’applications (ex. Apache Mesos). C’est une technologie plutôt récente qui a été largement adoptée dans les entreprises. Il est donc nécessaire d’en avoir connaissance et de savoir, même partiellement, la mettre en œuvre. 🞄 🞄 🞄 Docker Windows