Ce document est une introduction à l’utilisation des conteneurs pour isoler les composants d’une application. En pratique, nous utilisons Docker. L’objectif est de pouvoir produire des environnements de compilation et d’exécution d’applications répétables, versionnables et portables.
Vocabulaire
Conteneur
Un conteneur est une sorte de bac à sable (sandbox) qui isole un ensemble de processus du système en utilisant les namespaces du noyau linux et leur associe une carte réseau virtuelle. Un conteneur est l’instance d’une image. Généralement, un conteneur fournit un et un seul service.
Image
Une image est système de fichier isolé qui contient tout ce qui est nécessaire pour l’exécution d’un conteneur. Elle contient aussi d’autres informations comme des metadonnées, des variables d’environnement ou la commande par défaut à exécuter. Souvent une image s’appuie sur une autre image à laquelle elle apporte des modificiations.
Container Daemon (containerd, Docker Daemon)
Le container daemon est un serveur qui gère les images, les conteneurs, les réseaux et les volumes de stockage. Il peut communiquer avec d’autres container daemons et offre une API standard.
Docker Client
Le docker client est un outils en ligne de commande pour envoyer des commandes à un ou plusieurs container daemons locaux ou distants.
Par exemple, un docker client peut demander à un container daemon d’exécuter une image ce qui produit un conteneur. Si l’image n’est pas disponible localement, elle est téléchargée par le container daemon depuis un registry.
Installation
Docker peut être installé directement sous linux, il partage alors le noyau de l’hôte. Il peut aussi être installé dans une machine virtuelle Linux (sous Linux, Windows et MacOs) par exemple via Docker Desktop.
Exécuter un conteneur
La commande docker container run permet d’executer la commande par défaut d’une image dans un espace isolé. Si l’image n’est pas disponible pour le docker daemon, elle est téléchargée à partir d’un registry (par défaut Docker Hub).
Comme premier exemple, exécutons l’image hello-world qui illustre ce fonctionnement (lire le résultat) :
Une commande docker commence par le type d’objet qu’elle manipule (container, image, …) suivi d’une sous-commande. Par exemple, il est possible de demander au docker daemon de précharger ou de mettre à jour une image avec la sous-commande pull.
Pour télécharger ou mettre à jour Alpine Linux qui est une distribution linux très légère :
Le format du nom d’une image est [registry_hostname[:port]][path]image_name[:tag]. Si l’adresse du registry est omise c’est celle de dockerhub qui est utilisé (docker.io). Si l’image est “officielle” path peut-être omis et vaut library, sinon il s’agit généralement du compte de l’utilisateur qui fourni l’image. tag permet de différencier les versions d’une image, s’il est omis il vaut latest.
Ainsi, le nom d’image alpine correspond en fait à docker.io/library/alpine:latest. La documentation indique les tags existants. En production, pour obtenir des exécutions reproductibles il est conseillé de spécifier la version
Il est possible de choisir la commande à utiliser au lancement du container en l’indiquant après le nom de l’image. Des variables d’environnement peuvent êtrée crées dans le conteneur avec l’option --env.
docker container run \ alpine:3.17.2 uname -a
Unable to find image 'alpine:3.17.2' locally
3.17.2: Pulling from library/alpine
af6eaf76a39c: Pulling fs layer af6eaf76a39c: Downloading 32.77kB/3.262MBaf6eaf76a39c: Downloading 262.1kB/3.262MBaf6eaf76a39c: Downloading 1.212MB/3.262MBaf6eaf76a39c: Verifying Checksum af6eaf76a39c: Download complete af6eaf76a39c: Extracting 32.77kB/3.262MBaf6eaf76a39c: Extracting 3.262MB/3.262MBaf6eaf76a39c: Pull complete Digest: sha256:ff6bdca1701f3a8a67e328815ff2346b0e4067d32ec36b7992c1fdc001dc8517
Status: Downloaded newer image for alpine:3.17.2
Linux c247430e3361 6.10.4-linuxkit #1 SMP Mon Aug 12 08:47:01 UTC 2024 aarch64 Linux
docker container run --env FIRSTNAME="John"--env NAME="Doe"\ alpine:3.17.2 env
Les conteneurs produisent généralement le log du service sur la sortie standard. Quand --detach est utilisé la commande logs permet de le consulter (ajouter -f pour un suivi en continu).
docker logs my-redis
1:C 02 Oct 2024 04:43:37.747 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 02 Oct 2024 04:43:37.747 # Redis version=7.0.9, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 02 Oct 2024 04:43:37.747 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
1:M 02 Oct 2024 04:43:37.748 * monotonic clock: POSIX clock_gettime
1:M 02 Oct 2024 04:43:37.748 * Running mode=standalone, port=6379.
1:M 02 Oct 2024 04:43:37.748 # Server initialized
1:M 02 Oct 2024 04:43:37.749 * Ready to accept connections
La sous-commande ls permet d’obtenir tous les conteneurs en cours d’exécution.
docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a796bfaf5d1f redis:7.0.9 "docker-entrypoint.s…" Less than a second ago Up Less than a second 6379/tcp my-redis
La commande ls -a affiche aussi ceux qui se sont arrêtés.
docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a796bfaf5d1f redis:7.0.9 "docker-entrypoint.s…" 1 second ago Up Less than a second 6379/tcp my-redis
69f7049b2982 ubuntu:22.04 "bash -" 7 seconds ago Exited (0) 6 seconds ago my-bash
0e307073c958 alpine:3.17.2 "sh -c 'echo 'Hi !' …" 12 seconds ago Exited (0) 11 seconds ago relaxed_ptolemy
c2866205ef3e alpine:3.17.2 "env" 12 seconds ago Exited (0) 11 seconds ago optimistic_davinci
c247430e3361 alpine:3.17.2 "uname -a" 13 seconds ago Exited (0) 11 seconds ago exciting_almeida
4fb713c0f3ab hello-world "/hello" 19 seconds ago Exited (0) 18 seconds ago unruffled_leakey
Un conteneur s’arrête quand sa commande termine. Il peut être arrêté manuellement avec la sous-commande stop.
docker container stop my-redis
my-redis
Un conteneur arrêté peut être relancé avec les même paramètres avec la commande start.
docker container start my-redis
my-redis
Un container arrêté peut être détruit avec la commande rm en indiquant son Id ou son nom.
docker rm my-bash
my-bash
La commande exec permet d’exécuter une commande dans un conteneur en fonctionnement. Par exemple, pour utiliser l’interface en ligne de commande de redis depuis le conteneur my-redis (qui exécute déjà le serveur) pour ajouter une valeur de clé “a.b@x.fr”.
docker exec my-redis redis-cli SET a.b@x.fr "Pierre,Durand,12"
OK
L’option --rm de la commande run provoque la suppression du conteneur dès l’arrêt.
On peut alors créer un autre conteneur éphémère pour exécuter la commande de récupération d’une valeur de redis via le réseau (la partie réseau est expliquée plus tard) à partir de l’image du serveur.
docker run --rm--link my-redis redis:7.0.9 redis-cli -h my-redis GET a.b@x.fr
Pierre,Durand,12
docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a796bfaf5d1f redis:7.0.9 "docker-entrypoint.s…" 2 seconds ago Up Less than a second 6379/tcp my-redis
Un conteneur est généralement éphémère et sans état pour pouvoir être arrêté et relancé sans perte de données. Les fichiers créés devant être persistants doivent donc l’être en dehors du conteneur. Pour cela, on utilise les montage de systeme d fichiers (bind mount) ou les volumes.
Un bind volume, c’est-à-dire un montage du système de fichier de l’hôte vers un répertoire du conteneur, est définit avec l’option -v <source>:<destination> de la commande run que l’on peut uiliser plusieurs fois. Le chemin doit être absolu mais peut utiliser la variable d’environement ${PWD} pour simuler un chemin relatif.
Dans l’exemple suivant, un premier conteneur crée le fichier text.txt dans un volume monté de l’hôte (le répertoire /tmp/mydata) dans le conteneur (à l’emplacement /data). Le conteneur est ensuite détruit. Un second conteneur monte le même répertoire de l’hôte (/tmp/mydata dans le nouveau conteneur mais dans /databis) et affiche le contenu.
docker container run --rm\-v /tmp/mydata:/databis \ ubuntu:22.04 \ cat /databis/test.txt
hello
un volume peut aussi être un volume nommé géré par le docker daemon de façon transparente pour l’utilisateur. Il n’est alors plus aussi simple de partager des fichiers entre l’hôte et les conteneurs mais il n’y a plus de problème d’UID du propriétaire ni de droits. Il s’agit de la meilleure solution quand le docker daemon est exécuté sur un cluster et non sur une seule machine car alors le système de fichier peut être distribué.
L’exemple suivant exécute une base de données via l’image de PostgreSQL dans un premier conteneur. Le volume nommé postgres-test-data est monté dans le répertoire indiqué dans la documentation de l’image pour contenir la base de données (/var/lib/postgresql/data). Comme celui-ci est initialement vide, l’image est construite pour créer alors automatiquement une base de données à partir des variables d’environnement fournies.
docker run -d--rm--quiet\--name postgres-test \--env POSTGRES_PASSWORD=mysecretpassword \-v postgres-test-data:/var/lib/postgresql/data \ postgres:15.2
Il est possible de monter le même volume et un bind volume dans une autre conteneur par exemple pour faire une sauvegarde. En pratique, on préfère pg_dump, mais nous l’illustrons ici avec un simple tar. Dans le cas d’une base de données relationnelles l’arrêt du conteneur (ou un snapshot) est obligatoire pour garder la cohérence.
docker run --rm\-v postgres-test-data:/var/lib/postgresql/data \-v /tmp/backup:/backup \ ubuntu:22.04 \ tar zcf /backup/mydb.tar.gz /var/lib/postgresql/data
tar: Removing leading `/' from member names
Ensuite, un autre conteneur PostgreSQL peut être lancé en utilisant la même base.
docker run -d--rm\--name postgres-test \--env POSTGRES_PASSWORD=mysecretpassword \-v postgres-test-data:/var/lib/postgresql/data \ postgres:15.2
docker: Error response from daemon: Conflict. The container name "/postgres-test" is already in use by container "d75fdd3f66239a25cacd6055c82bb05ab164391c98a5a2190e87df045c178628". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.
docker logs postgres-test
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.
The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are disabled.
fixing permissions on existing directory /var/lib/postgresql/data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
Success. You can now start the database server using:
pg_ctl -D /var/lib/postgresql/data -l logfile start
initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.
waiting for server to start....2024-10-02 04:43:53.171 UTC [49] LOG: starting PostgreSQL 15.2 (Debian 15.2-1.pgdg110+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
2024-10-02 04:43:53.172 UTC [49] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2024-10-02 04:43:53.173 UTC [52] LOG: database system was shut down at 2024-10-02 04:43:53 UTC
2024-10-02 04:43:53.175 UTC [49] LOG: database system is ready to accept connections
done
server started
/usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
waiting for server to shut down....2024-10-02 04:43:53.288 UTC [49] LOG: received fast shutdown request
2024-10-02 04:43:53.289 UTC [49] LOG: aborting any active transactions
2024-10-02 04:43:53.290 UTC [49] LOG: background worker "logical replication launcher" (PID 55) exited with exit code 1
2024-10-02 04:43:53.290 UTC [50] LOG: shutting down
2024-10-02 04:43:53.290 UTC [50] LOG: checkpoint starting: shutdown immediate
2024-10-02 04:43:53.293 UTC [50] LOG: checkpoint complete: wrote 3 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.003 s; sync files=2, longest=0.001 s, average=0.001 s; distance=0 kB, estimate=0 kB
2024-10-02 04:43:53.294 UTC [49] LOG: database system is shut down
done
server stopped
PostgreSQL init process complete; ready for start up.
2024-10-02 04:43:53.401 UTC [1] LOG: starting PostgreSQL 15.2 (Debian 15.2-1.pgdg110+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
2024-10-02 04:43:53.402 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
2024-10-02 04:43:53.402 UTC [1] LOG: listening on IPv6 address "::", port 5432
2024-10-02 04:43:53.403 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2024-10-02 04:43:53.406 UTC [63] LOG: database system was shut down at 2024-10-02 04:43:53 UTC
2024-10-02 04:43:53.409 UTC [1] LOG: database system is ready to accept connections
docker stop postgres-test
postgres-test
Il est possible de lister et de supprimer les volumes avec les commandes ls et rm. Les volumes dont les noms sont des hachés appelés volumes anonymes sont présentés plus tard.
docker volume ls
DRIVER VOLUME NAME
local ae5e7e841549fa3fa2216ee68a9c7be5e1ac3607ecfb11e71350cbb8b7812c54
local postgres-test-data
docker volume rm postgres-test-data
postgres-test-data
docker volume ls
DRIVER VOLUME NAME
local ae5e7e841549fa3fa2216ee68a9c7be5e1ac3607ecfb11e71350cbb8b7812c54
Réseau
Chaque conteneur dispose d’une carte réseau virtuelle et d’au moins une adresse IP. Un conteneur peut utiliser le réseau de l’hôte (--host) mais généralement il appartient à un ou plusieurs réseau virtuels. Si aucun n’est précisé expliciement, le conteneur appartient à un réseau par défaut. Comme les conteneurs sont éphémères et que les adresses IP changent, elles ne sont généralement pas utilisées directement.
Les réseaux peuvent être créés, listés et détruits avec les commandes network {create, ls, rm}.
Ainsi deux conteneurs qui exécutent un shell sont sur le réseau par défaut.
docker run --quiet-dit--rm--name alpine1 alpine ashdocker run --quiet-dit--rm--name alpine2 alpine ash
NETWORK ID NAME DRIVER SCOPE
16cb1a2ff3ee bridge bridge local
ae33eca811d2 host host local
1b7d57c60e4f none null local
La commande docker inspect {container, volume, network} <name|ID> permet de consulter le détails d’un objet docker. jq est un outils pour traiter des documents json.
Avec l’option --link lors du runun conteneur peut utiliser le nom d’un autre conteneur comme nom d’hôte. Ici on utilise un image qui contient des utilitaires réseaux.
PING alpine1 (172.17.0.2) 56(84) bytes of data.
64 bytes from alpine1 (172.17.0.2): icmp_seq=1 ttl=64 time=0.117 ms
--- alpine1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.117/0.117/0.117/0.000 ms
docker container stop alpine1 alpine2
alpine1
alpine2
entre conteneurs
Pour que deux conteneurs puissent communiquer simplement, le mieux est qu’ils appartiennent à un même réseau et qu’ils soient nommés. Le nom du conteneur peut alors être utilisé comme un nom d’hôte sur le réseau.
si aucun réseau n’est précisé, le conteneur se trouvent dans le réseau par défaut. Il est alors obligatoire d’utiliser l’option --link qui est dépréciée.
ce serveur peut être atteint depuis un autre conteneur sur le même réseau en utilisant son nom. Ici on exécute la commande pandoc qui convertit un site web en texte :
docker run --quiet--network mynet --rm pandoc/core:3.1 --to plain http://nginx-01
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
Welcome to nginx!
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.
Thank you for using nginx.
NETWORK ID NAME DRIVER SCOPE
16cb1a2ff3ee bridge bridge local
ae33eca811d2 host host local
3c8ffc92fbd8 mynet bridge local
1b7d57c60e4f none null local
docker stop nginx-01docker network rm mynet
nginx-01
mynet
entre l’hôte et les conteneurs
Il est possible de mettre en place une redirection de ports entre l’hôte et les conteneurs avec l’option -p <port hote>:<port conteneur> à la création. Cela permet d’accéder aux services docker depuis l’hôte ou le réseau local.
On peut donc lancer deux serveur nginx de deux versions différents qui écoutent chacun sur le port 80 de leur conteneur respectif mais qui sont accessibles depuis les port 8081 et 8082 de l’hôte.
docker run --quiet--rm-d--name nginx-01 -p 8081:80 nginx:1.22docker run --quiet--rm-d--name nginx-02 -p 8082:80 nginx:1.23
Le nom host.docker.internal permet d’accéder à l’hôte depuis les conteneurs.
docker run --rm--quiet\--add-host=host.docker.internal:host-gateway \ curlimages/curl:7.88.1 --silent--head host.docker.internal:8081|head-n 2
HTTP/1.1 200 OK
Server: nginx/1.22.1
docker run --rm--quiet\--add-host=host.docker.internal:host-gateway \ curlimages/curl:7.88.1 --silent--head host.docker.internal:8082|head-n 2
HTTP/1.1 200 OK
Server: nginx/1.23.4
docker stop nginx-01 nginx-02
nginx-01
nginx-02
Illustration avec une base de données relationnelles
Pour illustrer les concepts précédents, nous allons créer une base de données Postgresql à partir de l’image officielle dans un sous-réseau backnet et dont les données seront persistées dans un volume nommé postgres-01-data.
Il est ensuite possible d’interroger cette base de données avec la commande psql exécutée dans un autre conteneur ephémère.
sleep 2 docker run --rm\--network backnet \--env PGPASSWORD=mysecretpassword \ postgres:15.2 \ psql -h postgres-01 -U dba mydb -c\"CREATE TABLE IF NOT EXISTS PERSON(ID serial PRIMARY KEY,NAME VARCHAR NOT NULL); INSERT INTO PERSON (NAME) VALUES ('Pierre'); INSERT INTO PERSON (NAME) VALUES ('Marie');"
CREATE TABLE
INSERT 0 1
INSERT 0 1
Le conteneur de la base de donnée peut être détruit (cf –rm) et un autre créé avec le même volume donc sans perte de données.
Pour finir, le conteneur postgres-01 est arrêté et donc détruit, ainsi que le réseau.
Dans notre exemple, le volume postgres-01-data est aussi détruit mais attention les données perdues.