Conteneurs: Création d’une image

Containers
Docker
I211
PO43
Lecture
Création d’image de conteneurs, application avec Docker.
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

2024-10-02

Source
Branch
  • develop (b23c847)
  • 2024-03-08 09:42:51
Java
  • OpenJDK Temurin-21.0.4+7
  • Apache Maven 3.9.9
Docker
  • Client: 27.2.0 - buildx v0.16.2 - compose v2.29.7
  • Server: 27.2.0

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:jammy
docker history ubuntu:jammy
jammy: Pulling from library/ubuntu

a186900671ab: Pulling fs layer a186900671ab: Downloading  278.5kB/27.36MBa186900671ab: Downloading  1.393MB/27.36MBa186900671ab: Downloading  3.627MB/27.36MBa186900671ab: Downloading  6.695MB/27.36MBa186900671ab: Downloading  10.04MB/27.36MBa186900671ab: Downloading  12.56MB/27.36MBa186900671ab: Downloading   13.4MB/27.36MBa186900671ab: Downloading  18.13MB/27.36MBa186900671ab: Downloading  21.21MB/27.36MBa186900671ab: Downloading   24.3MB/27.36MBa186900671ab: Downloading  27.36MB/27.36MBa186900671ab: Verifying Checksum a186900671ab: Download complete a186900671ab: Extracting  294.9kB/27.36MBa186900671ab: Extracting  6.783MB/27.36MBa186900671ab: Extracting  11.21MB/27.36MBa186900671ab: Extracting  20.05MB/27.36MBa186900671ab: Extracting   23.3MB/27.36MBa186900671ab: Extracting  25.95MB/27.36MBa186900671ab: Extracting  26.84MB/27.36MBa186900671ab: Extracting  27.36MB/27.36MBa186900671ab: Pull complete Digest: sha256:58b87898e82351c6cf9cf5b9f3c20257bb9e2dcf33af051e12ce532d7f94e3fe
Status: Downloaded newer image for ubuntu:jammy
docker.io/library/ubuntu:jammy
IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
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        

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

docker run --name my-ubuntu-container --interactive ubuntu:jammy bash -  <<EOF
apt-get update &> /dev/null
apt-get install --yes --no-install-recommends &> /dev/null \
    git 
apt-get clean autoclean &> /dev/null
apt-get autoremove --yes &> /dev/null
rm -rf /var/lib/apt/lists/*
EOF

puis en validant ces modifications sous la forme d’une nouvelle couche d’une image

docker commit my-ubuntu-container mygit:latest
sha256:aeb9a3599ebad8c2aa6477c3e6cdc7d13aa9a384e74a9d96a14963896d95b5c8

le conteneur modifié peut alors être supprimé

docker rm my-ubuntu-container
my-ubuntu-container

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        
docker image rm mygit
Untagged: mygit:latest
Deleted: sha256:aeb9a3599ebad8c2aa6477c3e6cdc7d13aa9a384e74a9d96a14963896d95b5c8
Deleted: sha256:7786473d6cbb2770659a6dd2e7be5d8ab527f4713e0d7b72aef5b7779346ff69

Dockerfile

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 image
FROM python:3.12-slim

# An argument sets at build command with --build-arg
# It as a default value
ARG BUILD_DATE=1970-01-01T00:00:00Z

# key-value pair as image metadata
LABEL maintainer="emmanuel.bruno@univ-tln.fr"
# See http://label-schema.org/rc1/ for a list of usefull labels
LABEL org.label-schema.build-date=$BUILD_DATE

# An environment variable
ENV NAME="John Doe"

# Creates and moves to a directory
WORKDIR ${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 
RUN pip install --requirement requirements.txt

# Copy the sources 
COPY hello.py ./

# Set the default command for the image
CMD python ${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.

( cd sample-python/helloworld && \
 docker image build \
     --quiet \
     --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\
     --tag brunoe/helloworld_python:0.1 \
     . 
)
sha256:9d20c5328d2b7335e1d298f6747ea27b820358450e12a3b71d367373c4a36bb7

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:0
docker 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.

docker image ls|grep brunoe/helloworld_python|tr -s ' '
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.

docker push brunoe/helloworld_python:0.1
docker push brunoe/helloworld_python:latest

Entrypoint

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 alpine
VOLUME /workdir
WORKDIR /workdir
RUN apk --no-cache add sqlite
ENTRYPOINT ["sqlite3","-box","local.db"]
CMD ["--version"]
docker build --quiet --tag sqlite sample-sqlite/
sha256:7ffc3b4a38355b8f9bf79283723e8d6ed2228edc76e9e94143de964a365c8115

Le run affiche par défaut la version au lieu de l’aide.

docker run --rm sqlite
3.45.3 2024-04-15 13:34:05 8653b758870e6ef0c98d46b3ace27849054af85da891eb121e9aaa537f1e8355 (64-bit)

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;"
3.45.3 2024-04-15 13:34:05 8653b758870e6ef0c98d46b3ace27849054af85da891eb121e9aaa537f1e8355 (64-bit)
3.45.3 2024-04-15 13:34:05 8653b758870e6ef0c98d46b3ace27849054af85da891eb121e9aaa537f1e8355 (64-bit)
3.45.3 2024-04-15 13:34:05 8653b758870e6ef0c98d46b3ace27849054af85da891eb121e9aaa537f1e8355 (64-bit)
3.45.3 2024-04-15 13:34:05 8653b758870e6ef0c98d46b3ace27849054af85da891eb121e9aaa537f1e8355 (64-bit)

Multistage

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-build
WORKDIR /src/app
COPY . /src/app
RUN gcc -Wall -Os helloworld.c -o helloworld

FROM debian:bullseye-slim
COPY --from=stage-build /src/app/helloworld /helloworld
ENTRYPOINT ["/helloworld"]
( cd sample-c/helloworld/ && \
 docker image build \
     --quiet \
     --tag brunoe/helloworld_c:0.1 \
     . )
sha256:e1c7ad32af61aa76c08340faa7dcafee7230c17b645d899b21d7f81284bf45ad
docker run brunoe/helloworld_c:0.1 Pierre
Hello Pierre !

Volumes et réseaux

EXPOSE et VOLUME

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.

Réutilisation