{{indexmenu_n>30}} # Gérer proprement son script d'entrypoint Une pratique commune est d'utiliser un script shell en tant qu'`ENTRYPOINT`, qui va effectuer des opérations quelconques (injection de secrets, initialisation de base de données...), puis démarrer le service, sous forme d'un ou de plusieurs processus. ## Entrypoint qui lance un unique processus ### Situation Dans ce cas, le `Dockerfile` spécifie : * Une instruction `ENTRYPOINT` associée à un script shell * Une instruction `CMD`, passée en arguments de l'entrypoint L'entrypoint est exécuté par un shell comme `/bin/sh` ou `/bin/bash`. C'est le premier processus exécuté dans le conteneur, il a donc le PID 1. Traditionnellement, ce script lance le service désigné par `CMD`. Quels sont les risques ? ### Problématique Les scripts ne transmettent pas les signaux à leurs enfants (voir [[technique:docker:good_practices:init|explications ici]]). Le processus désigné par `CMD` ne reçoit donc **jamais** les signaux qui sont envoyés au conteneur. Or, ces signaux peuvent être importants : signal de terminaison, signal de rechargement de configuration... De plus, un processus tué brutalement peut créer des situations indésirables, comme la corruption d'une base de données. Il est donc très important qu'il reçoive les signaux qui lui sont destinés. Supposons un Dockerfile du style : ``` FROM debian:buster-slim ... ENTRYPOINT [ "/entrypoint.sh" ] # Notez qu'on lance nginx en premier plan CMD [ "nginx", "-g", "daemon-off;" ] ``` Avec un entrypoint du style : ```bash #!/bin/sh # Injection de secrets sed [...] # Démarrage de nginx $@ ``` `$@` désigne l'ensemble des arguments passés à l'entrypoint, c'est-à-dire le contenu du `CMD` du `Dockerfile`. Dans ce cas, la situation ressemble à ceci : ``` NOM PID entrypoint.sh 1 |__nginx 2 ``` Comme nous venons de le voir, tout signal envoyé au conteneur arrivera à l'entrypoint, qui n'en fera rien, ce qui conduira Docker à envoyer un `SIGKILL` au bout d'un moment. Comment faire ? ### Utiliser exec Le comportement désiré serait d'exécuter les instruction de l'entrypoint, puis de **remplacer** l'entrypoint par son processus enfant. C'est exactement le but de la commande [exec](https://linux.die.net/man/3/exec). On remplace l'entrypoint par : ```bash #!/bin/sh # Injection de secrets sed [...] # Démarrage de nginx exec $@ ``` Après exécution de l'entrypoint, les processus sont comme suit : ``` NOM PID nginx 1 ``` L'entrypoint a été remplacé par nginx. Il reçoit correctement les signaux. ## Entrypoint qui lance plusieurs services Attention, la philosophie Docker se résume généralement en : *un conteneur, un processus*. En réalité, c'est plutôt **un conteneur, un service**. Il y a donc des cas où il est légitime que plusieurs processus tournent simultanément, mais s'il s'agit de parties bien cloisonnées d'une application (*e.g.* serveur web et serveur applicatif), il est préférable de les faire tourner dans deux conteneurs séparés. Un entrypoint qui lance plusieurs services pourrait ressembler à ceci : ```bash #!/bin/sh # Injection de secrets sed [...] # Lancement de PHP en arrière plan php-fpm & # Lancement de nginx en premier plan exec nginx -g 'daemon off;' ``` Après lancement, la situation est la suivante : ``` NOM PID nginx 1 |__php-fpm 2 ``` Pourquoi ? Tout simplement parce que `nginx` s'est substitué au shell via `exec`, **mais** il a aussi hérité des processus enfants du shell, comme `php-fpm`. Ici, on se retrouve avec une problématique similaire : `nginx` va bien recevoir les signaux, mais ne les transmet pas à ses enfants. Et pour le coup, on ne peut pas ré-utiliser un `exec`, car `nginx` a toujours besoin de s'exécuter. Dans ce cas précis, il va falloir utiliser des astuces plus ou moins sales. Docker propose une [documentation sur ce sujet](https://docs.docker.com/config/containers/multi-service_container/). En gros, il y a deux solutions : * Utiliser [supervisord](http://supervisord.org/). C'est plutôt la moyenne artillerie, mais ça permet de faire ce que l'on veut. `supervisord` est un processus d'initialisation, il va s'exécuter en tant que PID 1 et assumer toutes ses fonctions. C'est ce qui a été retenu pour [pica-nginx](https://gitlab.utc.fr/picasoft/projets/services/nginx). * Faire du cas par cas. Par exemple, si on sait qu'un processus enfant doit pouvoir recevoir un signal important, qu'on connaît à l'avance, on utilisera un `trap`. Les trap sont des *handlers* de signaux. On ne développe pas ce point, mais c'est la solution retenue pour [Mumble](https://gitlab.utc.fr/picasoft/projets/services/mumble). Voici un extrait de son entrypoint : ```bash python3 /exporter.py & # Trap SIGUSR1 signal to reload certificates without restarting _reload() { echo "Caught SIGUSR1 signal!" /usr/bin/pkill -USR1 murmurd wait } trap _reload SIGUSR1 # Run murmur murmurd -v -ini $CONFIG_FILE wait ``` Ici, `python` et `murmurd` sont lancés en arrière plan. Le shell attend ensuite que les processus terminent. Dès qu'un signal `USR1` est reçu, il est capté par l'instruction `trap` et la fonction `_reload` s'exécute. Elle a pour effet de transmettre le signal à Murmur, puis on attend de nouveau. C'est un peu *hackish*, mais dans ce cas spécifique, c'est suffisant car on a pas réellement besoin de transmettre tous les signaux à Murmur. Dans ce cas, un `docker stop` terminera **quand même** en timeout puis en `SIGKILL`, car l'entrypoint ne fait rien du `SIGTERM`. On pourrait implémenter un *handler* pour `SIGTERM` qui l'envoie aux processus enfants, et ce serait même plus propre. C'est laissé à l'appréciation de chacun·e. ## Est-ce-que c'est suffisant ? Dans cette page, nous avons montré comment faire en sorte que les processus d'un conteneur Docker reçoivent les signaux qui leur sont destinés. En revanche, ca ne règle pas tous les problèmes, car les processus qui ont un PID 1 sont traités spécialement. En particulier, un processus avec un PID de 1 n'a pas de comportement par défaut quand il reçoit un signal qu'il ne traite pas explicitement, ce qui peut être problématique. Voir la [[technique:docker:good_practices:init|documentation sur l'utilisation d'un système d'initialisation]] pour parfaire la configuration.