Aller au contenu

Construire une image

Dans cette section, il est expliqué comment construire une image Docker à partir de zéro.

Dockerfile

Un Dockerfile est comme une recette. C'est un fichier texte contenant des instructions et leurs arguments permettant à Docker de construire automatiquement des images. C'est l'élément de base d'une image.

image-creation-process

Voici une liste d'instructions:

Instructions Explications
FROM Pour spécifier l'image de base.
LABEL Pour spécifier les informations de métadonnées de l'image Docker.
RUN Pour exécuter des commandes pendant le processus de construction de l'image (e.g. commandes linux...).
COPY Copie les fichiers et répertoires locaux dans l'image.
ADD Similaire à l'instruction COPY avec des fonctionnalités supplémentaires comme l'extraction de tar uniquement locale et le support des URLs distantes. Cependant, préférez l'instruction COPY à ADD.
ENV Définit les variables d'environnement à l'intérieur de l'image qui seront disponibles pendant la construction et aussi dans le conteneur en cours d'exécution. Pour définir uniquement les variables au moment de la construction, utilisez l'instruction ARG.
ARG Définit les variables de construction avec une clé et une valeur. Les variables ARG ne sont pas disponibles dans le conteneur en cours d'exécution. Pour les variables persistantes dans un conteneur en cours d'exécution, utilisez ENV.
WORKDIR Définit le répertoire de travail courant. Vous pouvez réutiliser cette instruction dans un fichier Docker pour définir un répertoire de travail différent. Si WORKDIR est défini, les instructions comme RUN, CMD, ADD, COPY, ou ENTRYPOINT sont exécutées dans ce répertoire. Préférez l'utilisation de WORKDIR plutôt que des instructions comme RUN cd ..
USER Définit le nom d'utilisateur et l'UID lors de l'exécution du conteneur. Utilisez le pour définir un utilisateur non-root du conteneur. L'utilisateur doit être créé s'il n'existe pas.
ENTRYPOINT Spécifie les commandes qui seront toujours exécutées au démarrage du conteneur Docker. ENTRYPOINT ne peut être "surchargé" qu'avec l'option --entrypoint. Il prend la forme de tableau JSON (ex : ENTRYPOINT ["ls","-l"]) ou de texte. Il ne peut y avoir qu'une seule instruction ENTRYPOINT. Cette instruction n'est pas éxecutée lors de la phase de build mais lors de la phase de run.
CMD Exécute une commande dans un conteneur en cours d'exécution.Elle peut aussi servir à spécifier des arguments par défaut qui seront envoyés à l'ENTRYPOINT; dans ce cas-ci les instructions CMD et ENTRYPOINT doivent être spécifiées au format de tableau JSON. Il ne peut y avoir qu'une seule instruction CMD mais peut être remplacée par la CLI de Docker. Cette instruction n'est pas éxecutée lors de la phase de build mais lors de la phase de run.
EXPOSE Spécifie le port à exposer pour le conteneur Docker.
VOLUME Il est utilisé pour créer ou monter le volume pour le conteneur Docker.

Mon premier build

Le but ici est de créer une image jupyter notebook personnalisée en y installant un plugin vscode.

Selon la documentation officielle, dans la plupart des cas, il est préférable de placer chaque Dockerfile dans un répertoire vide. Ensuite, ajoutez à ce répertoire seulement les fichiers nécessaires à la construction du Dockerfile; donc :

  • Créez un répertoire vide appelé build :
mkdir build
  • Copiez-y le dossier python_codes :
cp -rp python_codes build && cd build
  • Créez un fichier nommé Dockerfile (il est possible de remplacer code-server_xxx_amd64.deb par code-server_xxx_arm64.deb pour les systèmes ARM):
FROM quay.io/jupyter/minimal-notebook:lab-4.2.6

LABEL maintainer="<amar.hami@lpnhe.in2p3.fr> & <aurelien.bailly-reyre@lpnhe.in2p3.fr>"

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Switch to root user to install packages:
USER root

