Docker et Docker Compose


Table of Contents


Cette page propose un ensemble d’exercices pour découvrir Docker et Docker Compose.

Tout au long des exercices, des questions vous sont posées dans des cadres similaires 
à celui-ci. Essayer de répondre à ces questions doit vous permettre de mieux comprendre le 
fonctionnement de Docker.

Installation

Si vous utilisez une VM provisionnée dans le cloud, vous pouvez ignorer cette étape. Tous les logiciels sont déjà installés sur les VMs.

Installation de Docker sur une machine Linux

To install Docker, run the following commands (the last one may take some time to complete):

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

Then, to be able to start Docker containers as a normal user, run:

sudo usermod -aG docker [USER-ID]

[USER-ID] should be replaced by your user id on the VM (e.g., my_user_account_gmail_com)

Finally, for the changes to take effect, you should log out from the VM (i.e., close the SSH session) and log in again.

To verify that your Docker installation is working properly, simply run:

docker run hello-world

You are ready to play with Docker.

Installation de Docker Compose

In most cases, the previous script already installs the docker compose plugin.

To verify if docker compose is already installed, run:

docker compose version

The instructions to install docker-compose are available here: https://docs.docker.com/compose/install/. We recommend that you install the plugin manually


Premiers pas avec Docker

Premier conteneur

Comme recommandé dans la documentation docker, nous vous suggérons de tester si le docker engine fonctionne correctement en essayant de démarrer le conteneur hello-world, qui affiche simplement un message dans le terminal.

docker run hello-world
- Observez ce qu'il s'est passé lors de l'exécution de la commande `docker run`

On peut observer les conteneurs en cours d’exécution avec la commande suivante:

docker container ls

Ici, on ne voit aucun conteneur en cours d’exécution. Le conteneur se termine dès que la commande par défaut du conteneur a été exécutée.

Pour afficher les conteneurs terminés:

docker container ls -a
Quels sont les 2 identifiants associés au conteneur que nous avions créé?

Affichons ensuite les images docker présentes sur la machine:

docker image ls

On peut voir que hello-world est la seule image présente et elle est très petite.

D'après ce que nous savons sur la construction d'images, plusieurs images 
devraient être présentes localement. Pourquoi n'est ce pas le cas?

Nous pouvons supprimer l’image hello-world, qui ne sera pas utile pour la suite:

docker image rm hello-world:latest

Nous ne pouvons pour le moment pas supprimer l’image car un conteneur la référence toujours, ainsi il faut commencer par supprimer le conteneur:

docker container rm [CONTAINER_ID]
docker image rm hello-world:latest

Manipuler des conteneurs

Pour apprendre comment manipuler des conteneurs, nous vous proposons de suivre les manipulations proposées par Jérôme Petazzoni dans ses tutoriels.

Quelques exercices pour tester nos connaissances

Observer et manipuler un conteneur en cours d’exécution

Pour observer un conteneur en cours d’exécution, nous allons démarrer un nouveau conteneur à l’aide de la commande suivante:

docker run -d --name looper ubuntu:16.04 sh -c 'while true; do date; sleep 1; done'

Ce conteneur appelé looper exécute la commande date toute les secondes.

Effectuez les operations suivantes:
1. Vérifiez que le conteneur est en cours d'exécution
2. Observez en *continu* les logs générés par le conteneur
3. Mettez en pause l'exécution du conteneur et observez le résultat dans les logs
4. Débloquez l'exécution du conteneur et observez le résultat dans les logs
5. Tout en conservant un terminal qui affiche les logs, ouvrez un nouveau terminal 
   dans lequel vous viendrez vous attacher au conteneur en cours d'exécution.
   (utiliser *docker attach*)
6. Exécutez *CRTL+C* (signal *SIGINT*) pour vous détacher du conteneur et observez 
   ce qu'il se passe
7. Utilisez la commande *docker exec* pour créer un fichier au sein du conteneur
   en cours d'exécution (*touch fichier.txt*)
   - *docker exec -d [CONTAINER_ID] touch fichier.txt*
