Howto Docker
- Installation
- Utilisation basique
- Astuces
- Configuration
- Plomberie
- Dockerfile
- Docker registry
- Swarm
- Compose/stack (docker stack)
- Déployer (ou mettre à jour à chaud) une nouvelle stack
- Lister les stacks
- Lister les services, toutes stacks confondues ou pour une stack donnée
- Lister les tasks (replicas) d’une stack ou d’un service donné
- Lister toute les tasks de tous les services (sans docker service)
- Supprimer une stack
- Fichier YAML de description de stack (anciennement docker-compose.yml)
- Surcharge de paramètre
- Réseaux (docker network)
- Volumes (docker volume)
- Fichiers de configuration (docker config)
- Fichiers sensibles (docker secrets)
- FAQ
- Les conteneurs ont des problèmes de connectivités entre eux/vers l’extérieur
- Voir les ressources utilisées par les conteneurs
- Espace insuffisant lors du build d’une image
- Voir et modifier le ENTRYPOINT et le CMD d’une image pour déboguer
- Lors d’un redéploiement d’une stack Docker (docker stack deploy), les services ne sont pas redémarrer avec la nouvelle image
- 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é) ?
- Au
sein des conteneurs, un
getent <service>
ougetent tasks.<service>
ne retourne pas l’adresse IP du service alors que celui-ci est bien lancé. - 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) ?
- Comment trouver le PID d’un processus roulant dans un conteneur ?
- Accéder au namespace réseau (ou autres) d’un conteneur
- Accéder aux ports locaux (docker-proxy) avec minifirewall
- Autoriser les conteneurs vers des services de l’hôte
- Restreindre des services dans des conteneurs exposés à l’extérieur
- Problème de résolution DNS dans Docker
- Redémarrer le démon Docker sans redémarrer les conteneurs ?
- Error response from daemon: rpc error: code = Unimplemented desc = unknown method ListServiceStatuses
- Operation not permitted
- Activer le userns-remap
- Docker rootless
- Permettre aux conteneurs d’accéder au service sur l’hote :
- Le conteneur produit des processus zombies
- Restriction générale des ressource de tous les containers
- Documentation : https://docs.docker.com/
- Rôle Ansible : https://gitea.evolix.org/evolix/ansible-roles/src/branch/stable/docker-host
- Statut de cette page : test / bookworm
Docker est une solution qui permet de créer, déployer et gérer des conteneurs Linux. Il intègre une gestion avancée des images permettant de les compiler, les compléter, les héberger, etc.
Installation
Nous utilisons le paquet docker-ce
des dépôts du projet
Docker :
# apt install apt-transport-https
# echo "deb https://download.docker.com/linux/debian bullseye stable" > /etc/apt/sources.list.d/docker.list
# wget -O /etc/apt/trusted.gpg.d/docker.asc https://download.docker.com/linux/debian/gpg
# dos2unix /etc/apt/trusted.gpg.d/docker.asc
# chmod 644 /etc/apt/trusted.gpg.d/docker.asc
# apt update
# apt install docker-ce
# systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
Active: active (running) since Thu 2023-11-02 16:11:27 CET; 2 weeks 5 days ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 1255 (dockerd)
Tasks: 17
Memory: 115.2M
CPU: 3min 43.816s
CGroup: /system.slice/docker.service
└─1255 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
# docker -v
Docker version 24.0.7, build afdd53b
Il faut ajouter un utilisateur dans le groupe docker pour pouvoir interagir avec le démon dockerd sans passer root :
# adduser $USER docker
Utilisation basique
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.
Voici un concentré des commandes les plus utiles :
# docker images
# docker ps -a
# docker logs <ID ou nom du conteneur> -f
# docker inspect <ID ou nom du conteneur> | less
# docker network ls
Gérer les conteneurs
Lister les conteneurs
$ docker ps
Options utiles :
-a : lister tous les conteneurs (y compris ceux qui sont "éteint")
-l : lister les conteneurs récemment lancés
-q : lister uniquement les ID des conteneurs
Pour voir les ressources utilisées par les conteneurs
Instancier un nouveau conteneur
$ docker run <image> [commande]
Si une commande spécifiée, celle-ci remplacera celle du
CMD
de l’image
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
--network NOM : lancer le conterneur sur un réseau docker spécifique existant
--user nom_user|uid : lancer le process du conteneur avec l'utilisateur/uid spécifié
Options de montages
- v <chemin-dossier-hôte>:<chemin-dossier-conteneur>
--mount type=<type>,src=<chemin-hôte>,dst=<chemin-conteneur>[...]
- On peut monter un dossier simplement avec l’option
-v
. - Si on veut monter un fichier, il faut utiliser
--mount type=bind...
( cela marche aussi pour les dossiers ) - Si on veut monter un “volume” docker, il faut utiliser
--mount type=volume,src=<nom-volume>,dst=<chemin-conteneur>
- Si on veut un tmpfs (filesystem en ram) non persitant
--mount type=tmpfs,[tmpfs-size=<taille>,tmpfs-mode=<mode>]
Lancer un conteneur pour faire des tests
# docker run -it --rm debian /bin/bash
root@916438fc1bd0:/# apt update && apt install netcat-openbsd -y
root@916438fc1bd0:/# exit
Le conteneur est supprimé automatiquement lorsqu’on exit bash.
Démarrer un conteneur existant
Un conteneur existant est un conteneur précédemment instancié avec
docker run
.
$ docker start <ID ou nom du conteneur>
Éteindre ou tuer un conteneur
$ docker stop|kill <ID ou nom du conteneur>
Lorsque le conteneur n’est plus en fonction, il existe toujours et
peut être listé à l’aide de la commande docker ps -a
Autostart d’un conteneur
Pour s’assurer qu’un conteneur démarre ou non au démarrage du démon Docker, il existe un paramètre RestartPolicy :
$ docker inspect -f "{{ .HostConfig.RestartPolicy.Name }}" monconteneur
Les valeurs possibles sont :
no Ne redémarre pas automatiquement le conteneur. (défaut)
on-failure Redémarre le conteneur s'il crash suite à une erreur (code de sortie non nul)
always Toujours redémarrer le conteneur s’il s’arrête. S'il est arrêté manuellement, il est redémarré uniquement lorsque le démon Docker redémarre ou que le conteneur lui-même est redémarré manuellement.
unless-stopped Semblable à always, sauf que lorsque le conteneur est arrêté (manuellement ou autrement), il n'est pas redémarré même après le redémarrage du démon Docker.
Pour mettre à jour la politique :
$ docker update --restart=always monconteneur
Supprimer un conteneur
$ docker rm <ID ou nom du conteneur>
Exécuter des commandes dans un conteneur en fonctionnement
$ docker exec <ID ou nom du conteneur> <commande>
Options utiles :
-t : alloue un TTY
-i : attache stdin (mode interactif)
On utilise habituellement la commande suivante pour obtenir un shell dans un conteneur en fonctionnement :
$ docker exec -ti <ID ou nom du conteneur> /bin/sh
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 <ID ou nom du conteneur>
Options utiles :
-f : suivre les logs en direct
-t : afficher un timestamp devant chaque ligne
Afficher les informations d’un conteneur
$ docker inspect <ID ou nom du conteneur>
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.
Il est aussi possible de récupérer une sous partie en utilisant l’argument –format
# Récupérer les IP du container d05daab5c59e
$ docker inspect d05daab5c59e --format "{{range .NetworkSettings.Networks }}{{ .IPAddress }}{{ end }}"
# Récupérer l'IP du container f4bae02ef1407adc92f1aa2cc32c8e9fae75dac87126e2bf4964db265e9ad55d sur l'interface docker_gwbridge
$ docker inspect docker_gwbridge --format "{{ .Containers.f4bae02ef1407adc92f1aa2cc32c8e9fae75dac87126e2bf4964db265e9ad55d.IPv4Address }}"
Note : De notre expérience, l’option –format peut se montrer capricieuse, notamment s’il y a en jeu des identifiant de containers. Une alternative est de parser du json manuellement avec jq
Communication entre un conteneur et son hôte
Exposer un service du conteneur dans l’hôte
Le principe de Docker est de compartimenter ou isoler un service de l’hôte qui l’héberge.
Pour exposer un service du conteneur, on lancera le conteneur avec
l’option -p
pour mapper un port de l’hôte vers le conteneur
:
$ docker run -p <HOST_INTERFACE_IP>:<HOST_PORT>:<CONTAINER_PORT> (…)
On utilisera de préférence 127.0.0.1
comme
<HOST_INTERFACE_IP>
. Si on a besoin d’exposer le
service à l’extérieur, on mettra en place un reverse proxy qui gèrera la
teminaison SSL, avec Nginx par
exemple.
Documentation : https://docs.docker.com/engine/reference/commandline/run/#publish
Fournir un service de l’hôte au conteneur
Pour faire l’inverse, c’est-à-dire permettre au conteneur d’accèder à un service de l’hôte, il faut :
- Faire écouter ce service sur l’IP de l’interface réseau Docker
commune entre l’hôte et le conteneur (par défaut
docker0
). - Autoriser l’IP du conteneur sur ce port dans le firewall de l’hôte.
Gérer les images
Lister les images locales
$ docker image ls
Construire une image
Pour construire ou mettre à jour une image :
$ docker build <repertoire>
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 <tag actuel> <nouveau tag>
Pousser une image sur un dépôt distant
$ docker push <image>
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 <image>
Copier des fichiers dans/depuis un conteneur
$ docker cp <fichier> <conteneur>:<chemin>
On peut aussi le faire de conteneur à conteneur :
$ docker cp <conteneurA>:<fichier> <conteneurB>:<chemin>
Astuces
Éteindre/Tuer/Supprimer tous les conteneurs :
$ docker ps -aq |xargs -r docker stop|kill|rm
Supprimer toutes les images :
Démarrer un conteneur existant avec un shell bash (pour des fins de debug par exemple) :
$ docker run -it IMAGE bash
Configuration
Changer le chemin de stockage
Créer le fichier /etc/docker/daemon.json
et y mettre
:
{
"data-root": "<VOTRE_CHEMIN>",
"storage-driver": "overlay"
}
NB : Anciennement il fallait “graph” à la place de “data-root”.
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
Changer les plages d’ips utilisé par docker
La liste des plages utilisées par défaut est dans le git,
codé en dur avec globalScopeDefaultNetworks
et
localScopeDefaultNetworks
ATTENTION Il faut éviter les collisions avec d’autres réseaux lan
On peut configurer ces plages, pour que les changement sur soient pris en compte, il faudra redemarrer le démon docker.
Docker utilise différentes plages d’ips pour 3 choses différentes :
- Le bridge par défaut
Docker0
( danslocalScopeDefaultNetworks
)
il faut indiquer dans /etc/docker/daemon.json
[...]
,"bip": "172.17.0.1/16"
,"fixed-cidr": "172.17.0.0/16"
bip
est l’ip du bride et fixed-cidr
est la
plage d’ip en notation CIDR (bien mettre en
.0/XX
) utilisable par le bridge
Il est parfois difficile de changer le CIDR utilisé par docker0, on conseil de passer temporairement à un autre CIDR inutilisé, par exemple 10.0.1.1/24, puis de mettre le CIDR voulu. On peut aussi supprimer les bridges custom, cela peut aider.
- Les bridges customs
User-defined bridges
( danslocalScopeDefaultNetworks
) créés par l’utilisateur
[...]
,"default-address-pools":[
{"base":"172.24.0.0/13","size":24},
{"base":"10.200.0.0/16","size":24}
]
Avec default-address-pools
on définit une liste de
plages utilisables pour les bridges customs.
base
est la plage d’ip en notation CIDR (bien mettre en
.0/XX
) utilisable pour créer des bridges
size
est la taille (masque) des bridges
- Avec docker swarm ( dans
globalScopeDefaultNetworks
), voir la section sur swarm plus bas
Plomberie
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.
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
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/
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 <jdoe@example.com>"
# 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 monté 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é, en mode "shell"
CMD /usr/sbin/rsyslogd -n
# Equivalent à CMD ["/bin/sh","-c","/usr/sbin/rsyslogd","-n"] (format JSON)
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.
Ici l’exécutable est spécifié pas l’instruction CMD
mais
ce n’est pas toujours le cas. L’instruction ENTRYPOINT
peut
être utilisé pour spécifier l’exécutable, tandis que CMD permettra de
donner les arguments. Dans ce cas ENTRYPOINT
et
CMD
doivent être spécifiés au format JSON.
À noter qu’il peut y avoir plusieurs instructions
ENTRYPOINT
dans un Dockerfile mais seul la
dernière sera prise en compte.
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
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.
On peut le déployer avec la stack suivante :
version: "3.6"
services:
registry:
image: registry:2
deploy:
replicas: 1
restart_policy:
condition: on-failure
environment:
REGISTRY_HTTP_TLS_CERTIFICATE: /run/secrets/certificate
REGISTRY_HTTP_TLS_KEY: /run/secrets/certificate
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
volumes:
- registry:/var/lib/registry
secrets:
- certificate
ports:
- :5000:5000
volumes:
registry:
secrets:
certificate:
file: certificate.pem
Attention, le certificat doit absolument être valide et le Common Name doit correspondre avec le nom utilisé pour y accéder.
Lors d’un déploiement d’une stack existante, en cas d’erreur :
image registrydocker.example.com:5000/XXX could not be accessed on a registry to record
its digest. Each node will access registrydocker.example.com:5000/XXX independently,
possibly leading to different nodes running different
versions of the image.
Il peut s’agir de différents problèmes (réseau, SSL, etc…), Docker n’est pas très bavard sur la cause de l’échec. Un bon moyen de déboguer la situation est avec :
$ curl https://registrydocker.example.com:5000/v2/_catalog
La requête devrait retourner la liste des images hébergées au format json.
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 <token> <IP du premier node>
- Pour limiter les plages d’ips (pool / range) que peux utiliser
docker pour swarm (notamment pour éviter des conflit avec le 10.0.0.0/8
utilisé par défaut qui pourrait être utilisé ailleur), il faut
initialiser la swarm avec l’option
--default-addr-pool 172.24.0.0/13
avec la pool en notation CIDR. > Attention : on ne pourra pas changer cela aprés que la swarm ait été initialisé ! - Pour indiquer sur quel réseau / adresse les noeuds de la swarm vont
communiquer, il faut ajouter les option
--listen-addr 10.0.0.1:2377 --advertise-addr 10.0.0.1
(par exemple)
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 <manager|worker>
Lister les machines du cluster
# docker node ls
Ajouter des labels à une machine
# docker node update --label-add <clé>=<valeur> <machine>
Les labels servent notamment à définir des contraintes de placement des services lors de l’utilisation de docker stack.
Drainer une machine du cluster
Pour effectuer un maintenance ou autre sur une machine (node), il faut la drainer (déplacer tous ses conteneurs vers les autres noeuds)
# docker node update --availability drain <noeud>
On peut verifier qu’il n’y a plus de services qui tournent sur cette machines #lister-toute-les-tasks-de-tous-les-services-sans-docker-service
Supprimer une machine du cluster
De préférence après avoir drainé le noeud
# docker node rm <hash-machine>
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 <stack_name.yml> <stack name>
Lister les stacks
# docker stack ls
Lister les services, toutes stacks confondues ou pour une stack donnée
# docker service ls
# docker stack services <stack name>
Lister les tasks (replicas) d’une stack ou d’un service donné
# docker stack ps <stack name>
# docker service ps <service name>
Lister toute les tasks de tous les services (sans docker service)
# for stack_name in $(docker stack ls --format "table {{.Name}}");do docker stack ps $stack_name 2>&1 | sed 1d | grep -v "nothing found in stack" ;done ;
# On peut aussi filtrer avec les task uniquement sur un noeud de la swarm
# for stack_name in $(docker stack ls --format "table {{.Name}}");do docker stack ps $stack_name --filter "NODE=nodeName" 2>&1 | sed 1d | grep -v "nothing found in stack" ;done ;
Supprimer une stack
# docker stack rm <stack name>
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/
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
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, chaque bridge est isolée des autre reseaux et docker et permet de resoudre les noms des conteneurs au sein du bridge.
host : Aucune isolation réseaux, le conteneur tourne comme un processus normal dans le namespace par défaut : il partage le réseaux avec son hôte.
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 (chaque conteneur à une mac unique), donc aucun NAT n’est fait contrairement aux précédents.
Attention l’interface de l’hôte doit être configurée pour fonctionner en mode “promiscuité” pour que cela fonctionne.
ipvlan : Comme macvlan sauf que la mac utilisée est celle de l’hôte donc pas besoin d’activer la promiscuité.
none : Isolation complete du conteneur
Créer un réseau :
# docker create -d <driver> […] <network name>
Lister les réseaux créés :
# docker network ls
Informations détaillées sur un réseau :
# docker network inspect <network name>
Pour voir les subnet ou plages d’ips utilisé par chaque réseau :
# for n in `docker network ls --quiet` ; do docker network inspect $n --format "network {{ .Name }} has subnet : {{range .IPAM.Config }}{{ .Subnet }} with gateway {{ .Gateway }}{{ end}}" ; done
Volumes (docker volume)
Les volumes Docker sont des répertoires ou fichiers présents sur l’hôte qui peuvent être montés à l’intérieur des conteneurs. L’intérêt est de pouvoir écrire des données persistantes depuis les conteneurs, qui seront alors conservés après arrêt et suppression du conteneur et accessible depuis l’hôte. Les volumes sont indépendants des conteneurs.
Ils sont généralement utilisés pour stocker des fichiers de logs, des répertoires de bases de données ou des configuration.
Sauf si un chemin est spécifié, Docker mets ses volumes dans /var/lib/docker/volumes/, et les données sont accessibles dans /var/lib/docker/volumes/“nom du volume”/_data/.
On peut gérer les volumes avec la commande docker volume
:
# docker volume create vol1
vol1
# docker volume ls
DRIVER VOLUME NAME
local vol1
# docker volume rm vol1
vol1
Dans le cas d’un cluster Docker Swarm, les volumes ne sont pas répliqués entre les membres du cluster. Si un conteneur monte un volume, il faut bien penser à restreindre son placement à un Docker node spécifique, ou bien mettre en place un système de réplication de fichiers tier.
Fichiers de configuration (docker config)
Les config sont des objets permettant de stocker un unique fichier, généralement un fichier de configuration d’un service.
À la différence des volumes, les config sont répliqués au sein d’un cluster Swarm, ce qui permet de ne pas avoir à copier le fichier de configuration sur chaque serveur. Lors de la création de l’objet config, il sera rendu accessible à tous les membres du cluster qui en auront besoin.
À l’intérieur des conteneurs, les objets config sont montés en lecture seule. Un processus qui a besoin de réécritre lui-même sa configuration durant son exécution ne marchera donc pas, et il faudra préférer l’utilisation d’un volume.
Les config ne peuvent pas non plus être mis à jour lors d’un redéploiement de stack :
failed to update config foo: Error response from daemon: rpc error: code = InvalidArgument desc = only updates to Labels are allowed
Il faut obligatoirement soit supprimer l’objet config et donc les objets qui en dépendent (conteneurs), soit créer un nouveau config (avec un nouveau nom donc) et l’ajouter au conteneur en question (non testé). On perd donc la souplesse des volumes dans ce cas là.
Les config se gèrent avec la commande
docker config
. Le serveur doit avoir le rôle
manager dans le cluster Swarm pour pouvoir créer un objet
config, puisque cela impacte l’ensemble du cluster :
# docker config create vim-config .vimrc
07kaw58mhvtkqem46ipkd97i1
# docker config ls
ID NAME DRIVER CREATED UPDATED
07kaw58mhvtkqem46ipkd97i1 vim-config Less than a second ago Less than a second ago
# docker config rm vim-config
vim-config
Fichiers sensibles (docker secrets)
Ce sont des objets permettant de stocker des fichiers, au même titre que les configs, mais dédiés aux fichiers sensibles comme les clés privée ou fichiers contenant des mots de passe.
Ils sont rendus accessibles dans /run/secrets/ dans les conteneurs.
Les secret héritent des mêmes caractéristiques que les config (voir ci-dessus), à savoir qu’ils sont accessibles partout dans un cluster Swarm mais ne peuvent ni être modifiés depuis un conteneur ni depuis l’hôte, ils doivent être recréés en cas de modification.
Les config se gèrent avec la commande
docker secret
.
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
Voir les ressources utilisées par les conteneurs
On peut voir quelle conteneur prennent le plus de ressources (Cpu, Memoire, Disque) avec docker stats. Par defaut docker stats fonctionne un peu comme htop et met a jour la sortie
$ docker stats [OPTIONS] [CONTAINER...]
Options utiles :
-a : lister tous les conteneurs (y compris ceux qui sont "éteint")
--no-stream : avoir un seule sortie
--no-trunc : ne pas tronquer la sortie (largeur des colonnes)
Avoir une sortie style ps:
docker stats --no-stream --no-trunc
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.
Voir et modifier le ENTRYPOINT et le CMD d’une image pour déboguer
Voir les commandes
$ docker image inspect $nomImage -f "{{ .Config.Entrypoint }}{{ .Config.Cmd}}"
Lancer l’image en changeant le Entrypoint
$ docker run -d --entrypoint="/bin/sleep" $imageConteneur infinity
$ docker run -d --entrypoint="" $imageConteneur echo hello
L’option --entrypoint
remplace le Entrypoint et Cmd
Re-créer l’image en modifiant un conteneur de cette image
$ docker commit -c 'ENTRYPOINT ["/bin/sleep"]' -c 'CMD ["infinity"]' $idConteneur $nomImage:tag
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 <service>
ou
getent tasks.<service>
ne retourne pas l’adresse IP
du service alors que celui-ci est bien lancé.
Stopper le conteneur du service avec un
docker stop <conteneur du service>
. 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 <nom du service>
L’adresse IP des différentes tasks d’un service :
$ getent hosts tasks.<nom du service>
Comment trouver le PID d’un processus roulant dans un conteneur ?
Docker et autres technologies de conteneurs, utilisent des namespace linux pour isoler les processus. L’utilité pgrep(1) est capable de filtrer les processus par namespace sur la base d’un autre processus.
$ pgrep --ns $dockerPID $query
Ce concept provient du système Plan9
Accéder au namespace réseau (ou autres) d’un conteneur
Docker ne met pas le namespace réseau qu’il utilise pour ses
conteneurs dans var/run/netns/
, on ne voit donc rien si on
joue ip netns
car le namespace est dans
/proc/${pid_conteneur}/ns/
.
On peut monter /proc/${pid_conteneur}/ns/
dans
var/run/netns/
pour utiliser ip OU plus simplement utiliser
nsenter à la place :
Nous permet d’avoir un shell dans le namespace, -t
indique d’utiliser le pid target et -n
indique que nous
voulons aller dans le namespace netns
Accéder aux ports locaux (docker-proxy) avec minifirewall
Vous devez autoriser activer DOCKER=on
et autoriser ce
que vous voulez sur l’interface docker0
Dans /etc/default/minifirewall
:
DOCKER='on'
Et dans /etc/minifirewall.d/zzz-custom
:
# On autorise les paquets de retour depuis le conteneur vers docker-proxy
/sbin/iptables -A INPUT -p tcp --dport 1024:65535 -s 172.16.0.0/12 -d 172.16.0.0/12 -m state --state ESTABLISHED -j ACCEPT
# Pour autoriser des docker network supplémentaires
#/sbin/iptables -A INPUT -p tcp --dport 1024:65535 -s 192.168.0.0/20 -d 192.168.0.0/20 -m state --state ESTABLISHED -j ACCEPT
# Pour tout autoriser sur le bridge par défaut
#/sbin/iptables -A INPUT -i docker0 -j ACCEPT
Autoriser les conteneurs vers des services de l’hôte
Pour autoriser la communication de conteneurs vers des services de l’hôte, on utilise les autorisations semi-publics de minifirewall.
Restreindre des services dans des conteneurs exposés à l’extérieur
Pour restreindre l’accès à un service par une adresse IP extérieure,
via /etc/minifirewall.d/zzz-custom
:
# iptables -I MINIFW-DOCKER-INPUT-MANUAL -p tcp -s 192.0.2.42 --dport 5432 -j RETURN
Note : On notera l’importance d’utiliser RETURN à place de ACCEPT
Problème de résolution DNS dans Docker
En général on utilise une stack réseau distincte dans les conteneurs, et donc la résolution DNS dans les conteneurs est assurée par « Docker DNS embedded server » qui va transférer aux serveurs DNS définis sur l’hôte. Si les serveurs DNS de l’hôte fonctionnent bien mais que la résolution DNS ne se fait plus dans les conteneurs, vous pouvez en désespoir de cause redémarrer le démon Docker.
Redémarrer le démon Docker sans redémarrer les conteneurs ?
L’option "live-restore"
à ne pas confondre avec le RestartPolicy permet de redémarrer
le démon Docker sans redémarrer les conteneurs
Important On ne peut pas utiliser cette option pour les services de docker swarm.
Dans la configuration, si
grep "live-restore" /etc/docker/daemon.json
n’est pas à
"live-restore": true
on peut le mettre et reload docker
systemctl reload docker
Après quoi on peut redémarrer le démon docker sans éteindre les
conteneurs systemctl restart docker
.
Error response from daemon: rpc error: code = Unimplemented desc = unknown method ListServiceStatuses
Cette erreur peut arriver quand un noeud de la swarm est dans une version plus ancienne, différente de celle des autres noeuds
# docker node ls
xxxxxxxxxxxxxxx appxx Ready Active Leader 18.09.7
Il faut alors mettre à jour docker sur ce noeud.
Operation not permitted
Si une application executée dans Docker génère des erreurs du style
Operation not permitted
, il est peut être necessaire de
lancer le conteneur avec une (ou les deux) de ces options :
--security-opt seccomp=unconfined
--security-opt apparmor=unconfined
Par exemple, ça nous a déjà permis de corriger l’erreur suivante :
OpenBLAS blas_thread_init: pthread_create failed for thread 1 of 4: Operation not permitted
Activer le userns-remap
Documentation officielle sur userns-remap
Cette option, non active par défaut, permet de lancer les conteneur avec des utilisateur non priviligié qui sont des subuid de l’utilisateur docker-remap. Cela permet d’éviter que un attaquant qui s’echape du conteneur soit root sur l’hote.
Pour activer le userns-remap par défaut dans docker, on peut modifier
/etc/docker/daemon.json pour y ajouter
"userns-remap": "default"
Attention : si on active l’option il faudra relancer docker – docker va creer un nouveau dossier dans son home, par exemple /var/lib/docker/1017504.1017504, celui-ci sera le nouveau home pour TOUS les conteneur – Les conteneurs existant devront donc etre recréé ou migré.
Limitations :
- On ne peut plus utiliser –pid=host ou –network=host lorsque on conteneur est lancé
- On doit spécifier –userns=host pour utiliser l’option –privileged
Tips :
- Pour désactiver le userns pour un conteneur, on peut le lancer avec
l’option –userns=host ou dans docker compose avec
userns_mode: "host"
- Lorsqu’on utilise des volume docker qui n’existent pas, docker va créer le dossier avec docker-remap comme propriétaire, si on ne veut pas qu’il appartienne à docker-remap, il faut le créer à la mains avec le bon propriétaire
Docker rootless
Encore mieux que de lancer les conteneur avec un utilisateur non priviligié avec le userns-remap, on peux lancer le démon docker avec un utilisateur non priviligié.
Le démon docker est lancé avec un rootlesskit dans un user-namespace :
- Le démon docker est piloté par une unitée systemd utilisateur, propre à chaque utilisateur.
- Les conteneur lancé ne seront evidemment pas lancé avec root, mais avec l’utilisateur
Avantages :
- On peut avoir plusieurs démons docker qui s’executent pour chacuns utilisateur ou l’unitée systemd est configuré ( en plus du démon de base qui peut lancer des conteneurs en root). Voir la section docker context pour voir comment manipuler plusieurs démons docker.
- La sécurité de l’hote, un attaquant qui s’echape du conteneur n’est par root sur l’hote.
Inconvénients :
- Les performances réseaux sont fortement degradé, il y a un workaround en béta pour l’instant : bypass4netns
- Comme docker tourne dans un namespace réseau, les ips de conteneurs docker (dans le bridge docker0 notamment) ne sont pas joignable depuis le namespace par défaut de l’hote, il faudra exposer les ports des conteneurs pour y acceder depuis l’hote.
Mise en place sur Debian 12 :
On peut mettre on place docker rootless sur un utilisateur non root, ici l’utilisateur rootless-docker est pris en exemple.
Prérequis :
- Il faut avoir installé docker normalement en suivant les etapes d’installation de ce wiki
- De préference, désactiver docker pour root
# systemctl disable --now docker.service
- Installer les dépendances :
# apt install dbus-user-session fuse-overlayfs slirp4netns docker-ce-rootless-extras uidmap systemd-container
- Permettre la persistence de la session utilisateur :
# loginctl enable-linger rootless-docker
- Pour permettre au conteneur de ping : ajouter
net.ipv4.ping_group_range = 0 2147483647
dans/etc/sysctl.conf
ou/etc/sysctl.d/docker.conf
puis# sysctl --system
Intallation :
- IMPORTANT se connecter avec l’utilisateur via ssh
ou via
# machinectl shell rootless-docker@
- Jouer
dockerd-rootless-setuptool.sh install
la commande devrez nous donner l’emplacement de la socket, faire un exportexport DOCKER_HOST=unix:///run/user/1001/docker.sock
- Activer et lancer l’unitée systemd :
$ systemctl --user enable docker
,$ systemctl --user start docker
- Vérifir que docker tourne bien via systemd et
docker ps
- Bonus : créer un docker context pour pouvoir interagir avec le démon
sans se connecter à l’utisateur rootless-docker
# docker context create docker-rootless --docker "host=unix:///run/user/1001/docker.sock"
Limitations :
- l’ip du conteneur donné dans docker inspect ne peut pas etre résolu car elle est propre au namespace du rootkit.
- Il faudra exposer des port < à 1024 à moins de changer le comportement de linux
Permettre aux conteneurs d’accéder au service sur l’hote :
- C’est possible depuis docker 26 : https://docs.docker.com/engine/release-notes/26.0/#new
- slirp4netns : https://github.com/rootless-containers/slirp4netns/blob/master/slirp4netns.1.md#filtering-connections
Par défaut, slirp4netns (qui gére le namespace réseau) n’est pas
configuré par docker-rootless pour permettre la communication avec
localhost 127.0.0.1
Pour permettre cela il faut configurer
le démon docker avec une variable d’environnement (depuis l’utilisateur
rootless) :
$ systemctl --user edit docker
[Service]
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=false
$ systemctl --user restart docker
Les conteneurs peuvent ensuite communiquer avec un service sur l’hote
en utilisant l’ip 10.0.2.2
dans le conteneur.
Attention cela permet aux conteneurs de communiquer avec n’importe que process en local (qui écoute sur l’hote dans le namespace réseaux par défaut).
Si on veut réduire plus finement les accés on peut utiliser socat ou un pair ethernet virtuel : https://stackoverflow.com/questions/72500740/how-to-access-localhost-on-rootless-docker
Le conteneur produit des processus zombies
C’est parce que le point d’entrée du conteneur Docker (qui tourne en tant que PID 1 dans le PID namespace du conteneur) ne gère pas le fonctionnement spécial attendu du PID 1 sur un OS Unix-like.
Il faut démarrer les conteneurs avec l’argument --init
de docker
pour qu’un init minimal soit inséré dans
le container en tant que PID 1 avec l’entrypoint démarré comme PID
2.
Le fonctionnement spécial attendu est décrit rapidement par le README de
tini
(leinit
de Docker) mais avec un peu plus de détails :
- Le kernel ne fallback pas sur le comportement par défaut des signaux s’ils ne sont pas gérés explicitement par le processus (à l’exception de SIGKILL).
- Les processus qui se terminent/meurent se retrouvent dans l’état “zombie” et doivent être
wait()
par leur parent. Si le parent se termine sans faire ça, le process devient “orphelin” et est déplacé pour avoir PID1 comme parent qui doit alors appeléwait()
sur ce process.La vaste majorité des programmes utilisés comme entrypoint (notamment
sh
,bash
,npm
etjava
) ne gère pas ces particularités.
Restriction générale des ressource de tous les containers
Il est possible de restreindre la consomation de ressources des containers docker d’un point de vue global. On va utiliser les slices de systemd dans ce but.
Exemple pour restreindre la mémoire utilisable par tous les containers à 4G :
# cat /etc/systemd/system/docker-limit-ressources.slice
[Unit]
Description=Slice with limits for Docker
Before=slices.target
[Slice]
MemoryAccounting=true
MemoryLimit=4096M
Puis on ajoutera la directive cgroup-parent
dans le
fichier /etc/docker/daemon.json
avec le nom de notre slice
: ex : "cgroup-parent": "docker-limit-ressources.slice"
.
Pour appliquer les réglages, un coup de
systemctl daemon-reload
puis on peut redémarrer docker
!
# systemctl daemon-reload
# systemctl restart docker
# systemctl status docker-limit-memory.slice
● docker-limit-ressources.slice - Slice with limits for Docker
Loaded: loaded (/etc/systemd/system/docker-limit-ressources.slice; static)
Active: active since Mon 2024-09-30 16:06:58 CEST; 2min 21s ago
Tasks: 43
Memory: 49.8M (limit: 50.0M available: 974.1M)
CPU: 7.799s
CGroup: /docker.slice/docker-limit.slice/docker-limit-memory.slice
├─docker-0ccd10fad70e7205b0bf4b8ec581ed5bd26cbd983632a1c6bf0ad66ada6d758a.scope
....