Le format d’une image est défini par un standard ouvert. En plus de différentes métadonnées comme la command par défaut, une image est composée d’un ensemble de couches qui indiquent les changements effectués à partir de la couche précédente. Cela permet de partager efficacement des bases communes entre images mais demande quelques précautions.
Manuellement
La commande history montre les couches qui composent une image.
docker pull ubuntu:jammydocker history ubuntu:jammy
Pour créer une image, il est possible d’exécuter un conteneur, de faire de faire des modifications manuelles puis de les valider. Cela provoque la création d’une nouvelle couche dans le système de fichier et donc d’une nouvelle image. Cette approche est déconseillée car difficile à maintenir (par exemple lors d’un simple changement de version d’un logiciel).
On peut ainsi fabriquer un conteneur simple pour fournir la commande git.
En exécutant manuellement les commandes dans un conteneur
et la nouvelle image utilisée pour créer autant de conteneur que nécessaire
docker run --rm mygit git --version
git version 2.34.1
Nous pouvons voir que l’ID de l’avant-dernière couche est celui de la dernière de l’image d’ubuntu et qu’une nouvelle couche qui contient les modifications faite par les commandes précédentes a été ajoutée.
docker history mygit
IMAGE CREATED CREATED BY SIZE COMMENT
aeb9a3599eba 1 second ago bash - 64.2MB
981912c48e9a 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:53ce73ebbd6d87a23… 69.2MB
<missing> 2 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ARG RELEASE 0B
L’approche recommandée est de créer un fichier Dockerfile (cf. https://docs.docker.com/engine/reference/builder/) pour automatiser la création d’une image. Ce fichier peut être édité lors des mises à jour et versionné avec git pour gérer un historique de l’infrastructure ainsi que des versions parallèles.
Structure de base
Le fichier Dockerfile suivant illustre les concepts de base. Il s’appuie sur l’image python:3.11-slim-bullseye (FROM), ajoute les fichiers de l’application depuis le contexte (COPY), exécute les commandes nécessaire à la construction de l’image (RUN) et défini la commande par défaut de l’image (CMD).
# syntax=docker/dockerfile:1# Choose the parent imageFROM python:3.12-slim# An argument sets at build command with --build-arg# It as a default valueARG BUILD_DATE=1970-01-01T00:00:00Z# key-value pair as image metadataLABEL maintainer="emmanuel.bruno@univ-tln.fr"# See http://label-schema.org/rc1/ for a list of usefull labelsLABEL org.label-schema.build-date=$BUILD_DATE# An environment variableENV NAME="John Doe"# Creates and moves to a directoryWORKDIR ${WORKDIR}# Copy the requirements them in the new image.# Done before the copy of the src to limit cache invalidations# when only source code changes.COPY requirements.txt ./# Installation of the dependencies RUNpip install --requirement requirements.txt# Copy the sources COPY hello.py ./# Set the default command for the imageCMDpython${WORKDIR}/hello.py
La commande suivante construit une image tagguée à partir du Dockerfile contenu dans le répertoire courant (indiqué par le . à la fin). C’est ce dernier paramètre qui est le contexte. Les options --build-arg permettre de fixer les ARG du Dockerfile.
d’autres tags peuvent être appliqués pour facilier l’usage de l’image, généralement des versions plus génériques (1.2.3, 1.2 et 1) et/ou des informations sur l’image de base avec des variantes (1.2.3-ubuntu, 1.2.3-debian, …) et/ou des variantes de constructions de l’image elle-même et toujours latest.
docker tag brunoe/helloworld_python:0.1 brunoe/helloworld_python:0docker tag brunoe/helloworld_python:0.1 brunoe/helloworld_python:latest
Cette image peut alors être utilisée localement.
docker run --rm brunoe/helloworld_python:0.1
WARNING:root:Hello John Doe, I'm Python in a container !
docker run --env NAME="Pierre"--rm brunoe/helloworld_python:0.1
WARNING:root:Hello Pierre, I'm Python in a container !
Il est intéressant de connaitre la taille d’une image et le détail des couches qui la compose.
brunoe/helloworld_python 0 9d20c5328d2b 2 seconds ago 336MB
brunoe/helloworld_python 0.1 9d20c5328d2b 2 seconds ago 336MB
brunoe/helloworld_python latest 9d20c5328d2b 2 seconds ago 336MB
docker history brunoe/helloworld_python:0.1
IMAGE CREATED CREATED BY SIZE COMMENT
9d20c5328d2b 2 seconds ago CMD ["/bin/sh" "-c" "python ${WORKDIR}/hello… 0B buildkit.dockerfile.v0
<missing> 2 seconds ago COPY hello.py ./ # buildkit 254B buildkit.dockerfile.v0
<missing> 2 seconds ago RUN |1 BUILD_DATE=2024-10-02T04:45:47Z /bin/… 187MB buildkit.dockerfile.v0
<missing> 12 seconds ago COPY requirements.txt ./ # buildkit 13B buildkit.dockerfile.v0
<missing> 12 seconds ago WORKDIR / 0B buildkit.dockerfile.v0
<missing> 12 seconds ago ENV NAME=John Doe 0B buildkit.dockerfile.v0
<missing> 12 seconds ago LABEL org.label-schema.build-date=2024-10-02… 0B buildkit.dockerfile.v0
<missing> 12 seconds ago LABEL maintainer=emmanuel.bruno@univ-tln.fr 0B buildkit.dockerfile.v0
<missing> 12 seconds ago ARG BUILD_DATE=2024-10-02T04:45:47Z 0B buildkit.dockerfile.v0
<missing> 19 hours ago CMD ["python3"] 0B buildkit.dockerfile.v0
<missing> 19 hours ago RUN /bin/sh -c set -eux; for src in idle3 p… 36B buildkit.dockerfile.v0
<missing> 19 hours ago RUN /bin/sh -c set -eux; savedAptMark="$(a… 43.6MB buildkit.dockerfile.v0
<missing> 19 hours ago ENV PYTHON_VERSION=3.12.7 0B buildkit.dockerfile.v0
<missing> 19 hours ago ENV GPG_KEY=7169605F62C751356D054A26A821E680… 0B buildkit.dockerfile.v0
<missing> 19 hours ago RUN /bin/sh -c set -eux; apt-get update; a… 9.17MB buildkit.dockerfile.v0
<missing> 19 hours ago ENV LANG=C.UTF-8 0B buildkit.dockerfile.v0
<missing> 19 hours ago ENV PATH=/usr/local/bin:/usr/local/sbin:/usr… 0B buildkit.dockerfile.v0
<missing> 5 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 5 days ago /bin/sh -c #(nop) ADD file:28df1cb6a6576d40b… 97.1MB
Partage de l’image
Une image peut ensuite être partagée sur un registry (dockerhub ou un autre qui peut être public ou privé). Pour cela le docker client doit s’authentifier (docker login) puis pousser les différents tags de l’image (docker push <image_name>). Attention, le nom de l’image doit généralement préciser le compte sur le repository.
La directive ENTRYPOINT prend en paramètre une commande permet que l’image docker se comporte comme cette commande en acceptant directement les paramètres lors d’un run. La commande exécutée est la concaténation de ENTRYPOINT et des paramètre de run ou de CMD. CMD correspond donc aux options par défaut que l’on souhaite si aucun paramètre n’est donné.
L’exemple suivant montre comment construire une commande qui crée une base de donnée sqlite dans repertoire courant.
FROM alpineVOLUME /workdirWORKDIR /workdirRUNapk--no-cache add sqliteENTRYPOINT ["sqlite3","-box","local.db"]CMD ["--version"]
et il n’est plus nécessaire de redonner la commande, le nom de l’image suffit. En utilisant un alias, il est donc possible de donner l’illusion qu’il s’agit d’un programme.
# sqlite() {docker run -v ${PWD}:/workdir --rm sqlite}sqlite(){docker run -v /tmp:/workdir --rm sqlite}sqlite"DROP TABLE IF EXISTS data;"sqlite"CREATE TABLE IF NOT EXISTS data (key TEXT PRIMARY KEY,value TEXT NOT NULL);"sqlite"INSERT INTO data values('A','10');INSERT INTO data values('B','20');"sqlite"SELECT * FROM data;"
Un Dockerfile dit multistage utilise plusieurs FROM. Chacun définit une image mais seule la dernière sera le résultat final. Les images précédentes peuvent être utilisées pour produire des fichiers qui seront copiés dans une étape (stage) suivant.
Pour cela, chaque FROM peut ête nommé avec AS pour qu’une étape suivante puisse accéder à son système de fichier avec COPY --from=<stage_name> <source> <destination>.
L’utilisation la plus classique est d’avoir deux stages : (1) compilation et (2) exécution. La première étape contient l’environnement de développement (gcc pour le C, JDK+Maven pour Java, …) et les sources, elle produit l’exécutable (un binaire pour le C, du bytecode ou un jar pour Java, …). La seconde étape ne contient que l’environnement d’exécution (éventuelles librairies dynamiques pour le C, JRE pour Java, …) et l’exécutable.
FROM gcc:12-bullseye as stage-buildWORKDIR /src/appCOPY . /src/appRUNgcc-Wall-Os helloworld.c -o helloworldFROM debian:bullseye-slimCOPY--from=stage-build /src/app/helloworld /helloworldENTRYPOINT ["/helloworld"]
L’instruction VOLUME <chemin dans le conteneur> définit ce chemin comme un point de montage pour un volume. Cela signifie que les données stockées dans ce répertoire seront persistantes même si le conteneur est supprimé.
L’instruction EXPOSE <PORT> expose le port du conteneur aux autres conteneurs.