8. Connectez vous au conteneur en cours d'exécution et vérifiez que le fichier a 
   bien été créé
   - *docker exec -it [CONTAINER_ID] bash*

Arrêtez et supprimez le conteneur looper avant de passer à l’exercice suivant.

Pour supprimer tous les conteneurs arrétés sur votre machine, vous pouvez utiliser la commande:

docker container prune

Construire des images Docker

Premières images

Nous allons maintenant nous intéresser à une autre image célèbre: docker/whalesay

Nous pouvons exécuter un conteneur comme suit:

docker run docker/whalesay cowsay boo

Pour vérifier la popularité de l’image docker/whalesay et voir si de nombreuses images mettant en oeuvre des baleines parlantes existent, nous pouvons exécuter:

docker search whalesay

L’image docker docker/whalesay est un peu frustrante car elle exige à chaque exécution de préciser le message à afficher.

Nous aimerions que dans le cas où l’utilisateur ne donne aucune commande au moment de l’exécution du conteneur, un message par défaut soit affiché par la baleine.

Nous allons construire une nouvelle image pour cela à partir du Dockerfile suivant :

FROM docker/whalesay

CMD cowsay meeuuuuh
Expliquez le contenu de ce Dockerfile.

Avant de créer une nouvelle image, il est important de créer un nouveau répertoire dans lequel seront stockés le Dockerfile ainsi que tous les fichiers permettant de créer l’image. Ce répertoire sera le contexte utilisé pour la création de l’image:

mkdir whalesay
cd whalesay
nano Dockerfile #éditer le fichier

Nous pouvons maintenant créer la nouvelle image en lui donnant un nom (mywhalesay)

docker build -t mywhalesay .
Observez et essayez d'expliquer les messages générés par cette commande

On peut ensuite vérifier que la nouvelle image se comporte comme attendu:

docker run mywhalesay

Ensuite nous voulons créer une nouvelle image encore plus évoluée, dans laquelle nous allons installer et utiliser l’outil fortune pour générer des messages à afficher.

Le Dockerfile pour cette nouvelle étape est le suivant:

FROM docker/whalesay:latest

RUN apt-get -y update
RUN apt-get install -y fortunes

CMD /usr/games/fortune -a | cowsay
Créez une nouvelle image *mywhalesay2* et testez la nouvelle image

Nous pouvons aussi observer l’historique de la nouvelle image créée:

docker history mywhalesay2
- Combien de couches ont été ajoutées au dessus de `docker/whalesay` pour construire
  la nouvelle image?
- A quoi correspond chacune de ces couches?

Copie de fichiers dans une image

Sur le même principe que l’exercice précédent, nous voulons maintenant que le message par défaut affiché lors de l’exécution du conteneur provienne d’un fichier hello.txt copié dans l’image au moment de sa création.

Ainsi, voici le nouveau Dockerfile que nous allons utiliser.

FROM docker/whalesay:latest

COPY hello.txt default_msg.txt

RUN apt-get -y update
RUN apt-get install -y fortunes

CMD cat default_msg.txt | cowsay
Créez un fichier `hello.txt` avec le contenu de choix et construisez la nouvelle image

En supposant que votre nouvelle image s’appelle mywhalesay-msg, obtenez un shell interactif dans le nouveau conteneur créé pour vérifier que le fichier a été copié :

docker run -it mywhalesay-msg bash
1. Exécutez le conteneur avec la commande par défaut pour vérifier que tout 
   fonctionne correctement
2. Modifiez le fichier hello.txt:
   - Est ce que si vous ré-éxécutez le conteneur, les changements vont avoir 
     été pris en compte? Pourquoi?
3. Reconstruisez l'image après la modificiation de hello.txt
   - Est ce que si vous ré-éxécutez le conteneur, les changements vont avoir 
     été pris en compte? Pourquoi?
4. Qu'observez vous lors du processus de recontruction de l'image après avoir 
   modifié hello.txt?
   - Est ce que vous pensez que le fichier `Dockerfile` pourrait être amélioré?
   - Testez.

CMD vs ENTRYPOINT

Youtube-dl est un outil à la ligne de commande pour télécharger des vidéos youtube.

