# Docker ![Schema Docker Swarm](images/docker-wave-whale.svg "Schema Docker Swarm") __Images de conteneurs__ Maxime Poullain • Christian Tritten ### Qu'est-ce qu'une image de conteneur ? Une image : - Est un template de conteneur en lecture seule. - Est composée de fichiers et de métadonnées. - Peut par exemple contenir un système d'exploitation Debian avec Apache et une application web pré-installés. - Est "instanciée" pour créer un ou plusieurs conteneurs. ![Les images Docker les plus populaires en 2020](images/most-popular-docker-official-images-in-2020.png) Source : https://www.docker.com/blog/docker-index-shows-continued-massive-developer-adoption-and-activity-to-build-and-share-apps-with-docker/ ### Images incrémentales et réutilisables ![Images réutilisables](images/docker-image-reusability.png) **Remarque importante** Si l'image de base est modifiée (par exemple dans le cadre de l'application d'une mise à jour de sécurité), et que l'on souhaite propager cette mise à jour sur les images dérivées, il faudra reconstruire ces dernières. ### Système de fichiers multi-couches - Les images sont composées d'une ou plusieurs couches superposées. - Chaque couche représente un différentiel des changements apportés par rapport à la couche inférieure. - Lorsque plusieurs images partagent des couches, cela permet d'optimiser l'espace disque, et les temps de transfert. ![Système de fichiers multi-couches](images/docker-image-layers.jpg) - La principale différence entre un conteneur et une image réside dans la _couche en r/w_ du sommet. - Toutes les écritures qui ajoutent ou modifient des données dans un conteneur sont stockées sur cette couche. - Quand le conteneur est supprimé la couche en r/w est aussi supprimée, les couches inférieures de l'image de base restent inchangées. - On peut sauvegarder les modifications effectuée au sein d'un conteneur en utilisant la commande : `docker commit` - Ceci va ajouter une _nouvelle couche_ en lecture seule au dessus de la pile composant l'image. Lorsqu'une image est modifiée, une nouvelle couche est ajoutée au sommet de la pile : ![Ajout d'une nouvelle couche](images/docker-image-new-layer.jpg) ### Partage d'image entre plusieurs conteneurs ![Partage d'image entre plusieurs conteneurs](images/docker-container-sharing-layers.jpg) ## Gestion des images Outils et commandes pour la gestion des images ### Images locales `docker image ls` ```none $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE composeformation_web latest 0d2d42971538 20 hours ago 237MB debian-formation latest 257c415ffcc7 20 hours ago 237MB mariadb 10.1.24 98f78d96be9c 2 weeks ago 395MB debian jessie 3e83c23dba6a 3 weeks ago 124MB hello-world latest 48b5124b2768 4 months ago 1.84kB ``` ### Le Docker Hub Le [Docker Hub](https://hub.docker.com/) est un entrepôt d'images de conteneurs sur lequel on peut télécharger : * des images _officielles_ certifiées par Docker * des images _publiques_ maintenues par la communauté ![Recherche mariadb sur le Docker Hub](images/docker-hub.png) __Attention !__ Docker ne garantie pas le bon fonctionnement ni même l'absence de faille de sécurité sur les images non officielles. ### Chercher une image sur le Docker Hub `docker search IMAGE` ```none $ docker search mariadb NAME DESCRIPTION STARS OFFICIAL mariadb MariaDB is a community-developed for... 1354 [OK] bitnami/mariadb Bitnami MariaDB Docker Image 37 paintedfox/mariadb A docker image for running MariaDB 5... 29 million12/mariadb MariaDB 10 on CentOS-7 with UTF8 default 14 toughiq/mariadb-cluster Dockerized Automated MariaDB Galera ... 11 webhippie/mariadb Docker images for mariadb 9 gists/mariadb MariaDB on Alpine 7 panubo/mariadb-galera MariaDB Galera Cluster 7 kakilangit/mariadb Docker for MariaDB with OQGraph & To... 6 maxexcloo/mariadb Service container with MariaDB insta... 4 tianon/mariadb DEPRECATED; use mariadb:* -- ♪ "I ju... 4 takaomag/mariadb docker image of archlinux (mariadb) 2 drupaldocker/mariadb MariaDB for Drupal 1 ... ``` ### Récupérer une image sur le Docker Hub `docker pull IMAGE[:TAG]` ```none $ docker pull mariadb:10.7.1 Using default tag: latest latest: Pulling from library/mariadb 10a267c67f42: Pull complete c2dcc7bb2a88: Pull complete 17e7a0445698: Pull complete 9a61839a176f: Pull complete 64675690edb1: Pull complete 3de17e251488: Pull complete f814b22b783e: Pull complete 733ce1f03439: Pull complete fb7b719835fd: Pull complete 8d3f82357729: Pull complete a4f4cbdfcf7c: Pull complete Digest: sha256:4b54358541679032f6c3a9d9fc944ad96d77ae72fecd6cb44bf18cf97743da24 Status: Downloaded newer image for mariadb:10.7.1 ``` ### Visualiser l'historique d'une image `docker history IMAGE` ```none $ docker history mariadb:10.7.1 IMAGE CREATED CREATED BY SIZE 98f78d96be9c 2 weeks ago /bin/sh -c #(nop) CMD ["mysqld"] 0B 2 weeks ago /bin/sh -c #(nop) EXPOSE 3306/tcp 0B 2 weeks ago /bin/sh -c #(nop) ENTRYPOINT ["docker-ent... 0B 2 weeks ago /bin/sh -c ln -s usr/local/bin/docker-entr... 34B 2 weeks ago /bin/sh -c #(nop) COPY file:d559178e6a2929... 5.6kB 2 weeks ago /bin/sh -c #(nop) VOLUME [/var/lib/mysql] 0B 2 weeks ago /bin/sh -c sed -Ei 's/^(bind-address|log)/... 5.27kB 2 weeks ago /bin/sh -c { echo mariadb-server-$MARIAD... 252MB 2 weeks ago /bin/sh -c echo "deb https://repo.percona.... 114B 2 weeks ago /bin/sh -c set -ex; export GNUPGHOME="$(m... 21.1kB 2 weeks ago /bin/sh -c apt-get update && apt-get insta... 14.3MB 2 weeks ago /bin/sh -c set -x && apt-get update && ap... 4.58MB 2 weeks ago /bin/sh -c groupadd -r mysql && useradd -r... 330kB 3 weeks ago /bin/sh -c #(nop) ADD file:f4e6551ac34ab44... 124MB ``` ### "Inspecter" une image `docker inspect IMAGE` ```none $ docker inspect mariadb [ { "Id": "sha256:98f78d96be9c7f513f21de040d083ee7ba23d74c8f3bc499373e56e93c...", "RepoTags": [ "mariadb:10.7.1" ], "RepoDigests": [ "mariadb@sha256:4b54358541679032f6c3a9d9fc944ad96d77ae72fecd6cb44bf1..." ], "Parent": "", "Comment": "", "Created": "2017-05-09T17:28:06.071608373Z", "Container": "83ce76bba170200d3783bde70b7c1d06a61ed2b91bec7351a5c5a664f5...", "ContainerConfig": { "Hostname": "200591939db7", "Domainname": "", "User": "", "ExposedPorts": { "3306/tcp": {} ``` ### Supprimer une image `docker image rm IMAGE [IMAGE...]` ```none $ docker image rm mariadb:10.7.1 Untagged: mariadb:10.7.1 Untagged: mariadb@sha256:4b54358541679032f6c3a9d9fc944ad96d77ae72fecd6cb44bf18cf... Deleted: sha256:98f78d96be9c7f513f21de040d083ee7ba23d74c8f3bc499373e56e93c8e9ec9 Deleted: sha256:bea03b338eb87d64861847305aa63f6104212c60719168f25b54ca713db4b870 Deleted: sha256:519db73d66bef13a78573160ddf2059f9dc382e03fd2e85f354c3172ded67b90 Deleted: sha256:7f728a3fd818a51a5425306ef40f398c7698f4252ade70cb83b3d26b825bb613 Deleted: sha256:48159803f1446a31e60af329025fc5c3ae8ef07f950d8750a7e14d46d1d1191c Deleted: sha256:ff5b1cc6d50c6f7ab9e6ee77ff89b0b037a6840f7b1f44cbe234499362221c15 Deleted: sha256:d147674f5cce42f85942e815f154c6a7ecb86359689d823a9840b28126a12f4e Deleted: sha256:571be45150dde0fb8f6c3862abbcfa06fbad0e6a128d459a6b8ad0660f9f0660 Deleted: sha256:458271f19a2c854fc6fd3338f9151662173b96a14cfe1a2e46eca95d27e4102c Deleted: sha256:ba779192baede4aadd009c269406b5e8fd885c653ce19719316bf40cc66a6cf3 Deleted: sha256:2302bd8bbdd530199aa432c357a4da9eab2621c3ba4c4dacb4ea0f4afecbcae7 Deleted: sha256:00771f8e1e12bdfc9d47bc52a78e3f5ce5306a1caa5dd6237731cff9ca106040 Deleted: sha256:8d4d1ab5ff74fc361fb74212fff3b6dc1e6c16d1e1f0e8b44f9a9112b00b564f ``` ### Importer / Exporter une image 1. Export `$ docker save -o mon-image.tar IMAGE` 2. Import `$ docker load -i mon-image.tar` ### Travaux pratiques ![Travaux pratiques](images/tp.gif) [TP Docker Images](../travaux-pratiques/slides/docker/tp-images.html) ## Dockerfile Automatiser la construction d'une image - Il est possible de construire une image à la main puis de la sauvegarder avec un `docker commit`. - Toutefois ceci est fastidieux, non parfaitement reproductible et donc potentiellement source d'erreur. - D'autre part, comment gérer les mises à jours d'une telle image ? - Docker est capable de construire des images automatiquement à partir des instructions d'un _Dockerfile_. - Le Dockerfile est un fichier texte qui contient toutes les instructions permettant de construire une image Docker pour une application donnée. [https://docs.docker.com/engine/reference/builder/](https://docs.docker.com/engine/reference/builder/) ![Dockerfile](images/dockerfile-image-container.jpg) - Le Dockerfile est versionnable et permet de produire une image à tout moment. - Ceci s'inscrit dans la philosophie _Infrastructure as Code_ qui prône la définition d'une architecture dans des fichiers textes déclaratifs. - Grâce au Dockerfile on peut reconstruire périodiquement une image afin quelle intègre les dernières mises à jour applicatives et les derniers patches de sécurité. ### Exemple de Dockerfile ```docker FROM debian:bullseye LABEL maintainer "robert@produpot.com" # On installe Apache httpd ENV DEBIAN_FRONTEND noninteractive RUN apt update \ && apt install -y apache2 \ && rm -rf /var/lib/apt/lists/* # Ajout d'un script d'init ADD run.sh /run.sh RUN chmod 755 /run.sh # Importe l'application RUN mkdir -p /app && rm -fr /var/www/html && ln -s /app /var/www/html ADD homepage/ /app # On expose le port 80 EXPOSE 80 # On indique le script qui doit être lancé au démarrage du conteneur ENTRYPOINT ["/run.sh"] ``` ### Principales directives Directive | Description - | - `FROM` | Spécifie l'image de départ pour la construction de la nouvelle image. `LABEL` | Ajoute des métadonnées à la nouvelle image. `ENV` / `ARG` | Ajoute des variables d'environnement. `ADD` / `COPY` | Copie des fichiers ou des dossiers sur le système de fichiers de l'image. `VOLUME` | Créé un point de montage à l'instanciation du conteneur. ### Principales directives (suite) Directive | Description - | - `RUN` | Exécute une commande et "commite" le résultat dans une nouvelle couche de l'image. `USER` | Spécifie l'utilisateur à utiliser pour jouer les instructions `RUN` et `ENTRYPOINT`. `HEALTHCHECK` | Indique une commande qui sera lancée à l'intérieur du conteneur pour vérifier que celui-ci tourne correctement. `ENTRYPOINT` | Définie une commande de base à exécuter dans le conteneur. `CMD` | Définie les paramètres par défaut de la commande de base. ### LABEL ```docker LABEL maintainer="user@example.com" \ vendor=ACME\ Incorporated \ com.example.version="0.0.1-beta" \ com.example.release-date="2015-02-12" ``` ### Choisir entre `ARG` et `ENV` * `ARG` et `ENV` permettent de déclarer des variables qui sont utilisables à partir du moment où elles sont déclarées dans le Dockerfile. * Les `ARG` peuvent être surchargées au moment du build via l'option `--build-arg`. * Les `ENV` peuvent être surchargées à l'exécution via l'option `-e`. * Si une variable `ARG` est utilisée sans valeur par défaut et qu'aucune valeur n'est fournie via `--build-arg`, cela déclenche une erreur lors du build. ![ARG vs ENV](images/docker-environment-args-env.png) https://vsupalov.com/docker-arg-env-variable-guide/ `Dockerfile` ```docker ARG MY_VAR_1 <---- expect a build-time variable ARG MY_VAR_2=pouet <---- set a build-time variable RUN touch "$MY_VAR_2" ARG A_VARIABLE <---- expect a build-time variable ENV another_var=$A_VARIABLE <---- use the value to set the ENV var default if not overridden, that value of another_var will be available to your containers! ``` ### Choisir entre `ADD` et `COPY` Les deux directives ont la même syntaxe : ```docker COPY ... ADD ... ``` Selon le guide des bonnes pratiques Docker : * Utilisez `COPY` dans tous les cas, sauf si vous avez besoin d'extraire automatiquement le contenu d'une archive, dans ce cas précis utilisez `ADD`. * Pour récupérer des fichiers distants, préférez plutôt l'instruction `RUN wget ...`. ### CMD et ENTRYPOINT Les commandes `CMD` et `ENTRYPOINT` permettent de définir la commande par défaut à exécuter à l'intérieur du conteneur. - `ENTRYPOINT` définie la commande de base pour le conteneur, - `CMD` définie les paramètres par défaut pour cette commande. ```docker FROM debian:bullseye RUN apt update && apt install -y cowsay ENTRYPOINT ["/usr/games/cowsay"] CMD ["hello"] ``` ```none $ docker build -t cowsay . ... Successfully built a27691083512 Successfully tagged cowsay:latest ``` ```none $ docker run cowsay ------- < hello > ------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || ``` ```none $ docker run cowsay 2,21 Gigowatts ?! ------------------- < 2,21 Gigowatts ?! > ------------------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || ``` ### HEALTHCHECK L'exemple suivant teste toutes les 5 minutes que le conteneur est capable de servir une ressource HTTP en moins de 3 secondes : ```docker HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost/ || exit 1 ``` Le test de santé étant lancé depuis l'intérieur du conteneur, _la commande `curl` utilisée dans l'exemple ci-dessus doit être présente dans le conteneur_. Le status _healthy_ / _unhealthy_ est consultable via `docker ps` ou `docker inspect` ```none $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS 9e2ea5f59f8b xian/web "/start.sh" 28 minutes ago Up 28 minutes (healthy) 20851619e1af xian/web "/start.sh" 23 minutes ago Up 23 minutes (unhealthy) ``` ```none $ docker inspect -f '{{json .State.Health.Status}}' conteneur "healthy" ``` Il s'agit d'un simple message informatif, en effet Docker ne relance pas de lui-même un conteneur détecté comme _unhealthy_. * Toutefois, Le changement de santé d’un conteneur génère un évenement Docker que les outils de monitoring et d’orchestration peuvent intercepter. * Par exemple l'orchestrateur Swarm utilise cette information pour remplacer automatiquement le conteneur défectueux par une nouvelle instance. ### Construire l'image à partir du Dockerfile `docker build -t IMAGE[:TAG] .` Les tags permettent de proposer plusieurs versions d'une image. `docker build -t mon-image:v1 .` Une même image peut être tagguée plusieurs fois : ex : `debian:10` et `debian:buster` ### Derrière un proxy `--build-arg http_proxy=PROXY` ```none $ docker build --build-arg http_proxy=http://my.proxy.url:3128 \ --tag debian-formation . ``` ## Travaux pratiques ![Travaux pratiques](images/tp.gif) [TP Dockerfile](../travaux-pratiques/slides/docker/tp-dockerfile.html) ### Rendre une image publique * On peut rendre une image publique en la poussant sur le Docker Hub. * La création d'un Docker ID est nécessaire. * Le Docker ID sera le nom d'utilisateur pour le Docker Hub. * La commande `docker login` permet de se connecter au Hub. Pour pouvoir pousser une image sur le Hub, il faut la nommer avec un nom de la forme `docker-id/image:tag` Ceci se fait avec la commande `docker tag` : ```none $ docker tag mon-image mon-docker-id/mon-image:1.0.0 ``` Il est ensuite possible de pousser l'image avec la commande `docker push` : ```none $ docker push mon-docker-id/mon-image:1.0.0 ``` A partir de là, l'image devient accessible publiquement par n'importe qui. ### Dockerfile #### les bonnes pratiques Réutiliser au maximum la même image de base afin de mutualiser les couches entre vos différentes images applicatives. Eviter d'installer tout ce qui n'est pas strictement nécessaire. Paquets de la distribution, paquets applicatifs, ... Alléger les images en supprimant les données inutiles, mais pas n'importe comment ! ```docker RUN apt-get update && apt-get install -y package RUN rm -rf /var/lib/apt/lists/* ``` Chaque couche est commitée en readonly avant de passer à l'instruction suivante ! Il faut nettoyer dans la même couche ! ```docker RUN apt update \ && apt install -y package \ && rm -rf /var/lib/apt/lists/* ``` ```docker RUN wget archive.tar.gz \ && tar xzvf archive.tar.gz \ && rm archive.tar.gz ``` (Combiner plusieurs instructions `RUN` permet également de diminuer le nombre de couches constituant l'image.) Ne pas utiliser `apt-get upgrade` ou `apt-get dist-upgrade` dans vos images spécialisées. Ceci doit être fait dans l'image OS de base. Chaque conteneur devrait se focaliser sur une seule tâche. ![Un processus par conteneur](images/docker-one-process.png) Utiliser une image de base légère. Par exemple, [debian:bullseye-slim](https://hub.docker.com/_/debian/) pèse ~30MB tout en restant une distribution complète. Proscrire l'utilisation du tag `latest` en production. __Attention !__ `docker pull myimage` == `docker pull myimage:latest` Le tag `latest` est utilisé par défaut si vous n'en fournissez pas un dans vos commandes Docker ! 3 bonnes raisons de ne pas utiliser le tag `latest` * Votre outil de déploiement ne déploiera pas une nouvelle version de votre application taggué latest, tout simplement parce qu'il ne détectera pas la différence avec l'ancienne version elle aussi tagguée avec latest. * Tagguer une image avec un numéro de version unique permet d'effectuer de la tracabilité. On sait exactement quelle version est déployée. * En réutilisant systématiquement le tag latest, tout retour arrière à une ancienne version de l'image est impossible, car vous écrasez systématiquement la précédente version de l'image docker. Déterminer une stratégie pour les tags * Lorsque on crée une image, il nous appartient de lui ajouter des tags appropriés. * Utiliser une stratégie pour les tags qui soit cohérente et consistante sur toutes les images produites. * La stratégie doit être facilement compréhensible par les utilisateurs de ces images. __L'exécution de processus avec l'utilisateur `root` à l'intérieur d'un conteneur est un préliminaire à de nombreux types d'attaques.__ Déclarer un utilisateur dédié permet d'éviter un grand nombre d'attaques : ```docker RUN useradd -d /home/my-app-user -m -s /bin/bash my-app-user USER my-app-user ``` On peut forcer l'utilisateur à l'exécution : ```none $ docker run --user my-app-user -d -t my-application ``` L'utilisateur doit exister dans `/etc/passwd` à l'intérieur du conteneur. Ne pas écrire de secrets dans le Dockerfile. (secret == mot de passe, clé de chiffrement, clé d'API) Injecter la configuration à l'exécution du conteneur. Utiliser la fonctionnalité de _multi-stage build_ (Docker > v17.05) pour produire des images plus petites. Un cas d'usage est celui où l'on doit compiler une binaire afin de produire l'image applicative finale. En découpant le processus de build, on va tout d'abord construire une image dédié à la compilation du binaire et injecter le résultat de cette compilation dans l'image finale qui sera ainsi d'une beaucoup plus réduite. `Dockerfile` ```docker FROM golang:1.10 as builder WORKDIR /tmp/go COPY hello.go ./ RUN CGO_ENABLED=0 go build -a -ldflags '-s' -o hello FROM scratch CMD [ "/hello" ] COPY --from=builder /tmp/go/hello /hello ``` `$ docker build -t hello:1 .` `$ docker image ls` ```none REPOSITORY TAG IMAGE ID CREATED SIZE hello latest 212f44bc4048 4 seconds ago 3.2MB 08370cf772b1 5 seconds ago 693MB ``` Le résultat final est une image d'environ 3Mb au lieu de 700Mb sans le multi-stage build. Pour le reste des bonnes pratiques : * https://sysdig.com/blog/dockerfile-best-practices/ * https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/ * https://cloud.google.com/solutions/best-practices-for-building-containers #### Haskell Dockerfile Linter Un outil pour vérifier les bonnes pratiques sur les fichiers Dockerfile ```none $ docker run --rm -i hadolint/hadolint < Dockerfile /dev/stdin:2 DL3020 Use COPY instead of ADD for files and folders /dev/stdin:4 DL3025 Use arguments JSON notation for CMD and ENTRYPOINT arguments ``` https://github.com/hadolint/hadolint Alternatives : [Dockle](https://github.com/goodwithtech/dockle) | [Trivy](https://aquasecurity.github.io/trivy/) ## Nettoyer les images Les images ont tendance à s'accumuler et à remplir progressivement l'espace de stockage sur les hôtes. * `$ docker image prune` Supprime toutes les images qui ne sont ni taguées ni référencées par au moins un conteneur. * `$ docker image prune -a` Supprime toutes les images qui ne sont pas référencées par au moins un conteneur. ## Registry * Le composant Docker Registry permet le stockage et la distribution d'images Docker. * Docker Inc. fourni une image officielle prête à l'emploi pour déployer facilement un Registry. ### Fonctionnalités * Authentification des utilisateurs * Push/Pull d'images * Stockage des images sur une grande variété de backends (S3, Posix Filesystems, Ceph, Swift...) ### Utilisation du Docker Registry ```none $ docker pull hello-world $ docker tag hello-world mon_nom_de_domaine:5000/hello-world $ docker push mon_nom_de_domaine:5000/hello-world ... $ docker pull mon_nom_de_domaine:5000/hello-world ``` ## Sécurité - rapports * __NCC Group__ Understanding and Hardening Linux Containers Juin 2016 [www.nccgroup.trust/](https://www.nccgroup.trust/globalassets/our-research/us/whitepapers/2016/april/ncc_group_understanding_hardening_linux_containers-1-1.pdf) * __NIST__ Application Container Security Guide Sept. 2017 [http://nvlpubs.nist.gov/](http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-190.pdf) ### Sécurité - outils * Docker Bench for Security [github.com/docker/docker-bench-security](https://github.com/docker/docker-bench-security) * Trivy - Analyse de vulnérabilités sur les images [https://aquasecurity.github.io/trivy/](https://aquasecurity.github.io/trivy/) ### Outils alternatifs pour construire des Images compatibles OCI * Kaniko [https://github.com/GoogleContainerTools/kaniko](https://github.com/GoogleContainerTools/kaniko) * makisu [https://github.com/uber/makisu](https://github.com/uber/makisu) * Buildah [https://github.com/containers/buildah](https://github.com/containers/buildah) * Pas besoin d'élévation de privilèges (Docker build nécessite d'être root). * Utilisation d'un cache distribué pour optimiser les performances sur un cluster de build. * Permet de contrôler les couches générées dans l'image. ### Inspecter les couches d'une image * Dive https://github.com/wagoodman/dive ![dive](images/dive.gif)