SSD Nodes Learn 🎉 VPS dès $5.50/mois
Guides Matt ConnorPar Matt Connor

Ouvrir un shell interactif avec Docker Compose exec

Utilisez docker compose exec pour ouvrir un shell dans un service en cours. Si le service est arrêté, préférez run --rm sans perturber le conteneur existant.

Obtenir un shell interactif avec docker compose exec

docker compose exec web bash ouvre un shell interactif dans le conteneur qui exécute déjà le service web. Le nom placé après exec est le nom du service défini dans votre compose.yaml, et non le nom du conteneur. Si l’image ne contient pas bash, utilisez plutôt sh.

docker compose ps
docker compose exec web bash

Exécutez d’abord docker compose ps. La commande doit afficher web avec l’état running. La deuxième commande vous place ensuite à une invite dans le conteneur. exit ou Ctrl-D vous ramène sur l’hôte. Le service continue de fonctionner après votre sortie, car exec a démarré un deuxième processus à côté du processus principal. La fermeture de votre shell ne modifie pas le PID 1 (identifiant de processus 1), c’est-à-dire le processus pour lequel le conteneur a été conçu.

C’est l’une des deux façons d’entrer dans un conteneur. exec se connecte à un conteneur qui existe déjà. docker compose run crée un nouveau conteneur à partir de la même définition de service. Presque tout le reste de ce guide découle de cette seule différence.

Pourquoi -it est facultatif avec Compose, mais obligatoire avec docker simple

Deux options contrôlent la partie interactive d’une session. -i laisse stdin ouvert afin que ce que vous saisissez soit transmis au processus. -t alloue un pseudo-terminal, appelé TTY, afin que le shell affiche une invite et gère les touches fléchées. Par défaut, docker exec désactive les deux options. C’est pourquoi tous les exemples précédents écrivent docker exec -it. docker compose exec active automatiquement les deux options. docker compose exec -it web bash et docker compose exec web bash ont donc le même effet. Compose accepte également -it afin de préserver les habitudes existantes.

Vous remarquez l’absence de TTY en quelques secondes. Le shell s’exécute, mais n’affiche aucune invite et Ctrl-C n’atteint jamais le processus. Dans le cas inverse, où vous devez demander à Compose de ne pas allouer de TTY, il existe une option dédiée et une section spécifique plus bas.

Que faire lorsque l’image ne contient pas bash

Si vous demandez bash à une image basée sur Alpine, exec échoue comme suit :

OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown

Ce message n’indique pas un problème avec exec. Il signifie que le binaire demandé n’est pas présent dans l’image. Alpine fournit BusyBox, qui propose ash sous la forme /bin/sh, mais aucun bash. Demandez donc sh :

docker compose exec web sh

Les images basées sur Debian et Ubuntu, y compris les tags -slim, contiennent bien bash. bash fournit l’historique des commandes et une meilleure complétion. Essayez donc d’abord bash, puis utilisez sh en solution de repli. sh est présent dans presque toutes les images à usage général.

Certaines images ne contiennent aucun shell. Les images Distroless et les images construites FROM scratch contiennent volontairement uniquement le binaire de l’application et ses bibliothèques. Un shell absent ne peut pas être utilisé contre vous. Dans ces images, sh échoue avec le même message et il n’y a rien d’autre à essayer. Deux approches sont possibles. Les images Distroless de Google publient des tags :debug qui ajoutent un shell BusyBox. Vous pouvez donc utiliser temporairement ce tag pour accéder au conteneur. Vous pouvez aussi démarrer un conteneur séparé dans les namespaces de la cible :

CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot

Vous disposez alors des outils de netshoot sur le réseau de l’application. curl localhost:8080 et ss -lntp se comportent donc comme si vous étiez dans le conteneur cible. Le système de fichiers que vous voyez appartient à netshoot, pas à l’application. Comme le namespace des processus est partagé, ls /proc/1/root/ permet d’accéder aux propres fichiers de la cible lorsque vous êtes root.

Lorsque le service n’est pas en cours d’exécution, utilisez docker compose run --rm