!!! L’outil Youtube-dl n’existe plus!!!. Essayez de répondre aux questions de l’exercice même si vous ne pouvez pas tester.

Dans cet exercice, nous voulons créer une image nous permettant de télécharger des vidéos à l’aide de cet outil.

Voici le Dockerfile que nous vous proposons:

FROM ubuntu:18.04

WORKDIR /mydir

RUN apt-get update && apt-get install -y curl python
RUN curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
RUN chmod a+x /usr/local/bin/youtube-dl
ENV LC_ALL=C.UTF-8
CMD ["/usr/local/bin/youtube-dl"]

L’objectif est d’être capable de télécharger une vidéo en exécutant une commande telle que la suivante:

docker run [MY_IMAGE_NAME] https://www.youtube.com/watch?v=G8uIqWhVKGg
1. Créez une image à partir du Dockerfile décrit ci-dessus et exécutez un conteneur 
   avec la commande proposée.
   - Quel est le problème?
2. Modifiez le Dockerfile pour résoudre le problème et testez

Débugger une image

Lors de la création d’une nouvelle image, il est parfois nécessaire de se connecter de manière interactive au conteneur créé pour essayer de comprendre et corriger les problèmes de configuration au sein de l’image.

Après avoir effectué des modifications au sein d’un conteneur pour corriger un problème, il peut être utile:

Nous allons essayer de faire ce type de manipulations. Voici notre Dockerfile de départ :

FROM ubuntu

CMD echo "hello" > /workdir/hello.txt

L’exécution d’un conteneur à partir de cette image est supposée créer le fichier hello.txt dans le répertoire /workdir du conteneur. Cet image ne serait pas très utile en pratique mais est suffisante pour illustrer notre point.

1. Construisez une image à partir du Dockerfile et testez pour constater l'erreur
2. Créez un conteneur à partir de l'image dans lequel vous obtiendrez un shell interactif
3. Corrigez l'erreur (cad créer le répertoire manquant) et déconnectez vous de l'image
4. Observez les changements à l'aide de *docker diff*
5. Créez une nouvelle image à partir de vos changements à l'aide de docker commit
   - Attention, par défaut docker commit écrase la commande (CMD) définie pour l'image
     précédente et la remplace par la commande executée au démarrage de l'exécution intéractive.
   - La syntaxe suivante doit être utilisée pour s'assurer d'avoir la bonne *CMD* définie 
     pour la nouvelle image
   - docker commit --change='CMD echo "hello" > /workdir/hello.txt' \
     [CONTAINER_ID] [NEW_IMAGE_NAME]
6. Démarrez un nouveau conteneur pour vérifier que vos modifications ont bien été 
   prises en compte

Veuillez noter que cette solution fondée sur docker commit pour créer une nouvelle image ne doit être utilisée que lors de la phase de debug. Une fois l’image fonctionnelle obtenue, il faut modifier le Dockerfile en conséquence pour documenter la bonne manière de créer l’image.

Build multi-stages

Voici un fichier hello.c:

#include <stdio.h>
int main () {
  puts("Hello, world!");
  return 0;
}

Voici un Dockerfile permettant de créer une image où le fichier sera copié, compilé et exécuté:

FROM ubuntu
RUN apt-get update
RUN apt-get install -y build-essential
COPY hello.c /
RUN make hello
CMD /hello
1. Construisez une image à partir de ce Dockerfile et testez
2. Observez l'historique de l'image que vous avez construite
   - Quel est le principal défaut? (indice: consulter l'historique de l'image)

Voici une nouvelle version du Dockerfile, basé sur l’idée de build multi-stages qui permet de ne conserver que ce qui est réellement utile dans l’image finale.

FROM ubuntu AS compiler
RUN apt-get update
RUN apt-get install -y build-essential
COPY hello.c /
RUN make hello
FROM ubuntu
COPY --from=compiler /hello /hello
CMD /hello
1. Expliquez le fonctionnement de ce Dockerfile
2. Construisez et testez la nouvelle image
3. Observez l'historique de la nouvelle image

La gestion du réseau

Déployer un serveur web