# Container updated and some packages installed:
RUN apt-get update --yes && apt-get upgrade --yes &&\
    apt-get install --yes --quiet --no-install-recommends \
    curl \
    bsdmainutils \
    file \
    man-db && \
    apt-get --quiet clean && rm -rf /var/lib/apt/lists/*

# Code Server and server-proxy/vscode-proxy modules installed to integrate VSCode inside JupyerLab:
ENV CODE_VERSION=4.18.0
RUN curl -fOL https://github.com/coder/code-server/releases/download/v$CODE_VERSION/code-server_${CODE_VERSION}_amd64.deb \
    && dpkg -i code-server_${CODE_VERSION}_amd64.deb \
    && rm -f code-server_${CODE_VERSION}_amd64.deb

WORKDIR /tmp
RUN git clone https://github.com/betatim/vscode-binder \
  && cd vscode-binder \
  && pip install . \
  && cd $HOME \
  && rm -rf /tmp/vscode-binder

RUN mamba install -c conda-forge jupyter-server-proxy jupyter-vscode-proxy

# Jupyter ressource usage
#RUN mamba install -c conda-forge jupyter-resource-usage

# Cleaning
RUN mamba clean --all

# Switch back to jovyan to avoid accidental container running as root:
ENV USERNAME="jovyan"
USER $USERNAME

# Switch back to jovyan's home:
WORKDIR /home/$USERNAME
COPY --chown=jovyan:users python_codes work

Remarque

Aucune instruction ENTRYPOINT ou CMD n'apparaît ici. L'image résultante de ce Dockerfile hérite en réalité de l'instruction CMD ["start-notebook.py"] de l'image de base (jupyter/minimal-notebook:lab-4.0.7) sur laquelle elle est construite.

La construction de l'image se fait avec la commande docker build:

docker build [OPTIONS] -f <file> -t <image name>[:<tag>] <path>

où les arguments:

  • -f (--file): permet de spécifier le nom du fichier contenant la recette. Si le fichier se nomme Dockerfile, l'option -f Dockerfile peut être omise;
  • -t (--tag) : spécifie le nom de l'image et optionnellement un tag. Si celui-ci n'est pas spécifié l'image aura automatiquement le tag par défaut latest;
  • <path>: spécifie le chemin du répertoire contenant le Dockerfile.

Exercice

Construire l'image à partir du Dockerfile ci-dessus et vérifier qu'elle a bien été créée avec le tag latest.

Solution
docker build -f Dockerfile -t myjupyter . && docker image ls

On peut ajouter un ou plusieurs tag à une image a posteriori:

docker image tag <image source>[:<tag>] <target image>[:<tag>]

Exercice

Ajouter un tag v1 à l'image créée et vérifier ensuite son ajout.

Solution

docker image tag myjupyter:latest myjupyter:v1 && docker image ls
On remarque que myjupyter:latest et myjupyter:v1 ont le même IMAGE ID.

Note

Pour supprimer l'image myjupyter, il faut supprimer toutes "ses versions":

docker image rm myjupyter:latest &&\
docker image rm myjupyrer:v1

Layer cache

Une image Docker est consituée de couches, layers en anglais. Ces couches sont créées par chacune des instructions qui sont exécutées linéairement lors de la génération de l'image (cf le schéma ci-dessous qui provient du site docker). L'image finale créée n'est autre qu'un pointeur pointant vers la derniére couche générée.

docker-build-layers

Lorsque l'on lance le build de l'image, docker tente d'utiliser des couches déjà existentes (générées lors de précédentes constructions d'image). Ce mécanisme de cache permet de gagner du temps lors la construction d'une image en évitant ainsi de regénérer inutilement les couches intermédiaires inchangées. Si l'une d'entre elles se trouve modifiée par rapport au précédent build, alors elle doit être regénérée ainsi que les couches suivantes. L'ordre des instructions a donc son importance si l'on souhaite bénéficier au mieux du mécanisme de cache.

Pour voir l'ensemble des couches intermédiaires d'une image, vous pouvez utiliser la commande:

docker image history <image name>

Exercice

Voir l'ensemble des couches de l'image générée précedemment. Puis pour mettre en évidence l'efficacité du mécanisme de cache, décommenter la ligne dans le Dockerfile:

# Jupyter ressource usage
RUN mamba install -c conda-forge jupyter-resource-usage
et reconstruiser l'image.

Solution
docker image history myjupyter:latest &&\
docker build -f Dockerfile -t myjupyter:test &&\
docker image history myjupyter:test

ENTRYPOINT ou CMD?

Les instructions ENTRYPOINT et CMD ont des effets similaires lorsqu'elles sont utilisées séparément. Mais elles peuvent être aussi combinées, ce qui peut s'avérer intéressant dans certains cas. Comparez l'effet de ces deux instructions à l'aide d'exemples.

Ces deux instructions ne sont pas éxecutés lors de la phase de build mais lors de la phase de run.

Tout d'abord créez un Dockerfile contenant:

Dockerfile
FROM alpine:latest

RUN apk add htop

ENTRYPOINT ["echo", "Hello"]
CMD ["World"]
puis une image nommée exemple:

docker build -f Dockerfile -t exemple .

Exercice

Essayer d'anticiper ce que font les commandes suivantes avant de les tester:

docker run -it --rm exemple &&\
docker run -it --rm exemple You &&\
docker run -it --rm exemple htop &&\
docker run -it --rm --entrypoint htop exemple

Solution

Le conteneur exécute la commande définie par l'ENTRYPOINT à savoir echo Hello. L'instruction CMD (optionnelle), quant à elle, sert à spécifier un arguement par défaut, ici World à l'ENTRYPOINT. Donc par défaut lorsque le conteneur est exécuté, "Hello World" est affiché.

Il est possible de changer la valeur par défaut de CMD par l'intermédiaire de la CLI docker à l'image de la deuxième et troisième commande.

Si l'on souhaite modifier la commande à exécuter dans le conteneur, ici htop, il faut le préciser à l'aide de l'option --entrypoint <commande>.Dans le même répertpoire créer un fichier Dockerfile.cmd:

À l'aide d'un fichier Dockerfile.cmd contenant la recette suivante:

Dockerfile.cmd
FROM alpine:latest

RUN apk add htop

CMD ["echo", "Hello World"]

créez une image exemple avec le tag cmd :

docker build -f Dockerfile.cmd -t exemple:cmd .

Exercice

Exécuter les commandes suivante et conclure :

docker run -it --rm exemple:cmd &&\
docker run -it --rm exemple:cmd You ||\
docker run -it --rm exemple:cmd echo "Hello You" &&\
docker run -it --rm exemple:cmd htop

Solution

Ici, le conteneur exécute par défaut la commande qui se trouve dans l'instruction CMD. Comme dans l'exercice précédent, on peut surcharger cette instruction à l'aide de la CLI Docker avec une commande qui existe dans le conteneur. Ce qui n'est pas le cas avec la seconde commande, puisque You n'est pas une commande.

Authors: Aurélien Bailly-Reyre