Docker peut construire des images automatiquement en se basant sur ce qu’on appelle un Dockerfile. Une fois le Dockerfile créé il faut le “build” pour avoir une image.
Voici un exemple de Dockerfile nginx :
FROM ubuntu
RUN apt-get update && apt-get -y install nginx
CMD ["nginx", "-g", "daemon off;"]
J'expliquerai un peu plus bas ce Dockerfile
Il faut ensuite le build pour en faire une image :
docker build .
L’image sera dans notre “librairie” docker :
docker images
# OU
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 62d49f9bab67 11 days ago 133MB
On peut maintenant le lancer un container à partir de l'image créée ci-dessus :
# Création avec l'ID de l'image
docker container run -d -p 80:80 62d49f9bab67
La syntaxe d’un Dockerfile est la suivante :
FROM image
# Commentaires via #
INSTRUCTION arguments
Un dockerfile commencera toujours pas un FROM. Plusieurs FROM peuvent être utilisés (multi-stages).
Voici une liste des instructions les plus utilisées :
Pour expliquer le Dockerfile du point précédent :
On veut maitenant créer un Dockerfile avec un fichier index.html personnalisé.
Pour commencer on peut partir d’une image ubuntu
, se connecter en bash dessus afin de faire les commandes une à une avant de créer le Dockerfile :
docker container run -d -p 80:80 -t --name mynginx ubuntu
docker container exec -it mynginx bash
On va donc exécuter les commandes nécessaires pour installer nginx :
apt-get update
apt-get install nginx -y
echo “Welcome to nginx in Docker” > index.html
nginx -g ‘daemon off;’
On va maintenant passer à l’initialisation du Dockerfile. Pour se faire nous allons d’abord créer un fichier index.html à la racine du dossier de demo avec notre code HTML.
Puis créer un Dockerfile qui ressemblera à ça :
# Image ubuntu latest
FROM ubuntu
# Afin d'optimiser l'image, il est préférable de RUN tout sur une seule ligne
RUN apt-get update
RUN apt-get install -y nginx
# On va maintenant copier index.html de notre hôte vers le dossier /var/www/html du container
COPY index.html /var/www/html
# On lance nginx
CMD ["nginx", "-g", "daemon off;"]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HELLO WORLD</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
# Image ubuntu latest
FROM ubuntu
RUN apt-get update && apt-get install nginx -y && apt-get clean && rm -rf /var/lib/apt/lists/*
# On va maintenant copier index.html de notre hôte vers le dossier /var/www/html du container
COPY index.html /var/www/html
EXPOSE 80
# On lance nginx
CMD ["nginx", "-g", "daemon off;"]
On va ensuite build l'image :
docker build .
Exemple :
FROM busybox
COPY copy.txt /tmp
ADD add.txt /tmp
CMD [“sh”]
Les deux instructions servent à copier un fichier d’un localisation spécifique vers le container.
Utiliser ADD <URL> est cependant non recommandé ! Il est préférable d’utiliser curl/wget via l'instruction RUN.
En pratique :
FROM busybox
COPY copy.txt /tmp
ADD add.txt /tmp
ADD compress.tar.gz /tmp
CMD ['sh']
Hello from copy.txt!
Hello from add.txt!
Télécharger le fichier ici (sha256sum: c1c9f4f62a995b1cfabbe6268e39f945e2529c862ab3616cee66b74ce505dac2
): compress.tar.gz
Résultat dans le container :
$ cd tmp/
$ ls
add.txt compress.txt copy.txt
On voit que compress.tar.gz a été décompressé automatiquement par l'instruction ADD.
Exemple :
FROM ubuntu
RUN apt-get install service
EXPOSE 8080
CMD [“service”]
Le service “expose” le port 8080.
Ici le port n’est pas “publié” (-p, --publish), il est juste indiqué que le service tourne sur ce port.
EXPOSE est donc une INSTRUCTION à titre indicatif pour l'utilisateur qui va lancer le container.
Si l’on fait un docker ps après avoir lancé un nginx sans le -p :
$ docker run -d --name without-port nginx
08125d2f15931983efe2f497bc534555360a5a029e1fbd5061a108483d906e95
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
08125d2f1593 nginx "/docker-entrypoint.…" 2 seconds ago Up 1 second 80/tcp without-port
On voit que le port 80 est exposé mais pas publié.
Si on veut accéder au container depuis l’extérieur (i.e depuis notre machine), il faut bind un port hote → container via la commande -p 80:80 par exemple :
docker run -d --name nginx2 -p 80:80 nginx
S’il n’y a aucun “EXPOSE” dans le Dockerfile, cela va être compliqué pour l’utilisateur de savoir sur quel port écoute l’application. EXPOSE est donc mis à disposition afin de documenter.
L’instruction HEALTHCHECK permet de dire à Docker comment faire pour savoir si l’application est UP ou non.
Voici un exemple de HEALTHCHECK (sans grand intérêt) :
FROM busybox
HEALTHCHECK --interval=5s CMD ping -c 1 172.17.0.3 # ou l’ip ici est un autre container
CMD ["sleep", "3600"]
Ici on va faire un ping toutes les 5s sur un autre container (récupérer l'IP via la commande docker inspect
) :
$ docker build -t monitoring .
$ docker container run -d --name monitor monitoring
7e035427163ef6326ac15533c3088ef6ef87c39ace55fb8de30dacd9ffc12630
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7e035427163e monitoring "sleep 3600" 6 seconds ago Up 5 seconds (healthy) monitor
08125d2f1593 nginx "/docker-entrypoint.…" 11 minutes ago Up 11 minutes 80/tcp without-port
$ docker inspect monitor
[...]
"Config": {
[...]
"Healthcheck": {
"Test": [
"CMD-SHELL",
"ping -c 1 172.17.0.3"
],
"Interval": 5000000000
},
[...]
# OU
$ docker inspect -f '{{json .Config.Healthcheck}}' monitor | jq
{
"Test": [
"CMD-SHELL",
"ping -c 1 172.17.0.3"
],
"Interval": 5000000000
}
Dans notre cas, si on éteint le container avec l’adresse 172.17.0.3 (nginx without-port
)
Le container monitor va donc devenir unhealthy car les pings ne réussiront plus.
$ docker rm -f without-port
without-port
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7e035427163e 4608704a8909 "sleep 3600" 7 minutes ago Up 7 minutes (unhealthy) monitor
Pour rappel, le ping renvoie un code 0 si réussi, code 1 si échec.
On peut spécifier certains paramètres avant l’instruction CMD dans le HEALTHCHECK :
--interval=DURATION
(30s par défaut)--timeout=DURATION
(30s par défaut)--start-period=DURATION
(0s par défaut)--retries=N
(3 par défaut)FROM ubuntu:22.04
RUN apt-get update && apt-get install nginx curl -y && apt-get clean && rm -rf /var/lib/apt/lists/*
EXPOSE 80
HEALTHCHECK --interval=5s --timeout=2s --start-period=3s --retries=1 CMD curl -f http://localhost || exit 1
CMD ["nginx", "-g", "daemon off;"]
On peut aussi exécuter un healthcheck via un docker container run :
docker container run -dt --name tmp --health-cmd “curl -f http://localhost/” busybox sh
On va maintenant modifier l’interval et le retry :
docker container run -dt --name tmp --health-cmd “curl -f http://localhost/” --health-interval=5s --health-retries=1 busybox sh
--health-interval=5s
: Intervalle de temps entre chaque test--health-retries=1
: Nombre de tentative(s) avant de passer le container en UNHEALTHYENTRYPOINT sert à mettre en place la commande principale de l’image. Cependant elle n’écrase pas l’instruction CMD.
Différence entre CMD et ENTRYPOINT :
FROM busybox
CMD ["sh"]
On va maintenant change la CMD lors de l'exécution du container :
docker container run -it --rm --name test-cmd cmd ping -c 5 google.com
Dans l’exemple ci-dessus CMD sera donc ping…
FROM busybox
ENTRYPOINT [“/bin/ping”]
Ici on a mis /bin/ping en ENTRYPOINT. On va maintenant lui passer les arguments :
docker container run -it --rm --name test-entrypoint entrypoint -c 5 google.com
Cela va donc ajouter l'argument -c 20 google.com à l'ENTRYPOINT. Ce qui donnera la commande ping -c 20 google.com.
WORKDIR instruction va permettre de mettre le path par défaut (absolu et relatif) pour les commandes RUN, CMD, ENTRYPOINT, COPY, ADD.
Exemple :
FROM busybox
WORKDIR /root/demo
RUN touch file01.txt
CMD ["/bin/sh"]
Si on build l'image et qu'on exécute la commande sh dans le container :
$ docker build -t test-workdir .
$ docker run -it --rm --name test-workdir workdir ls
file01.txt
$ docker run -it --rm --name test-workdir workdir pwd
/root/demo
Par défaut, notre dossier de travail sera donc /root/demo
On voit qu'on est dans le répertoire /root/demo et que le fichier a été crée dans celui-ci.
On peut mettre plusieurs WORKDIR dans un dockerfile.
WORKDIR /a # Absolu
WORKDIR b # Relatif
WORKDIR c # Relatif
RUN pwd
On aura donc le retour suivant : /a/b/c
2eme exemple avec question :
FROM busybox
WORKDIR /root/demo # Absolu
WORKDIR context1 # Relatif
WORKDIR context2 # Relatif
RUN touch file01.txt
CMD ["/bin/sh"]
/root/demo/context1/context2
ENV instruction permet de mettre des variables d’environnement dans le container.
ENV <key>=<value> :
FROM busybox
ENV NGINX=1.2
RUN touch web-$NGINX.txt
CMD ["/bin/sh"]
Cela va donc créer un fichier web-1.2.txt :
$ docker build -t env .
$ docker run -it --rm --name test-env env ls
bin home proc tmp web-1.2.txt
On peut aussi utiliser ENV avec la commande docker container run avec -e, --env et --env-file :
$ docker container run --rm --env VAR1=value1 --env VAR2=value2 ubuntu env | grep VAR
VAR1=value1
VAR2=value2
On voit donc que les deux variables (VAR1 et VAR2) ont bien été transmises sur le container Ubuntu.
ARG instruction permet de mettre des variables d’environnement uniquement disponible lors du build.
ARG <key>=<value> :
FROM busybox
ARG NGINX=1.2
RUN touch web-$NGINX.txt
CMD ["/bin/sh"]
Le tag va permettre par exemple d’avoir la même image avec plusieurs fonctions (docker-git, docker-dind, docker…) ou de connaitre la version logiciel de l'image (v1.2, v2.0...).
Pour mettre en place un tag, c’est lors du build :
docker build -t demo:v1 .
# Pour effacer cette image
docker rmi demo:v1
# ou
docker image rm demo:v1
Ici le tag de l'image est v1.
Lors d’un build on peut oublier de mettre un tag. On peut l’ajouter plus tard :
docker tag 30fb3a7 demo:v2
On peut aussi “retag” une image déjà tagué par un “latest” par exemple :
docker tag ubuntu:latest myubuntu:v1
Très rarement effectué, mais lorsque l’on fait des changements dans un container, il peut être préférable de commit ses changements dans une nouvelle image. On va donc utiliser docker commit.
Pendant le commit, le container sera mis en pause.
docker container commit CONTAINER_ID myimage01
Voici un exemple :
FROM ubuntu
RUN apt-get update && apt-get install nginx -y && apt-get clean && rm -rf /var/lib/apt/lists/*
COPY index.html /var/www/html
EXPOSE 80
CMD nginx -g 'daemon off;'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HELLO WORLD</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
On va ensuite lancer un container et modifier le contenu de l'index :
$ docker run -d --rm --name test-docker-commit -p 80:80 test-docker-commit
$ curl 127.0.0.1
...
<body>
<h1>Hello world!</h1>
</body>
</html>
$ docker exec -it test-docker-commit /bin/bash
root@d3bd59d98f2b:/> cd /var/www/html/
root@d3bd59d98f2b:/var/www/html> echo "<h1>TOTO</h1>" > index.html
$ docker container commit test-docker-commit my-commited-image
$ docker run -d --rm --name test-new-commited-image -p 8080:80 my-commited-image
$ curl 127.0.0.1:8080
<h1>TOTO</h1>
Attention : Docker commit ne copie pas les volumes.
Pour une raison ou une autre on peut changer la CMD d’une image via un commit :
docker container commit --change=’CMD [“ash”]’ busybox myimage02
--change permet donc de changer les instructions qui ont été défini dans le Dockerfile de l’image en question :
Une image docker représente plusieurs couches (layers).
Chaque layer est une instruction.
FROM ubuntu
RUN dd if=/dev/zero of=/root/file1.txt bs=1M count=100
RUN dd if=/dev/zero of=/root/file2.txt bs=1M count=100
RUN rm -f /root/file1.txt
RUN rm -f /root/file2.txt
En détaillant le dockerfile ci-dessus :
En faisant le build en détail :
docker build -t test-layers .
docker image history test-layers
IMAGE CREATED CREATED BY SIZE COMMENT
2031d313a146 42 seconds ago /bin/sh -c rm -f /root/file2.txt 0B
4a6f11abe452 42 seconds ago /bin/sh -c rm -f /root/file1.txt 0B
a7e2fbf34909 43 seconds ago /bin/sh -c dd if=/dev/zero of=/root/file2.tx… 105MB
53e59aa5f939 45 seconds ago /bin/sh -c dd if=/dev/zero of=/root/file1.tx… 105MB
7e0aa2d69a15 6 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 6 days ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 6 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 6 days ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 811B
<missing> 6 days ago /bin/sh -c #(nop) ADD file:5c44a80f547b7d68b… 72.7MB
Il faut lire les couches de bas vers le haut. Les premères couches font partis de l'image Ubuntu.
A partir de la 6eme couche, on voit nos commandes : 2x105MB puis 2x0B.
Si on regarde la taille de l'image :
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
test-layers latest 2031d313a146 2 minutes ago 282MB
282MB... Je ne comprends pas, on vient pourtant de supprimer les 2 fichier (file1 et file2) qui pesaient à eux seul 200MB...
En effet ! Docker fonctionne par système de couche. C'est à dire qu'une fois l'instruction passée, la taille et le layer seront non modifiables même si on fait une commande rm derrière !
Pour réduire la taille de l’image il faut mettre l’instruction rm au sur le même layer que la création du fichier !
Soit :
FROM ubuntu
RUN dd if=/dev/zero of=/root/file1.txt bs=1M count=100 && rm -f /root/file1.txt && dd if=/dev/zero of=/root/file2.txt bs=1M count=100 && rm -f /root/file2.txt
En refaisant le build :
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
test-layers latest a2ad2dd0ee8b 10 seconds ago 72.7MB
On a donc réduit 4 couches en 1 seule, l'image ne pèse plus que 72.7MB !
Vous l'aurez donc compris, un dockerfile doit donc se faire sur le moins de couches possible. Les fichiers inutiles doivent être supprimés.
Voici les commandes disponibles :
docker image build
/ history
/ import
/ inspect
/ load
/ ls
/ prune
/ pull
/ push
/ rm
/ save
/ tag
docker image pull
/ docker pull
:
docker image ls
ou docker images
:
Inspecter une image docker peut servir pour plusieurs choses :
docker image inspect <image_name>
On peut aussi filtrer la sortie à l’aide de -f / --format :
docker image inspect -f “{{.Id}}” nginx
docker image inspect --format=”{{.Id}}” nginx
On peut aussi avoir le retour en json (format key/value au lieu de value) :
docker image inspect -f “{{ json .ContainerConfig}}” nginx
On peut récupérer que l’hostname dans ContainerConfig :
docker image inspect -f “{{ json .ContainerConfig.Hostname}}” nginx
Docker image prune va nous permettre de nettoyer les images inutilisées.
Par défaut, cela va enlever les images sans tags (qui ne sont pas utilisées) :
docker image prune
On peut aussi enlever les images qui ne sont pas référencées par au moins un container :
docker image prune -a
Il est possible d'exporter l'état d'un container avec la commande docker export
:
$ docker container run -dt --name myubuntu ubuntu
$ docker export myubuntu > myubuntudemo.tar
$ ls -l myubuntudemo.tar
On va ensuite réimporter cette image :
cat myubuntudemo.tar | docker import - myubuntu:latest
Attention : Docker export ne copie pas les volumes.
Cette image ne comprendra qu'un seul layer :
$ docker image history myubuntu
IMAGE CREATED CREATED BY SIZE COMMENT
9dcf1d6eb799 6 seconds ago 123MB Imported from -
Utilisation typique : sauvegarde de l'état actuel d'un conteneur ou migration d'un conteneur sans nécessité de conserver l'historique de l'image.
C’est ici qu’on va stocker l’image docker (registry). De base les images vont être récupérées sur le Docker Hub.
Il existe plusieurs types de registry :
Nous allons utiliser l’image docker “registry” qui permet de créer un espace de stockage pour les images :
$ docker run -d -p 5000:5000 --restart always --name registry registry:2
$ docker pull ubuntu
$ docker tag ubuntu:latest localhost:5000/myubuntu
$ docker push localhost:5000/myubuntu
Pour récupérer l’image depuis un registry :
docker pull localhost:5000/myubuntu
Comment push sur docker hub ?
Il faut évidemment un compte docker hub et créer un repository. Ensuite :
docker login
docker tag busybox <username>/mydemo:v1
docker push <username>/mydemo:v1
Si on veut pull :
docker pull <username>/mydemo:v1
docker logout
Il peut être difficile de trouver la bonne image sur le docker hub.
On peut chercher l’image via la commande docker search :
On va donc limiter les résultats :
docker search nginx --limit 5 # images ordonnées par nombre d’étoiles
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
nginx Official build of Nginx. 14798 [OK]
jwilder/nginx-proxy Automated Nginx reverse proxy for docker con… 2020 [OK]
richarvey/nginx-php-fpm Container running Nginx + PHP-FPM capable of… 813 [OK]
bitnami/nginx Bitnami nginx Docker Image 96 [OK]
nginxinc/nginx-unprivileged Unprivileged NGINX Dockerfiles 32
On peut aussi filtrer les résultats :
docker search --help
docker search --filter stars=3 nginx # minimum de 3 étoiles
docker search --filter is-official=true --filter stars=3 nginx
Envoyer une image docker sans passer par un registry :
docker save busybox > busybox.tar
Une fois l’image transférée :
docker load < busybox.tar
Utilisation typique : sauvegarde ou transfert d'images Docker complètes entre différents hôtes.
On sait que chaque image contient des layers.
Chaque commande dans un Dockerfile crée un nouveau layer.
Docker utilise un cache par layer afin d’optimiser le temps de construction de l’image.
FROM python:3.11-slim-bullseye
COPY . .
RUN pip install --quiet -r requirements.txt
ENTRYPOINT ["python", "server.py"]
flake8==6.1.0
pandas==2.0.3
flask==2.3.2
Le premier build va prendre du temps :
time docker build -t without-cache .
Si nous ne modifions aucune couche du dockerfile :
time docker build -t with-cache .
Le build se fera quasiment instantanément grâce au cache.
Voici une petite explication en image :
Nous avons vu la création, gestion et registre des images. Nous allons maintenant nous concentrer sur le réseau avec Docker.