--- title: Howto Docker categories: docker --- Docker est une solution qui permet de créer, déployer et gérer des conteneurs Linux. Documentation : # Écosystème ## Le démon docker et son client Docker est une application en mode client-serveur. *dockerd* est un démon qui fournit une API REST afin d'interagir avec les conteneurs. *docker* est un client qui permet d'interagir avec le démon en ligne de commande. Il peut interagir avec un démon en local (sur la même machine) ou avec un démon sur une machine distante. ## Les objets dockers ### Image Une image est un template contenant des instructions pour créer un conteneur docker. Ces instructions sont listées dans un fichier nommé *Dockerfile*. La plupart du temps, une image se base sur une autre image ce qui crée un système de couches. Lorsqu'on modifie une image, seules les couches qui sont modifiées sont reconstruites. Une fois qu'une image est créée on peut la publier dans un *registry* (`docker push`). ### Conteneur Un conteneur est une instance exécutable d'une image. ### Stack, service et task Lorsque Docker fonctionne en _swarm mode_ (en cluster), les notions de _stack_, _service_ et _task_ sont introduites, en plus des précédentes. Un _service_ est un objet qui contient des informations comme l'image Docker à instancier, des contraintes de placement ou de limitation de ressources, des objets à lier au conteneur qui sera lancé (volumes, réseau, etc…), le nombre de réplicas à démarrer, etc… Les services se manipulent avec la commande `docker service`, mais généralement on les définit dans un fichier YAML. On peut définir plusieurs services dans un fichier YAML ainsi que d'autres objets dont les services font référence (_volume_, _network_, _secret_, _config_, etc…). Cet ensemble représente alors une _stack_, que l'on peut manipuler avec la commande `docker stack`. Un _service_ démarre donc un ou plusieurs réplicas. Ces réplicas sont appelés des _tasks_. Chaque _task_ lance un et un seul conteneur. Les conteneurs étant des processus Docker indépendant, l'ordonnanceur de Docker Swarm introduit la notion de _task_ afin de manipuler les conteneurs. Concrètement, une _task_ représente un conteneur et un état dans lequel il est (_running_, _failed_, _stopped_ et des états transitoires). Sur le même principe qu'une unité systemd, monit ou supervisord peut surveiller ses processus et les redémarrer en cas de besoin, une _task_ Docker se comporte de la même manière avec son conteneur. Voir : [https://docs.docker.com/engine/swarm/how-swarm-mode-works/services/#services-tasks-and-containers](https://docs.docker.com/engine/swarm/how-swarm-mode-works/services/#services-tasks-and-containers) ## docker registry Un *registry* sert à héberger des images docker. Il existe des registres publics tels que *docker hub* ou *docker cloud* mais il est possible d'héberger son propre *registry*. # Docker Engine ## Installation Il est conseillé d'utiliser le paquet docker-ce des dépôts du projet Docker : ~~~ # apt install apt-transport-https # echo "deb http://download.docker.com/linux/debian stretch stable" > /etc/apt/sources.list.d/docker.list # curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - # apt update # apt install docker-ce ~~~ > *Note* : Pour Debian 8 : > > ~~~ > # echo "deb http://apt.dockerproject.org/repo debian-jessie main" > /etc/apt/sources.list.d/docker.list > # apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D > # apt update > # apt install docker-engine > ~~~ Sur sa machine de travail, il est utile d'ajouter son utilisateur dans le groupe _docker_ pour pouvoir interagir avec le démon _dockerd_ sans passer root à chaque fois : ~~~ # adduser $USER docker ~~~ ### Configuration #### Changer le chemin de stockage Créer le fichier `/etc/docker/daemon.conf` et y mettre : ~~~ { "graph": "", "storage-driver": "overlay" } ~~~ ### Ansible Le rôle docker-host permet d'installer le docker-engine sur un hôte distant et de l'exposer à l'externe ou non. La documentation concernant l'utilisation du rôle est située dans son répertoire. #### TLS Lorsque le docker-engine est exposé, il est important de le sécuriser avec TLS. Au moment de l'installation, une version altérée de shellpki est copiée dans le répertoire docker/tls. Ensuite, les certificats et la clé sont créés pour le serveur. (`shellpki init`) Pour autoriser des hôtes à se connecter à l'engine, il faut leur créer une clé et un certificat. Pour ce faire, il suffit de lancer le script: ~~~ /home/docker/tls$ ./shellpki create ~~~ Les fichiers seront créés, par défaut, dans le répertoire `/home/docker/tls/files/$CN` ## Utilisation de base Une image Docker contient un système minimal avec un ou plusieurs services. Un conteneur quant à lui est une instance (créée à partir d'une image) en cours d'exécution. ### Gérer les conteneurs #### Lister les conteneurs ~~~ $ docker ps ~~~ Options utiles : ~~~ -a : lister tous les conteneurs -l : lister les conteneurs récemment lancés -q : lister uniquement les ID des conteneurs ~~~ #### Instancier un nouveau conteneur ~~~ $ docker run [commande] ~~~ Options utiles : ~~~ --name="nom" : donner un nom au conteneur -p port_hôte:port_conteneur : rendre un port accessible depuis l'hôte -d : lancer le conteneur en mode 'détaché' -it : lancer le conteneur en mode intéractif avec tty ~~~ #### Démarrer un conteneur existant Un conteneur existant est un conteneur précédemment instancié avec `docker run`. ~~~ $ docker start ~~~ #### Éteindre ou tuer un conteneur ~~~ $ docker stop|kill ~~~ Lorsque le conteneur n'est plus en fonction, il existe toujours et peut être lister à l'aide de la commande `docker ps -a` #### Supprimer un conteneur ~~~ $ docker rm ~~~ #### Exécuter des commandes dans un conteneur en fonction ~~~ $ docker exec ~~~ Options utiles : ~~~ -t : alloue un TTY -i : attache stdin (mode interractif) ~~~ On utilise habituellement la commande suivante pour obtenir un shell dans un conteneur en fonction : ~~~ $ docker exec -ti bash ~~~ #### Visionner les journaux d'un conteneur Il s'agit en fait de la sortie standard et la sortie d'erreur du processus lancé à l'intérieur du conteneur : ~~~ $ docker logs ~~~ Options utiles : ~~~ -f : suivre les logs en direct -t : afficher un timestamp devant chaque ligne ~~~ #### Afficher les informations d'un conteneur ~~~ $ docker inspect ~~~ Cette commande s'applique généralement à n'importe quel objet Docker (conteneur, image, service, réseau…) et donne une liste exhaustive des attributs de l'objet, formaté en JSON. ### Gérer les images #### Lister les images locales ~~~ $ docker image ls ~~~ #### Construire une image Pour construire ou mettre à jour une image : ~~~ $ docker build ~~~ Le répertoire doit contenir un fichier _Dockerfile_ décrivant l'image à construire. Option utiles : ~~~ -t : ajoute un tag à l'image ~~~ #### Ajouter un tag à une image existante ~~~ $ docker tag ~~~ #### Pousser une image sur un dépôt distant ~~~ $ docker push ~~~ Avant de pousser une image, il est nécessaire de lui attribuer le bon _tag_ qui doit contenir l'adresse du dépôt distant. Par exemple pour pousser l'image _foo-image_ sur le dépôt Docker _registry.example.net:5000_ : ~~~ $ docker tag foo-image registry.example.net:5000/foo-image $ docker push registry.example.net:5000/foo-image ~~~ #### Récupérer une image d'un dépôt distant ~~~ $ docker pull ~~~ ### Astuces Éteindre/Tuer/Supprimer tous les conteneurs : ~~~ $ docker ps -aq |xargs -r docker stop|kill|rm ~~~ Supprimer toutes les images : ~~~ $ docker images -q |xargs -r docker image rm ~~~ Démarrer un conteneur existant avec un shell bash (pour des fins de debug par exemple) : ~~~ $ docker run -it IMAGE bash ~~~ ## Dockerfile Les fichiers _Dockerfile_ décrivent les étapes de construction d'une image. Ils permettent de reconstruire à l'identique votre image et de connaitre exactement ce qui a été fait. Ainsi au lieu de distribuer une image potentiellement volumineuse, on distribue uniquement la procédure de construction (_Dockerfile_) et les quelques fichiers annexes. Référence pour la syntaxe des fichiers _Dockerfile_ : [https://docs.docker.com/engine/reference/builder/](https://docs.docker.com/engine/reference/builder/) ### Procédure de création d'une nouvelle image Exemple avec une image exécutant rsyslog : - depuis un répertoire vierge, création d'un fichier _Dockerfile_ : ~~~ ~/docker-images/rsyslog $ $EDITOR Dockerfile # Image sur laquelle notre image se base FROM debian:stretch # Champs optionnels LABEL maintainer="John Doe " # Installation des paquets voulus ENV DEBIAN_FRONTEND noninteractive RUN apt-get update \ && apt-get install -y --no-install-recommends rsyslog procps \ && rm -rf /var/lib/apt/lists/* # Configuration de rsyslog. On peut modifier la configuration directement ou # bien copier des fichiers de notre machine RUN sed -i 's/^#\(module(load="imudp")\)/\1/; s/^#\(input(type="imudp" port="514")\)/\1/' /etc/rsyslog.conf COPY custom.conf /etc/rsyslog.d/ # /var/log/ est un volume qui doit être monter depuis l'extérieur lors de # l'exécution du conteneur VOLUME /var/log/ # Le port 514/udp est rendu public à l'extérieur du conteneur à son exécution EXPOSE 514/udp # La commande suivante est exécutée lorsque le conteneur est exécuté CMD /usr/sbin/rsyslogd -n ~~~ Dans la mesure du possible, voici quelques bonnes pratiques à respecter : - la commande spécifiée par _CMD_ doit s'exécuter en avant plan et ne pas forker. Si la commande rend la main, le conteneur sera alors arrêté ; - la commande doit envoyer ses logs sur stdout et/ou stderr. Cela permet de les consulter directement à l'aide de `docker logs`. Un exemple à ne **pas** écrire : ~~~ CMD /usr/bin/foo -d; tail -f /var/log/foo.log ~~~ Dans le cas où `/usr/bin/foo -d` lance le démon foo en arrière plan, le conteneur s'exécutera correctement mais Docker va monitorer le processus tail et non plus foo. Si foo crash, le conteneur ne passera pas en _failed_ et ne pourra pas être redémarré automatiquement. À supposer que foo ne puisse pas envoyer ses logs sur stdout, le bon example serait : ~~~ CMD touch /var/log/foo.log && tail -f /var/log/foo.log & /usr/bin/foo ~~~ À noter que seulement une seule directive `CMD` est acceptée dans un _Dockerfile_. On peut ensuite construire notre image, en lui donnant ici le _tag_ _rsyslog_ : ~~~ ~/docker-images/rsyslog $ ls Dockerfile custom.conf ~/docker-images/rsyslog $ docker build -t rsyslog . ~/docker-images/rsyslog $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE rsyslog latest 4bea99cda08c 8 minutes ago 470MB debian stretch 5b712ae16dd7 3 days ago 100MB ~~~ ## Utilisation avancée ### Swarm Swarm permet de mettre en communication plusieurs hôtes Docker afin d'en former un cluster. On pourra ainsi déployer des applications multi-conteneurs sur plusieurs machines. #### Initialiser le cluster ~~~ docker0# docker swarm init ~~~ Joindre les autres machines au cluster créé (il vous suffit généralement de copier-coller la commande retournée par `docker swarm init` : ~~~ docker1# docker swarm join --token ~~~ Par défaut la machine sur laquelle le cluster a été initialisée a le rôle de _manager_, et les suivantes ont le rôle de _worker_. On ne peut déployer de nouveaux services que depuis les _managers_. Les _workers_ se contentent de recevoir les services à rouler. Pour ajouter des machines plus tard, il suffit de générer un nouveau token : ~~~ docker0# docker swarm join-token ~~~ #### Lister les machines du cluster ~~~ # docker node ls ~~~ #### Ajouter des labels à une machine ~~~ # docker node update --label-add = ~~~ Les _labels_ servent notamment à définir des contraintes de placement des services lors de l'utilisation de _docker stack_. ### Compose/stack (docker stack) Docker permet de déployer des infrastructures multi-conteneurs (_stacks_) simplement à l'aide de `docker stack` (anciennement Docker Compose, logiciel tier). Il est très utile dans le cadre de déploiement sur un cluster Swarm. L'infra est à décrire dans un fichier YAML. Déployer (ou mettre à jour à chaud) une nouvelle _stack_ : ~~~ # docker stack deploy -c ~~~ Lister les _stacks_ : ~~~ # docker stack ls ~~~ Lister les _services_, toutes _stacks_ confondues ou pour une _stack_ donnée : ~~~ # docker service ls # docker stack services ~~~ Lister les _tasks_ (replicas) d'une _stack_ ou d'un _service donnée : ~~~ # docker stack ps # docker service ps ~~~ Supprimer une _stack_ : ~~~ # docker stack rm ~~~ ### Réseaux (docker network) Docker permet de gérer différentes topologies de réseaux pour connecter les conteneurs entre eux à l'aide de `docker network`. _drivers_ réseau supportés : - _bridge_ : utilise les bridges Linux ; - _overlay_ : utilisé dans le cas d'un cluster Swarm, permet d'avoir un réseau unique partagé entre tous les hôtes Docker et permet de faire du load-balancing entre les conteneurs (replicas) d'un service ; - _macvlan_ : permet d'assigner directement des adresses IP publiques aux conteneurs, donc aucun NAT n'est fait contrairement aux précédents. Créer un réseau : ~~~ # docker create -d […] ~~~ Lister les réseaux créés : ~~~ # docker network ls ~~~ Informations détaillées sur un réseau : ~~~ # docker network inspect ~~~ ### Volumes (docker volume) ### Fichiers de configuration (docker config) ### Fichiers sensibles (docker secrets) ### Fichier YAML de description de _stack_ (anciennement _docker-compose.yml_) Les _stacks_ Docker se décrivent à l'aide d'un fichier YAML. Anciennement le déploiement d'une _stack_ se faisait à l'aide de Docker-compose et le fichier s'appelait couramment _docker-compose.yml_. Docker stack ne définit pas de nom par défaut donc il peut porter n'importe quel nom. Le fichier contient une description de tous les objets Docker à créer pour déployer une _stack_ de zéro. Tout ce que l'on peut faire avec le fichier YAML peut être fait avec les commandes docker équivalentes (`docker service`, `docker volume`, `docker config`, etc…). Le nom des commandes et options sont exactement les mêmes. Le format YAML permet simplement de rendre plus simple la description d'une _stack_ qu'une série de commandes. Référence sur le format du fichier : [https://docs.docker.com/compose/compose-file/](https://docs.docker.com/compose/compose-file/) Voici un aperçu : ~~~ $ cat ma-stack.yml version: "3.6" services: web: image: my-website:latest deploy: replicas: 2 restart_policy: condition: on-failure ports: - "80:80" - "443:443" environment: - MYSQL_DB=foo_dev - MYSQL_USER=foo_dev - MYSQL_PASS=deb2Ozpifut? secrets: - ssl_cert configs: - source: nginx target: /etc/nginx/nginx.conf mysql: image: mariadb:latest deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.labels.role == sql volumes: - mysql-datadir:/var/lib/mysql volumes: mysql-datadir: configs: nginx: file: nginx.conf secrets: ssl_cert: file: example.com.pem ~~~ On lance ici 2 _services_, _web_ et _mysql_. On spécifie que _web_ doit avoir 2 réplicas, il y aura donc 2 _tasks_ (et donc 2 conteneurs) qui seront démarrés, peu importe où sur le cluster. Pour le service _mysql_ par contre, on spécifie une contrainte de placement de la _task_, elle doit être démarré sur une machine du cluster ayant le label _role == sql_. La raison est que comme on lui a associé un volume pour son _datadir_, il doit toujours être exécuter sur la même machine (les volumes ne sont pas répliqués entre les machines d'un cluster). On ne spécifie pas de chemin pour le _volume_ _mysql_datadir_, donc Docker le créera par défaut dans _/var/lib/docker/volumes/ma-stack_mysql-datadir/ sur la machine hôte. Le _service_ _web_ à besoin d'une _config_ appelée _nginx_ et d'un _secret_ appelé _ssl_cert_ que l'on déclare tout en bas et qui contiennent respectivement le fichier _nginx.conf_ et _example.com.pem_ dans notre répertoire courant, à côté du _ma-stack.yml_. Lorsque Docker crée une _config_ ou un _secret_ (au déploiement de la _stack_), il les rend disponible à tous les membres du cluster, ce qui fait qu'on n'a pas besoin de spécifier de contrainte de placement pour _web_. Ensuite on peut déployer notre _stack_ : ~~~ $ ls ma-stack.yml nginx.conf example.com.pem $ docker stack deploy -c ma-stack.yml ma-stack ~~~ #### Surcharge de paramètre On peut surcharger certains paramètres définit dans le premier en créant un second fichier contenant seulement les paramètres à surcharger. Les 2 fichiers devront être passer en paramètre de `docker stack deploy` et ils seront alors fusionnés. Cela permet de réutiliser un fichier de _stack_ pour différents environnement (preprod, prod…) en changeant uniquement des variables d'environement, mots de passe, etc… ~~~ $ cat ma-stack.dev.yml version: "3.6" services: web: environment: - DEBUG=1 - MYSQL_DB=foo_dev - MYSQL_USER=foo_dev - MYSQL_PASS=deb2Ozpifut? ~~~ On peut ensuite déployer ainsi : ~~~ $ docker stack deploy -c ma-stack.yml -c ma-stack.dev.yml ma-stack ~~~ ## FAQ > Les conteneurs ont des problèmes de connectivités entre eux/vers l'extérieur C'est très probablement lié à un outil manipulant les règles netfilter qui a effacé les règles spécifiques à Docker, notamment dans la table _nat_. Pour restaurer les règles netfilter de Docker, il n'y a pas d'autre moyen que de redémarrer le démon : ~~~ # /etc/init.d/docker restart ~~~ > Espace insuffisant lors du build d'une image Solutions: - Vérifier que le "build context" n'est pas trop grand. - Modifier la variable d'environnement DOCKER_TMPDIR . - Créer un fichier .dockerignore pour exclure des fichiers et répertoires du "build context" *Build context: Tout ce qui se trouve à la racine du Dockerfile.* > Lors d'un redéploiement d'une stack Docker (docker stack deploy), les services ne sont pas redémarrer avec la nouvelle image Vérifier que le tag _latest_ est bien précisé dans le nom de l'image dans le _docker-stack.yml_ : ~~~ image: registrydocker.example.com:5000/foo:latest ~~~ > Est-il possible de ne faire écouter un service d'une stack que sur une interface précise de la machine hôte (par exemple sur un LAN privé) ? Non, Docker ne supporte pas ça. Il faut bloquer le port en question dans le pare-feu, dans la chaîne iptables DOCKER-USER. Pour bloquer l'accès au _registry_ Docker depuis l'extérieur par exemple : ~~~ # iptables -A DOCKER-USER -i eth0 -p tcp -m tcp --dport 5000 -j DROP ~~~ > Au sein des conteneurs, un `getent ` ou `getent tasks.` ne retourne pas l'adresse IP du service alors que celui-ci est bien lancé. Stopper le conteneur du service avec un `docker stop `. Docker stack devrait le relancer automatiquement. > Comment obtenir l'adresse IP des _tasks_ d'un _service_ (dans le cadre de l'utilisation de Docker stack) au sein de conteneur (à des fins de debug) ? L'adresse IP virtuelle qui redirige aléatoirement sur chacune des _tasks_ (si le réseau utilise le driver _overlay_, cas par défaut) : ~~~ $ getent hosts ~~~ L'adresse IP des différentes _tasks_ d'un service : ~~~ $ getent hosts tasks. ~~~