Dans cette exercice, nous allons utiliser l’image docker us-docker.pkg.dev/google-samples/containers/gke/hello-app:1.0 qui contient un serveur web simple écoutant sur le port 8080.

Lancez un conteneur à partir de cette image, comme suit:

docker run us-docker.pkg.dev/google-samples/containers/gke/hello-app:1.0
1. Testez si il est possible d'accéder au serveur en éxécutant une requète *curl* 
   sur le port 8080 depuis la machine où s'exécute le serveur.
   - *curl localhost:8080*
2. Exécutez la commande *docker ps* pour comprendre ce qu'il se passe
3. Relancez votre serveur en vous assurant cette fois que le port 8080 du conteneur
   est associé au port 7000 de la machine hote.
   - Testez avec *curl localhost:7000*

Nous pouvons observer différentes informations sur le conteneur que nous avons démarré.

docker ps
docker port <containerID> 8080
docker inspect --format '{{ .NetworkSettings.IPAddress }}' <yourContainerID>
- A quoi correspond cette adresse IP?
- Depuis la machine sur laquelle s'exécute le conteneur, essayez d'envoyer une requête 
  au serveur en utilisant l'adresse IP du conteneur:
  - En utilisant le port 7000
  - En utilisant le port 8080
- Expliquez le résultat

Observer le fonctionnement du réseau

Pour comprendre le fonctionnement du réseau avec Docker, nous vous proposons de suivre le tutoriel disponible sur le site de Docker, qui présente un ensemble d’opérations pour bien comprendre le notion de bridge: https://docs.docker.com/network/network-tutorial-standalone/


La gestion des données

Les bind mounts

Dans cet exercice, nous considérons une image construite à partir du Dockerfile suivant:

FROM ubuntu

WORKDIR /workdir

CMD echo "hello" > /workdir/hello.txt

dont la commande écrit un fichier hello.txt dans le répertoire /workdir

Nous aimerions que la fichier généré soit accessible sur la machine hôte après l’exécution du conteneur, par exemple dans un répertoire /tmp/test_docker

Ceci se fait de la manière suivante:

docker run --mount type=bind,source=/tmp/test_docker,target=/workdir <nom_image>

Exécutez le conteneur avec les bons paramêtres et vérifiez que tout fonctionne correctement.

Pour observer la configuration que vous avez créé, vous pouvez inspecter le conteneur et en particulier la section Mounts avec la commande suivante:

docker container inspect <yourContainerID>
Considérez l'image basée sur *Youtube-dl* que vous avez construite dans un exercice précédent.
1. Exécutez cette image de telle manière que la vidéo téléchargée soit accessible 
   depuis la machine hôte

Les volumes

Nous voulons maintenant refaire le même exercice que précédemment, cette fois-ci, en utilisant un volume de données (data volume). Nous allons réutiliser la même image docker, cependant cette fois ci, à l’exécution du conteneur, nous voulons stocker le fichier généré dans un volume de données.

docker run --mount source=testvol,target=/workdir <nom_image>

Avant de démarrer le conteneur, listez les volumes existants sur votre machine:

docker volume ls

Refaites la même chose après avoir exécuté le conteneur pour vérifier q’un nouveau volume a bien été créé.

Pour observer le contenu du volume, nous devons tout d’abord récupérer le chemin vers le répertoire dans lequel le volume est stocké:

docker volume inspect volumeName
Vérifiez que tout fonctionne comme attendu

Un exercice en plus

Pour ce test, nous allons utiliser un serveur NGINX. L’image officielle NGINX est simplement appelée nginx, comme décrit ici

Voici les informations principales par rapport à cette image:

Nous voulons publier un fichier index.html avec le contenu suivant: Vive Grenoble !!!.

Sans créer de nouvelle image, démarrer un serveur  `NGINX` de telle manière à ce que:
- Il soit accessible sur le port 7000 de la machine
- Il renvoie le contenu souhaité

Astuce: Pour donner le chemin absolu d’un répertoire contenu dans le répertoire courant, vous pouvez utiliser la syntaxe "$(pwd)"/<yourDir>.

Un exercice un peu plus dur