exec nécessite un conteneur en cours d’exécution. Si vous lui indiquez un service arrêté, il refuse :

service "web" is not running

Il ne démarre rien automatiquement. docker compose run va :

docker compose run --rm web bash

run crée un conteneur à partir de la définition du service web, avec la même image, les mêmes variables d’environnement, les mêmes volumes et les mêmes réseaux, puis remplace la commande du service par celle que vous avez saisie. --rm supprime ce conteneur lorsque vous quittez. Si vous omettez --rm, les conteneurs restants s’accumulent sous des noms comme myproject-web-run-4f1c2b. docker compose ps -a les affiche, et aucun autre mécanisme ne les supprime.

Deux comportements de run surprennent souvent. La commande ne publie pas les ports du service, sauf si vous ajoutez --service-ports. C’est volontaire : un second conteneur qui tenterait de s’attacher au port hôte 8080 alors que le premier l’utilise encore échouerait avec bind: address already in use. run démarre également tout ce que le service déclare sous depends_on avant d’afficher votre shell. Une simple inspection peut donc démarrer une base de données et un cache. --no-deps désactive ce comportement.

run passe par l’ENTRYPOINT de l’image, contrairement à exec. exec démarre directement votre commande dans le conteneur existant. Le script d’entrypoint ne la voit donc jamais. Avec run, votre bash est transmis comme argument à ce script. De nombreuses images officielles terminent leur entrypoint par exec "$@". Les arguments sont alors transmis directement et vous obtenez votre shell. Un script qui interprète lui-même ses arguments fera autre chose. Vous pouvez alors remplacer l’entrypoint pour cette seule exécution :

docker compose run --rm --entrypoint sh web

C’est la raison la plus fréquente pour laquelle une commande qui fonctionne avec exec se comporte différemment avec run. La distinction entre la commande et l’entrypoint explique quelle partie de la configuration de l’image vous remplacez à chaque fois.

exec ou run : comment choisir

  • exec nécessite un conteneur en cours d’exécution. run n’en nécessite pas et peut démarrer les dépendances.
  • exec voit la liste des processus en cours et les fichiers dans leur état actuel, y compris ce que l’application a écrit depuis son démarrage. run utilise une copie propre de l’image. Ces éléments n’y sont donc pas.
  • exec ignore l’entrypoint. run l’exécute.
  • run laisse un conteneur après son exécution, sauf si vous utilisez --rm.

Utilisez exec pour vérifier ce qui se passe réellement. Utilisez run --rm pour obtenir une copie temporaire du même environnement, exécuter une commande de migration ponctuelle ou intervenir lorsque le service réel ne reste pas actif assez longtemps pour permettre l’utilisation de exec.

Options utiles de exec : utilisateur, répertoire de travail et réplicas

La plupart des images utilisent un utilisateur non-root. L’installation d’un outil de diagnostic depuis votre shell exec s’arrête donc ici :

E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)

-u root vous fournit un shell root dans le même conteneur :

docker compose exec -u root web sh

-w /srv/app définit le répertoire de travail pour cette commande uniquement. -e KEY=value ajoute une variable d’environnement à votre session, mais pas au service. Lorsqu’un service utilise plusieurs réplicas, --index 2 détermine le conteneur dans lequel vous arrivez. Si vous cherchez pourquoi les propriétaires des fichiers d’un répertoire monté posent problème, l’article PUID et PGID dans les images de conteneurs explique pourquoi ce sont les identifiants numériques, et non les noms d’utilisateur, qui déterminent qui peut y écrire.

Obtenir un shell psql ou mysql dans le conteneur de base de données

Le client se trouve déjà dans l’image de base de données. Vous n’en avez donc pas besoin sur l’hôte et vous n’avez pas besoin de publier le port :

docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -p

Les images Postgres contiennent psql, les images MySQL contiennent mysql et les images MariaDB contiennent mariadb. La connexion est établie depuis l’intérieur du conteneur. Cela fonctionne donc même si le fichier Compose ne publie aucun port de base de données. C’est la configuration la plus sûre : rien sur Internet ne peut atteindre un port que vous n’avez jamais publié.

