Docker Compose : command ou entrypoint, quelle différence
ENTRYPOINT lance le programme et command fournit ses arguments. Découvrez les 4 combinaisons d’override et pourquoi entrypoint efface le CMD de l’image.
Différence entre la commande et l’entrypoint dans Docker Compose, en une règle
Dans Docker Compose, entrypoint: définit le programme exécuté et command: définit les arguments transmis à ce programme. Le processus du conteneur correspond à la liste de l’entrypoint, suivie de la liste de la commande. Tous les autres comportements décrits sur cette page découlent de cette règle.
Ces deux clés correspondent à deux instructions du Dockerfile. entrypoint: remplace le ENTRYPOINT de l’image. command: remplace le CMD de l’image. Elles ne sont pas indépendantes, ce qui est souvent source de confusion : définir entrypoint: supprime également le CMD de l’image. La spécification Compose l’indique explicitement. Si entrypoint n’est pas null, Compose ignore la commande par défaut de l’image.
Lisez ce que l’image déclare déjà
Avant de remplacer quoi que ce soit, examinez ce que l’image fournit.
docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16Vous obtenez ["docker-entrypoint.sh"] et ["postgres"] : le conteneur exécute donc docker-entrypoint.sh postgres. Ce script crée le répertoire de données au premier démarrage, lit les variables POSTGRES_*, abandonne les privilèges au profit de l’utilisateur postgres, puis exécute les arguments qui lui ont été transmis. La décision consiste entièrement à déterminer quelle partie vous souhaitez modifier. Pour transmettre une option à la base de données, vous remplacez command:. Si vous remplacez entrypoint:, toute cette initialisation est ignorée.
Les quatre combinaisons, présentées sur une image minimaliste
Construisez une image dont le seul rôle est d’afficher la liste des arguments avec lesquels elle a été démarrée.
FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]docker build -t argdemo .services:
demo:
image: argdemoExécutez docker compose up après chaque modification et lisez l’unique ligne qu’il consigne.
- Aucune clé définie. Le processus est
/bin/echo ep cmdet le journal afficheep cmd. command: ["cmd2"]uniquement. Le processus est/bin/echo ep cmd2. L’entrypoint reste inchangé et seuls les arguments sont modifiés.entrypoint: ["/bin/echo", "ep2"]uniquement. Le processus est/bin/echo ep2et le journal afficheep2. Lecmdde l’image disparaît, sans aucun avertissement.- Les deux clés définies. Le processus est
/bin/echo ep2 cmd2. C’est le seul cas où vous contrôlez la totalité de la liste des arguments.
Pourquoi la définition de l’entrypoint efface le CMD de l’image
Le CMD d’une image est défini comme la liste d’arguments par défaut de son ENTRYPOINT. Si vous remplacez l’entrypoint, ces arguments appartiennent désormais à un programme qui ne s’exécute plus. Compose les supprime donc au lieu de construire une ligne de commande que l’auteur de l’image n’a jamais prévue. docker run --entrypoint se comporte de la même manière. Il s’agit donc du comportement de Docker, pas d’une particularité de Compose.
La conséquence est concrète. nginx:1.27 déclare ENTRYPOINT ["/docker-entrypoint.sh"] et CMD ["nginx", "-g", "daemon off;"]. Définissez entrypoint: /custom-init.sh et votre script démarre avec une liste d’arguments vide. Un script qui se termine par le exec "$@" habituel n’a alors rien à exécuter. exec ne fait rien, le script atteint sa dernière ligne, puis le conteneur se termine avec le code 0, sans message d’erreur.
Remettez vous-même les arguments :
services:
web:
image: nginx:1.27
entrypoint: /custom-init.sh
command: ["nginx", "-g", "daemon off;"]La règle à retenir : chaque fois que vous définissez entrypoint:, déterminez dans la même modification ce que doit être command:.
Forme exec et forme shell, et différences avec Compose
Un Dockerfile accepte deux syntaxes. CMD ["nginx", "-g", "daemon off;"] est la forme exec : le binaire s’exécute directement, sans passer par un shell. CMD nginx -g "daemon off;" est la forme shell : Docker la réécrit en /bin/sh -c 'nginx -g "daemon off;"', donc un shell s’exécute d’abord et votre programme devient son processus enfant.
Compose ne reprend pas cette règle, ce qui surprend souvent. Une chaîne dans command: est découpée en arguments, puis exécutée directement, sans wrapper /bin/sh -c. La documentation de référence de Compose est explicite : le champ command ne s’exécute pas dans le contexte SHELL défini dans l’image. Si vous avez besoin des fonctionnalités d’un shell, vous devez donc en invoquer un explicitement.
C’est pourquoi command: echo "hello $$HOSTNAME" affiche le texte littéral hello $HOSTNAME. Aucun shell n’a reçu la chaîne, donc rien ne l’a développée. Demandez l’exécution par un shell lorsque vous en avez besoin :
services:
demo:
image: alpine:3.20
command: /bin/sh -c 'echo "hello $$HOSTNAME"'Signaux, PID 1 et arrêt propre avec docker compose down
docker compose stop et docker compose down envoient SIGTERM au PID 1 de chaque conteneur, attendent stop_grace_period, puis envoient SIGKILL. Le délai de grâce par défaut est de 10 secondes.
Le PID 1 est particulier sous Linux. Le noyau n’applique pas l’action par défaut d’un signal au PID 1. Un processus qui n’installe aucun gestionnaire SIGTERM ignore donc simplement SIGTERM lorsqu’il s’exécute en tant que PID 1. Il reste actif pendant tout le délai de grâce, puis est tué de force. Toute connexion ouverte ou transaction non validée est alors interrompue.
Placer un shell devant votre programme augmente ce risque, car le shell devient le PID 1 et la plupart des shells ne transmettent pas les signaux à un processus enfant. Certains shells se remplacent par la commande finale dans une chaîne -c. Votre programme peut donc parfois devenir le PID 1. Cela dépend du shell et de la chaîne exacte. Ne le déduisez pas. Vérifiez-le :
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echoSi le PID 1 s’affiche comme /bin/sh -c ... au lieu de votre programme, vous avez deux solutions. Utilisez la forme exec dans l’image, ou conservez le shell et transférez-lui le processus avec exec :
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec remplace le processus du shell par votre programme au lieu de créer un processus enfant. Votre programme devient donc le PID 1 et reçoit le signal.
Certains programmes créent des processus enfants sans jamais les réattribuer, ce qui laisse des processus zombies, car le PID 1 est également chargé de les réattribuer. Compose propose une option pour cela :
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true exécute un petit processus init en tant que PID 1. Celui-ci transmet les signaux à votre processus et réattribue les processus enfants. stop_grace_period accorde davantage de temps à un arrêt réellement lent. Si votre programme attend un autre signal, stop_signal: SIGQUIT modifie le signal envoyé par Compose. Vérifiez ce que demande déjà une image avec docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27.
Si une stack prend systématiquement dix secondes par service avec docker compose down, cela indique que rien ne gère SIGTERM. Corrigez ce problème avant d’incriminer les outils, puis consultez la différence entre docker compose down et stop pour savoir ce que chaque sous-commande supprime.
La distinction entre exec et shell apparaît également à un autre endroit. Un healthcheck écrit sous la forme test: ["CMD", "curl", "-f", "http://localhost/"] exécute directement le binaire, tandis que test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] passe par un shell afin que || ait un effet. Écrire des healthchecks Compose qui échouent correctement présente les autres éléments de ce champ.
Ajouter un flag à une image officielle
C’est la raison principale pour laquelle la plupart des lecteurs sont ici. Vous voulez ajouter un flag à postgres sans perturber le script d’initialisation.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
command: postgres -c max_connections=200 -c shared_buffers=256MB
volumes:
pgdata:Seul command: a été modifié. docker-entrypoint.sh continue donc de s’exécuter et lance toujours la commande que vous lui avez fournie. Vérifiez le résultat au lieu de le supposer :
docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'La sortie doit afficher 200. Si elle affiche encore 100, exécutez docker compose config et vérifiez que le command attendu apparaît dans la sortie fusionnée. Compose fusionne les fichiers d’override en remplaçant entièrement command, et non en y ajoutant du contenu. Un second fichier qui définit également command: prend donc silencieusement le dessus.
Le ${POSTGRES_PASSWORD} ci-dessus est développé par Compose sur l’hôte à partir de votre fichier .env, avant la création du conteneur. Fichiers d’environnement et secrets dans Compose explique où cette valeur peut être stockée en toute sécurité.
Exécuter une migration ponctuelle avec docker compose run
docker compose run crée un nouveau conteneur à partir de la même définition de service et remplace la commande par tout ce que vous saisissez après le nom du service. L’entrypoint de l’image est toujours exécuté. Le conteneur est donc préparé exactement comme celui qui fonctionne en continu.
docker compose run --rm app python manage.py migrate--rmsupprime le conteneur lorsque la commande se termine. Sans cette option, chaque exécution laisse derrière elle un conteneur arrêté, visible dansdocker compose ps -a.- Les ports ne sont pas publiés. Un conteneur
runignore leports:du service, sauf si vous ajoutez--service-ports. Il ne peut donc pas entrer en conflit avec le service déjà démarré. - Les dépendances sont démarrées en premier. Tout ce qui figure dans
depends_onest démarré avant votre commande, tandis que--no-depsdésactive ce comportement. - Le conteneur reçoit un nom généré, comme
myproject-app-run-9f2c1a. Il n’entre donc jamais en conflit avec le conteneur du service.
Pour remplacer également l’entrypoint, utilisez l’option suivante :
docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'La liste d’arguments obtenue est /bin/sh -c 'python manage.py migrate', car les mots placés après le nom du service restent la commande. docker compose exec est l’autre outil, et son fonctionnement est différent : il exécute un processus dans un conteneur déjà démarré et ignore entièrement entrypoint: et command:. Utilisez run pour une tâche qui nécessite un conteneur neuf, et exec pour inspecter un conteneur en cours d’exécution. La fiche mémo des commandes Compose présente les autres sous-commandes côte à côte.
Pourquoi mon conteneur s’arrête-t-il immédiatement ?
Commencez par le code de sortie, car il permet de cibler rapidement la cause.
docker compose ps -a
docker compose logs appCode de sortie 0 et aucune sortie. La commande s’est exécutée puis terminée. La cause la plus fréquente est un remplacement de entrypoint: qui a également supprimé le CMD de l’image. L’entrypoint s’est alors exécuté avec une liste d’arguments vide et n’avait rien à transmettre.
Une erreur qui se termine par permission denied. Le script n’a pas le bit d’exécution dans l’image. Ce bit n’a généralement jamais été défini sur le fichier du dépôt. Définissez-le au moment du build avec COPY --chmod=0755 entrypoint.sh /entrypoint.sh.
Une erreur qui se termine par no such file or directory pour un fichier pourtant visible dans l’image. Le script utilise des fins de ligne Windows. Sa première ligne contient alors #!/bin/sh suivi d’un octet de retour chariot. Le noyau recherche donc un interpréteur dont le nom contient cet octet, sans en trouver. Exécutez dos2unix entrypoint.sh, puis ajoutez * text eol=lf à .gitattributes pour éviter que le problème ne réapparaisse.
executable file not found in $PATH. Le binaire indiqué dans command: n’est pas présent dans l’image, ou vous avez indiqué une commande interne du shell telle que cd alors qu’un programme réel est requis.
Obtenir un shell dans une image dont l’entrypoint échoue
Lorsque l’entrypoint s’arrête avant que vous puissiez effectuer la moindre vérification, remplacez-le :
docker compose run --rm --entrypoint /bin/sh appSi cette commande renvoie executable file not found in $PATH, l’image ne contient aucun shell. Les images Distroless et celles basées sur scratch n’en fournissent souvent pas. Vous pouvez tout de même lire le système de fichiers depuis l’extérieur, sans démarrer l’entrypoint :
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probeLorsque le conteneur doit rester actif pour pouvoir vous y connecter plusieurs fois, faites-le tourner avec un processus qui ne se termine jamais. Placez cette configuration dans un fichier d’override que vous ne committez pas :
services:
app:
entrypoint: ["tail", "-f", "/dev/null"]
command: []command: [] n’est pas strictement nécessaire, car la définition de entrypoint: a déjà supprimé la valeur de CMD de l’image. L’écrire indique toutefois clairement l’intention à la prochaine personne qui lira le fichier. Démarrez le conteneur, puis ouvrez un shell à l’intérieur :
docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/shExécutez maintenant le véritable entrypoint manuellement et surveillez l’endroit où il s’arrête. Le message d’erreur s’affiche ainsi dans votre terminal, au lieu d’apparaître dans un conteneur qui s’est arrêté une demi-seconde plus tôt. Si vous assemblez encore votre première stack, une première stack Compose sur un VPS présente l’organisation des fichiers supposée par tout ce qui précède.
FAQ
Pourquoi mon conteneur s’arrête-t-il immédiatement après docker compose up ?
Vérifiez docker compose ps -a pour connaître le code de sortie. Une sortie 0 sans résultat indique généralement que vous avez défini entrypoint: sur le service. Cela a également supprimé CMD de l’image. L’entrypoint s’est donc exécuté avec une liste d’arguments vide, puis s’est terminé. Ajoutez de nouveau les arguments avec command:. Une erreur qui se termine par permission denied indique que le script d’entrypoint n’a pas le bit d’exécution. Une erreur qui se termine par no such file or directory pour un fichier existant indique que le script utilise des fins de ligne Windows. Sa ligne shebang désigne donc un interpréteur qui n’existe pas.
La définition de l’entrypoint dans Compose supprime-t-elle le CMD de l’image ?
Oui. Si entrypoint n’est pas nul, Compose ignore la commande par défaut déclarée par l’image. Ce comportement est documenté et correspond à docker run --entrypoint. La raison est que CMD d’une image est écrit comme une liste d’arguments pour ENTRYPOINT de cette image. Une fois l’entrypoint remplacé, les anciens arguments ne correspondent donc plus à rien. Définissez command: sur le même service si le nouvel entrypoint a toujours besoin d’arguments.
Une chaîne dans la commande Compose est-elle exécutée par un shell ?
Non. Contrairement à un CMD de Dockerfile, une chaîne dans command: de Compose est découpée en arguments, puis exécutée directement, sans wrapper /bin/sh -c. Ainsi, $VARIABLE n’est jamais développé par un shell à l’intérieur du conteneur. Appelez vous-même le shell lorsque vous en avez besoin, comme dans command: /bin/sh -c 'echo "hello $$HOSTNAME"'. Le double $$ échappe le signe dollar afin que Compose le transmette au conteneur au lieu de le développer sur l’hôte.
Pourquoi docker compose down prend-il dix secondes pour un seul conteneur ?
Compose envoie SIGTERM au PID 1, attend stop_grace_period (10 secondes par défaut), puis envoie SIGKILL. Le kernel n’applique pas les actions par défaut associées aux signaux au PID 1. Un programme sans gestionnaire SIGTERM ignore donc le signal et attend toujours la fin de cette période. Identifiez le processus qui correspond réellement au PID 1 avec docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' '. S’il s’agit d’un shell, utilisez la forme exec dans l’image ou écrivez exec dans la chaîne du shell. Si le processus crée des processus enfants qu’il ne réattribue jamais, définissez init: true sur le service.