7. Créer une image Docker avec le Dockerfile - Docker

Comme Docker est un outil qui permet de "packager" un conteneur avec toutes ses dépendances et y inclure ce que l'on souhaite dans une image, nous allons voir de quoi est composée celle-ci pour y inclure dans notre exemple, des modules Python supplémentaires.

Les Layers

Pour construire une image Docker, il faut fournir des instructions dans un fichier nommé Dockerfile.

Chaque instruction ajoute une couche (ou layer) à l'image Docker.

Dans ce schéma, nous pouvons voir plusieurs lignes qui correspondent à chaque layer de l'image Docker. Chaque layer occupe un certain espace disque, et lui est attribué un hash.

Chacune des instructions ( FROM, RUN, COPY CMD etc...) correspond à une étape dans la construction de l'image.
Ces étapes correspondent par exemple à la copie d'un fichier, l'exécution d'une commande etc...

Le Dockerfile

Toujours dans notre exemple avec l'image Docker Python, j'ai un nouveau besoin, celui d'avoir le module Python requests de manière permanente dans mon image.
C'est-à-dire que lorsque je lancerai mon conteneur Python, si je le supprime et que je le recrée, même dans un nouvel environnement, le module Python requests devra être contenu dedans.

Plutôt que de lancer un conteneur Python, et d'exécuter la commande pip install requests à chaque lancement du conteneur, nous allons construire notre propre image Docker à partir de l'image Python.

Notre script main.py correspond à ceci :

import requests
import json

url = "https://jsonplaceholder.typicode.com/posts"

data = {
    "title": "foo",
    "body": "bar",
    "userId": 1
}

headers = {
    "Content-type": "application/json; charset=UTF-8"
}

response = requests.post(url, data=json.dumps(data), headers=headers)

print(response.json())

Pour la construction de notre image, nous allons créer un fichier nommé Dockerfile et ajouter cette première ligne :

FROM python

L'instruction FROM est la première ligne qui doit figurer obligatoirement dans votre Dockerfile. Elle indique à partir de quelle autre image Docker nous allons nous baser.
Cela va donc effectuer un docker pull de l'image mentionnée. Je n'ai pas précisé de tag, ce sera donc l'image python:latest qui sera utilisée.

Installons maintenant le module Python requests en ajoutant l'instruction RUN dans notre Dockerfile :

RUN pip install requests

L'instruction RUN exécute des commandes nécessaires à la construction de l'image. C'est comme si vous étiez connecté au conteneur avec un Shell et que vous exécutiez des commandes.

Notre image Python contient maintenant le module requests. Il est possible de passer à la suite, mais avant cela, regardons ensemble quelques instructions essentielles à la construction d'une image Docker.

Créons un utilisateur pour notre image car par défaut, c'est l'utilisateur root qui est utilisé. Ajoutons une deuxième instruction RUN avec la commande useradd comme ceci :

RUN useradd -d /home/python-user -m -s /bin/bash python-user

Définissons le comme utilisateur par défaut afin qu'il exécute nos instructions. Ajoutons l'instruction USER dans notre fichier Dockerfile comme ceci :

USER python-user

Précisons le répertoire courant par défaut dans lequel notre image se trouvera. Ajoutons l'instruction WORKDIR dans notre Dockerfile :

WORKDIR /python-user

Copions notre script main.py pour qu'il soit présent dans notre conteneur. C'est l'instruction COPY que nous allons ajouter à notre Dockerfile en précisant le répertoire cible :

COPY main.py /python-user

Maintenant que le script Python est contenu dans notre image, ajoutons l'instruction CMD pour spécifier la commande par défaut qui sera exécutée dans notre image. Dans notre cas, cette instruction servira plutôt d'argument à notre commande par défaut.

CMD ["main.py"]

Nous allons coupler cela avec l'instruction ENTRYPOINT Cela correspond à la commande qui sera exécutée constamment au démarrage du conteneur.
L'ENTRYPOINT par défaut de l'image Python est la commande python3, et comme nous avons spécifié la commande a exécuter, ce sera notre script main.py qui sera exécuté.

ENTRYPOINT ["python3"]

Le Build

Nous avons maintenant un Dockerfile qui correspond à ceci :

FROM python

RUN pip install requests

RUN useradd -d /home/python-user -m -s /bin/bash python-user

USER python-user

WORKDIR /home/python-user    

COPY main.py /home/python-user    

CMD ["main.py"]

ENTRYPOINT ["python3"]

Maintenant que nous avons défini toutes les instructions nécessaires à la construction de notre image, place au build.

Nous allons utiliser la commande docker build afin de construire une image Docker à partir d'un Dockerfile. Pour ce faire, plaçons nous dans le répertoire contenant notre Dockerfile, et lançons la commande suivante :

docker build -t python-requests:1.0.0 .
  • -t : Spécifie l'utilisation d'un tag Docker.
  • python-requests : Nom de notre image
  • 1.0.0 : Tag de l'image. Nous utilisons ici le Semantic Versioning (SemVer).
  • . : Correspond au contexte, à l'emplacement de notre Dockerfile.

A l'exécution de cette commande, vous verrez chacun des layers se construire pour obtenir l'image finale.

Lorsque le build est terminé, vous pouvez lancer la commande suivante afin d'observer les images Docker disponibles en local.

docker images

Vous verrez alors l'image que vous venez de construire précédemment. Vous pouvez donc maintenant lancer un conteneur avec la commande docker run à partir de cette image.

🖐️
Il est possible de créer des image Docker d'une autre manière, en utilisant la commande docker commit. Cela consiste à lancer un conteneur, effectuer les actions que l'on souhaite dedans et créer une nouvelle image à partir de l'état de ce conteneur.
Je n'ai pas abordé en détail cette méthode car elle n'est pas forcément conseillée. En effet, il est préférable d'utiliser des Dockerfile.

Nous avons vu les instructions les plus utilisées mais il en existe bien d'autres. Je vous laisserai vous documenter sur celles-ci lorsque vous en aurez besoin.

Dans ce cours, vous avez pu construire votre propre image Docker ! Sauf que je ne vous ai pas montré les bonnes pratiques à utiliser pour construire une image Docker.
Oui car il est possible d'optimiser une image Docker de bien des manières :

  • Optimiser les layers avec des opérateurs Bash comme &&.
  • Optimiser la taille d'une image Docker avec des arguments sur l'outil d'installation ( apt install --no-cache par exemple).
  • Effectuer du build multi-stage (niveau avancé).
  • Utiliser des variables d'environnement et des arguments de build

Et bien d'autres ! C'est ce que nous allons en partie voir dans le prochain cours dédié à l'optimisation d'une image Docker.