Un piège peut vous faire perdre une après-midi. Votre shell développe les variables sur l’hôte, avant que Docker ne voie la commande. Ainsi, -U "$POSTGRES_USER" transmet une chaîne vide lorsque cette variable existe uniquement dans le conteneur. Des guillemets simples et un shell exécuté dans le conteneur effectuent le développement au bon endroit :

docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

N’utilisez pas docker compose run --rm db sans commande ici. Cela démarre un deuxième serveur Postgres sur le même volume de données, qui refuse alors de démarrer :

FATAL:  lock file "postmaster.pid" already exists

Le fichier de verrouillage joue son rôle : deux serveurs écrivant dans un même répertoire de données le corrompraient. Tant que la base de données fonctionne, utilisez exec pour accéder au conteneur en cours d’exécution. La question de savoir si la base de données doit être gérée par Compose est distincte. La page exécuter la base de données dans Docker ou sur l’hôte présente les compromis.

Services nécessitant une console au démarrage : stdin_open et tty

exec et run concernent les shells que vous ouvrez manuellement. Un service dont le processus principal est interactif par nature nécessite deux clés dans le fichier Compose :

services:
  console:
    image: python:3.12-slim
    command: python
    stdin_open: true
    tty: true

stdin_open: true est docker run -i et tty: true est docker run -t. Sans ces clés, le conteneur démarre puis s’arrête immédiatement avec le code 0, et docker compose ps -a affiche Exited (0). Rien n’a planté. python, sans terminal sur stdin, lit immédiatement la fin du fichier et se termine normalement. C’est le comportement attendu pour un programme auquel personne ne saisit de texte.

Lorsque les deux clés sont définies, connectez-vous au processus en cours d’exécution :

docker attach $(docker compose ps -q console)

Détachez-vous avec Ctrl-P puis Ctrl-Q. Le processus continue alors de s’exécuter. Cette séquence fonctionne uniquement lorsque le conteneur dispose à la fois d’un TTY et de stdin ouvert. Ctrl-C envoie au contraire une interruption au PID 1 et arrête le service.

Laissez les deux clés désactivées pour les services ordinaires. Un serveur web ne lit jamais stdin, et tty: true amène de nombreux programmes à activer la sortie en couleur et la mise en tampon ligne par ligne, car ils pensent qu’une personne les regarde. Cela remplit docker compose logs de codes d’échappement.

Pourquoi exec scripté échoue dans cron et la CI : l’option -T

Une commande exec qui fonctionne dans votre terminal échoue dans une tâche cron ou un runner d’intégration continue (CI) :

the input device is not a TTY

Compose demande par défaut un pseudo-terminal. cron n’en fournit pas à la tâche. La demande échoue donc avant même l’exécution de votre commande. -T désactive cette demande :

0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump

-T est également nécessaire pour une autre raison. Un TTY réécrit le flux d’octets lors de sa transmission. Un dump compressé qui le traverse arrive donc endommagé. Toute sortie redirigée ou transmise par pipe nécessite -T.

Deux autres points concernent cron. Fournissez -f avec un chemin absolu, car cron exécute la tâche depuis le répertoire personnel, qui ne contient pas de fichier Compose. Compose s’arrête alors avec no configuration file provided: not found. De plus, exec renvoie le code de sortie de la commande exécutée. Ainsi, un pg_dump en échec fait échouer votre script avec set -e, au lieu d’écrire une sauvegarde vide et de signaler une réussite. Les autres commandes courantes sont regroupées dans une fiche récapitulative des commandes Compose à conserver à côté de ces scripts.

Pourquoi les modifications effectuées dans un conteneur disparaissent

Vous installez un outil avec exec, modifiez un fichier de configuration, corrigez le problème, puis une semaine plus tard, la correction a disparu. C’est le comportement attendu de la couche inscriptible du conteneur. docker compose up -d détruit l’ancien conteneur après toute modification du tag de l’image ou de la définition du service, puis en crée un nouveau à partir de l’image. Toutes les modifications manuelles sont alors perdues avec l’ancien conteneur.

docker compose restart est différent. Il arrête puis redémarre le même conteneur. Les modifications manuelles sont donc conservées. C’est pourquoi une correction manuelle peut sembler rester en place pendant des semaines, puis disparaître lors d’une mise à jour sans rapport. Les volumes nommés et les bind mounts survivent aux deux opérations, car leurs données sont stockées en dehors du conteneur. La section bind mounts et volumes nommés explique lequel choisir pour les données que vous souhaitez conserver.

Considérez donc un shell exec comme un emplacement pour consulter et tester. Une fois la correction identifiée, écrivez-la à un emplacement où elle sera conservée : ajoutez le paquet dans le Dockerfile et le paramètre dans le fichier compose. Ensuite, docker compose up -d pour l’appliquer, puis vérifiez avec un autre exec que le nouveau conteneur contient bien cette modification.

FAQ

Quelle est la différence entre docker compose exec et docker compose run ?

exec exécute une commande dans un conteneur déjà en cours d’exécution, à côté du processus principal, et ignore l’entrypoint de l’image. run crée un nouveau conteneur à partir de la même définition de service, avec la même image, le même environnement, les mêmes volumes et les mêmes réseaux. Il transmet votre commande à l’entrypoint et démarre d’abord les services depends_on. run ne publie pas non plus les ports du service, sauf si vous ajoutez --service-ports. Utilisez exec pour inspecter le service actif. Utilisez run --rm lorsque le service est arrêté ou lorsque vous ne voulez pas le perturber.

Pourquoi docker compose exec indique-t-il que le service n’est pas en cours d’exécution ?

exec se rattache à un conteneur existant et ne peut pas en créer un. Un service arrêté ou ayant planté renvoie donc service "web" is not running. Vérifiez docker compose ps -a, qui liste les conteneurs arrêtés avec un état tel que Exited (1), puis consultez docker compose logs web pour connaître la raison de l’arrêt. Pour ouvrir malgré tout un shell, exécutez docker compose run --rm --entrypoint sh web. Cette commande crée un conteneur vierge à partir de la même définition de service, sans exécuter la commande de démarrage défaillante.

Comment ouvrir un shell lorsque l’image ne contient pas bash ?

docker compose exec web bash échoue avec exec: "bash": executable file not found in $PATH lorsque bash est absent de l’image. C’est normal pour les images basées sur Alpine. Utilisez docker compose exec web sh, car BusyBox fournit /bin/sh. Les images Distroless et scratch ne contiennent aucun shell. Aucune commande exec ne fonctionnera donc. Utilisez le tag :debug de l’image si l’éditeur en propose un, ou démarrez un conteneur de debug dans les namespaces de la cible avec docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot, où $CID provient de docker compose ps -q web.

Pourquoi ma commande exec échoue-t-elle avec « the input device is not a TTY » dans cron ?

docker compose exec demande par défaut un pseudo-terminal, mais cron n’en fournit pas. La demande échoue donc avant l’exécution de votre commande. Ajoutez -T pour désactiver ce comportement : docker compose exec -T db pg_dump -U postgres app. Utilisez également -T pour toute sortie redirigée ou envoyée dans un pipe, car un TTY modifie le flux d’octets et peut endommager un dump binaire. Dans cron, indiquez aussi -f avec le chemin absolu vers votre fichier Compose, sinon Compose quitte avec no configuration file provided: not found.

Les modifications effectuées dans un conteneur avec exec sont-elles conservées après un redémarrage ?

Elles sont conservées lors de docker compose restart, qui réutilise le même conteneur. Elles sont perdues lors de docker compose up -d après toute modification de l’image ou de la configuration, car cette opération recrée le conteneur à partir de l’image et supprime sa couche inscriptible. Les données écrites dans des volumes nommés ou des bind mounts sont conservées dans les deux cas, car elles résident en dehors du conteneur. Effectuez les modifications de diagnostic avec exec, puis placez la version permanente dans le Dockerfile ou le fichier Compose.