Dans le test suivant, nous allons créer notre propre image de serveur NGINX. Puis nous allons créer un deuxième conteneur dont le role sera de superviser le nombre de connexions à notre serveur. Enfin, un troisième conteneur sera chargé d’envoyer une requète au serveur web.

Pour créer notre image de serveur NGINX, nous allons utiliser le Dockerfile suivant:

FROM ubuntu:20.04

RUN apt-get update
RUN apt-get install -y nginx

RUN echo 'Bonjour Grenoble' > /var/www/html/index.html

EXPOSE 80

Le conteneur devra être lancé avec la commande suivante: nginx -g "daemon off;"

Notre serveur NGINX génère un fichier de logs access.log dans lequel est enregistrée une trace de chaque requête reçue. Le fichier access.log est stocké par défaut dans le répertoire /var/log/nginx/.

Le conteneur en charge de la supervision devra exécuter le script suivant:

#!/bin/bash

while true
do
    cat /servlog/access.log |wc -l
    sleep 10
done

Pour créer le conteneur en charge d’envoyer une requête au serveur, nous allons utiliser l’image radial/busyboxplus:curl. La commande curl à exécuter sera définie à l’exécution du conteneur.

Pour cet exercice, vous devez mettre en place l’application mutli-conteneurs suivante:

Créez et déployez cette application manuellement

Docker Compose

Les premières compositions

Docker Compose nous permet de décrire la manière de déployer une application dans un fichier docker-compose.yml

Ici, nous allons réprendre certains des exercices que nous avons faits précédemment et déployer automatiquement les conteneurs à l’aide d’une composition.

Une composition simple

Pour cette première composition, nous considérons l’exercice Déployer un serveur web.

Ecrire la composition permettant de déployer le conteneur comme dans l'exercice original
avec la bonne redirection de ports.

Vous pouvez prendre l’exemple suivant venant de la documentation officiele comme point de départ: Define services in a Compose file

Vérifier que tout fonctionne correctement.

D’autres compositions

Dans les exercices suivants, l’objectif est à chaque fois de construire une composition équivalente à l’exercice original:

Contrôler le nombre de réplicas d’un service

Pour découvrir de nouvelles fonctionnalités de docker compose, nous allons manipuler un service web.

Le service Web jwilder/whoami est un service très simple qui retourne l’identifiant du conteneur dans lequel il s’exécute. Vous pouvez démarrer ce conteneur à l’aide de la commande suivante:

docker run -d -p 7000:8000 jwilder/whoami

Vous pouvez ensuite envoyer des requêtes au service démarrer en utilisant curl:

curl localhost:7000

Supprimez ce premier conteneur avant de passer à la suite.

Pour créer notre service web à l’aide de docker compose, nous pouvons utiliser le fichier docker-compose.yml suivant:

version: '3'

services: 
    whoami: 
      image: jwilder/whoami 
      ports: 
        - 7000:8000

Utilisez la commande docker compose up -d pour démarrer la composition et vérifiez que tout fonctionne.

Dans la suite, nous allons étudier la capacité de docker compose à créer plusieurs instances d’un même service. Pour cela, nous allons utiliser l’option --scale de la commande docker compose up.

1. Exécutez la commande suivante pour démarrer 3 instances du service
   - *docker compose up -d --scale whoami=3*
2. La commande échoue. Expliquez pourquoi?
3. Modifiez le fichier *docker-compose.yml* pour corriger le problème
   - Voir https://docs.docker.com/compose/compose-file/#ports sur comment laisser 
     *docker compose* choisir le numéro de port sur l'hôte.
4. Une fois les différentes instances du service créées, vous pouvez obtenir leur 
   numéro de port avec la commande suivante: *docker compose port --index 1 whoami 8000*

En pratique, utiliser docker-compose pour démarrer plusieurs instances d’un même service sur une même machine a peu d’intérêt. Des solutions telles que Kubernetes ou Docker Swarm, qui fonctionnent sur le même modèle que docker-compose, permettent de déployer des services sur plusieurs hôtes.

Des exercices en plus

Pour continuer à s’exercer: