SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor

Interactieve shell openen in Docker Compose container

Gebruik docker compose exec voor een shell in een draaiende service. Kies docker compose run --rm voor een nieuwe container als de service gestopt is of niet mag wijzigen.

Een interactieve shell verkrijgen met docker compose exec

docker compose exec web bash opent een interactieve shell in de container die al draait als de web service. De naam na exec is de servicenaam uit uw compose.yaml, niet de containernaam. Als de image geen bash bevat, vraag dan in plaats daarvan om sh.

docker compose ps
docker compose exec web bash

Voer eerst docker compose ps uit. Dit hoort web weer te geven met de status running. Daarna brengt het tweede commando u naar een prompt binnen de container, en exit of Ctrl-D brengt u terug naar de host. De service blijft draaien nadat u de sessie verlaat, omdat exec een tweede proces start naast het hoofdproces. Het sluiten van uw shell heeft geen invloed op PID 1 (process id 1), het proces waarvoor de container is gebouwd.

Dit is een van de twee manieren om toegang te krijgen. exec koppelt aan een container die al bestaat. docker compose run maakt een nieuwe container aan op basis van dezelfde servicedefinitie. Vrijwel alles in deze handleiding vloeit voort uit dat ene verschil.

Waarom -it optioneel is in Compose, maar vereist bij standaard docker

Twee vlaggen bepalen het interactieve karakter van een sessie. -i houdt stdin open, zodat uw invoer het proces bereikt. -t wijst een pseudo-terminal toe, een TTY genaamd, waardoor de shell een prompt toont en pijltoetsen verwerkt. Standaard laat docker exec beide opties uitgeschakeld, wat de reden is dat elk voorbeeld dat u heeft gezien docker exec -it gebruikt. docker compose exec schakelt beide voor u in, waardoor docker compose exec -it web bash en docker compose exec web bash hetzelfde resultaat geven. Compose accepteert nog steeds -it, zodat oude gewoontes blijven werken.

U merkt binnen enkele seconden dat een TTY ontbreekt. De shell draait wel, maar toont geen prompt en Ctrl-C bereikt het proces niet. Het tegenovergestelde scenario, waarbij u Compose moet verzoeken om geen TTY toe te wijzen, heeft een eigen vlag en wordt verderop in een eigen sectie behandeld.

Wat te doen als de image geen bash bevat

Wanneer u een op Alpine gebaseerde image om bash vraagt en de exec-opdracht faalt als volgt:

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

Die melding is geen exec-probleem. Het geeft aan dat het binaire bestand waar u om vroeg niet in de image aanwezig is. Alpine levert BusyBox, dat ash aanbiedt als /bin/sh en helemaal geen bash bevat, dus vraag om sh:

docker compose exec web sh

Op Debian en Ubuntu gebaseerde images, inclusief de -slim-tags, bevatten wel bash, en bash biedt u opdrachtgeschiedenis en betere aanvulling van opdrachten. Probeer daarom eerst bash en val terug op sh. sh is in bijna elke algemene image aanwezig.

Sommige images bevatten helemaal geen shell. Distroless-images en images die FROM scratch zijn gebouwd, bevatten enkel het applicatie-binaire bestand en de bijbehorende bibliotheken, en niets anders. Dit is een bewuste keuze, omdat een shell die niet aanwezig is, niet tegen u kan worden gebruikt. In die gevallen faalt sh met dezelfde melding en is er niets meer om te proberen. Twee benaderingen werken dan. De distroless-images van Google publiceren :debug-tags die een BusyBox-shell toevoegen, dus door tijdelijk van tag te wisselen krijgt u toegang. Of start een aparte container binnen de namespaces van het doel:

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

U heeft nu de tools van netshoot gekoppeld aan het netwerk van de applicatie, waardoor curl localhost:8080 en ss -lntp zich gedragen alsof u zich erin bevindt. Het bestandssysteem dat u ziet, is van netshoot, niet van de app. Omdat de proces-namespace wordt gedeeld, bereikt ls /proc/1/root/ de bestanden van het doel zelf wanneer u root bent.

Wanneer de service niet draait, gebruikt u docker compose run --rm

exec vereist een draaiende container. Richt u dit op een gestopte service, dan weigert het:

service "web" is not running

Het zal niets voor u opstarten. docker compose run doet dat wel:

docker compose run --rm web bash

run maakt een nieuwe container aan op basis van de web service-definitie, met hetzelfde image, dezelfde omgeving, volumes en netwerken, en vervangt het commando van de service door het commando dat u heeft ingevoerd. --rm verwijdert die container zodra u afsluit. Laat u --rm weg, dan hopen de restanten zich op onder namen als myproject-web-run-4f1c2b, die docker compose ps -a u zal tonen en die door niets anders worden opgeruimd.

Twee gedragingen van run verrassen gebruikers vaak. Het publiceert de poorten van de service niet, tenzij u --service-ports toevoegt, en dat is een bewuste keuze: een tweede container die poort 8080 op de host probeert te binden terwijl de eerste deze nog bezet houdt, zou falen met bind: address already in use. Het start ook alles wat de service onder depends_on vermeldt voordat uw shell verschijnt, dus een snelle blik binnenin kan een database en een cache opstarten. --no-deps slaat dat over.

run doorloopt de ENTRYPOINT van het image, en exec niet. exec start uw commando direct in de bestaande container, waardoor het entrypoint-script het nooit ziet. Bij run komt uw bash aan als argumenten voor dat script. Veel officiële images eindigen hun entrypoint met exec "$@", waardoor het direct wordt doorgegeven en u uw shell krijgt. Een script dat zijn eigen argumenten interpreteert, zal er echter iets anders mee doen; in dat geval vervangt u het entrypoint voor die ene run:

docker compose run --rm --entrypoint sh web

Dit is de meest voorkomende reden waarom een commando dat werkt onder exec zich anders gedraagt onder run, en het onderscheid tussen command en entrypoint legt uit welk deel van de image-configuratie u telkens vervangt.

exec of run: hoe kiest u de juiste

  • exec vereist een draaiende container. run doet dit niet en kan afhankelijkheden starten.
  • exec ziet de actieve proceslijst en de bestanden in hun huidige staat, inclusief alle wijzigingen die de applicatie sinds de start heeft aangebracht. run gebruikt een schone kopie van de image, waardoor deze wijzigingen niet aanwezig zijn.
  • exec slaat de entrypoint over. run voert deze uit.
  • run laat een container achter, tenzij u --rm gebruikt.

Gebruik exec om te onderzoeken wat er op dit moment gebeurt. Gebruik run --rm voor een tijdelijke kopie van dezelfde omgeving, voor een eenmalig migratiecommando, of wanneer de eigenlijke service niet lang genoeg actief blijft om met exec in te loggen.

Handige exec-vlaggen: gebruiker, werkmap en replica's

De meeste images schakelen over naar een niet-rootgebruiker, waardoor het installeren van diagnostische tools binnen uw exec-shell hier stopt:

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

-u root geeft u een root-shell in dezelfde container:

docker compose exec -u root web sh

-w /srv/app stelt de werkmap in voor uitsluitend dat commando. -e KEY=value voegt een omgevingsvariabele toe aan uw sessie en niet aan de service. Wanneer een service meer dan één replica draait, bepaalt --index 2 in welke container u terechtkomt. Als u onderzoekt wie de eigenaar is van bestanden in een gemounte map, behandelt PUID en PGID in container images waarom de numerieke id's, en niet de gebruikersnamen, bepalen wie daar schrijfrechten heeft.

Een psql- of mysql-shell openen in de databasecontainer

De client bevindt zich al in de database-image, dus u heeft er geen nodig op de host en u hoeft de poort niet te publiceren:

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

Postgres-images bevatten psql, MySQL-images bevatten mysql en MariaDB-images bevatten mariadb. De verbinding wordt van binnenuit de container gemaakt, dus dit werkt zelfs als het compose-bestand helemaal geen databasepoort publiceert. Dat is de veiligere opstelling: niets op het internet kan een poort bereiken die u nooit heeft gepubliceerd.

Eén valkuil kost mensen vaak een middag werk. Uw shell breidt variabelen op de host uit voordat Docker het commando ziet, waardoor -U "$POSTGRES_USER" een lege string verstuurt wanneer die variabele alleen binnen de container bestaat. Enkele aanhalingstekens en een shell binnen de container breiden de variabele op de juiste plek uit:

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

Gebruik hier niet docker compose run --rm db zonder commando. Dat start een tweede Postgres-server op hetzelfde datavolume, en die zal weigeren te starten:

FATAL:  lock file "postmaster.pid" already exists

Het lock-bestand doet zijn werk, omdat twee servers die naar dezelfde datamap schrijven deze zouden beschadigen. Terwijl de database actief is, voert u een exec uit in de draaiende container. Of de database überhaupt in Compose thuishoort is een aparte beslissing, en het draaien van de database in Docker of op de host zet de afwegingen uiteen.

Services die bij het opstarten een console vereisen: stdin_open en tty

exec en run zijn bedoeld voor shells die u handmatig opent. Een service waarvan het hoofdproces van nature interactief is, heeft twee sleutels in het compose-bestand nodig:

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

stdin_open: true is docker run -i en tty: true is docker run -t. Zonder deze instellingen start de container en stopt deze direct met code 0, en toont docker compose ps -a de melding Exited (0). Er is niets gecrasht. python leest zonder terminal op stdin direct het einde van het bestand en stopt normaal; dit is het juiste gedrag voor een programma waar niemand invoer aan levert.

Wanneer beide sleutels zijn ingesteld, kunt u aan het draaiende proces koppelen:

docker attach $(docker compose ps -q console)

Ontkoppel met Ctrl-P gevolgd door Ctrl-Q; het proces blijft hierbij draaien. Deze reeks werkt alleen wanneer de container zowel een TTY als stdin open heeft staan. Ctrl-C stuurt daarentegen een interrupt naar PID 1 en stopt de service.

Laat beide sleutels achterwege voor standaard services. Een webserver leest nooit stdin, en tty: true zorgt ervoor dat veel programma's overschakelen naar kleuruitvoer en line buffering omdat ze aannemen dat er een gebruiker meekijkt, wat docker compose logs vult met escape-codes.

Waarom scripted exec niet werkt in cron en CI: de -T flag

Een exec-commando dat in uw terminal werkt, faalt binnen een cron-job of een continuous integration (CI) runner:

the input device is not a TTY

Compose vraagt standaard om een pseudo-terminal, maar cron biedt de job geen terminal. Hierdoor faalt het verzoek nog voordat uw commando wordt uitgevoerd. -T schakelt dit verzoek uit:

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

-T is om een tweede reden van belang. Een TTY herschrijft de bytestroom tijdens het verzenden, waardoor een gecomprimeerde dump die hierdoorheen gaat beschadigd aankomt. Voor elke omgeleide of gepipte output is -T vereist.

Nog twee details over cron. Geef -f mee met een absoluut pad, omdat cron de job uitvoert vanuit de home-directory waar geen compose-bestand aanwezig is; Compose stopt vervolgens met no configuration file provided: not found. Daarnaast geeft exec de exit-code terug van het uitgevoerde commando. Een falende pg_dump zorgt er dus voor dat uw script faalt onder set -e, in plaats van een lege back-up te schrijven en succes te rapporteren. De overige alledaagse commando's zijn verzameld in een spiekbriefje voor Compose-commando's dat nuttig is om bij uw scripts te bewaren.

Waarom wijzigingen in een container verdwijnen

U installeert een tool via exec, bewerkt een configuratiebestand, lost het probleem op, en een week later is de wijziging verdwenen. Dit is de beschrijfbare laag van de container die werkt zoals ontworpen. docker compose up -d na een wijziging aan de image-tag of de service-definitie vernietigt de oude container en bouwt een nieuwe op basis van de image; elke handmatige aanpassing gaat verloren met de oude container.

docker compose restart werkt anders. Het stopt en start dezelfde container, waardoor handmatige aanpassingen behouden blijven. Daarom kan een handmatige fix wekenlang standhouden en vervolgens verdwijnen tijdens een niet-gerelateerde update. Named volumes en bind mounts overleven beide operaties, omdat hun data buiten de container leeft, en bind mounts en named volumes behandelt welke optie u moet kiezen voor data die u wilt behouden.

Beschouw een exec-shell daarom als een plek om te lezen en te testen. Zodra u de oplossing kent, verwerkt u deze op een plek waar deze behouden blijft: een pakket in de Dockerfile, een instelling in het compose-bestand. Voer daarna docker compose up -d uit om de wijziging toe te passen en bevestig met een nieuwe exec dat de nieuwe container de wijziging daadwerkelijk bevat.

FAQ

Wat is het verschil tussen docker compose exec en docker compose run?

exec voert een commando uit in een container die al draait, naast het hoofdproces, en slaat de entrypoint van de image over. run maakt een nieuwe container aan op basis van dezelfde servicedefinitie met dezelfde image, omgeving, volumes en netwerken, voert uw commando uit via de entrypoint en start eerst alle depends_on services. run publiceert de poorten van de service bovendien niet, tenzij u --service-ports toevoegt. Gebruik exec om een actieve service te inspecteren. Gebruik run --rm wanneer de service is gestopt of wanneer u deze niet wilt verstoren.

Waarom meldt docker compose exec dat de service niet draait?

exec koppelt aan een bestaande container en kan er geen aanmaken, dus een gestopte of gecrashte service geeft service "web" is not running. Controleer docker compose ps -a, waarin afgesloten containers met een status zoals Exited (1) worden vermeld, en lees docker compose logs web voor de reden van de stopzetting. Om toch een shell te krijgen, voert u docker compose run --rm --entrypoint sh web uit. Dit bouwt een nieuwe container op basis van dezelfde servicedefinitie zonder het defecte startcommando uit te voeren.

Hoe open ik een shell als de image geen bash bevat?

docker compose exec web bash dat faalt met exec: "bash": executable file not found in $PATH betekent dat bash niet aanwezig is in de image, wat normaal is voor alles wat gebaseerd is op Alpine. Gebruik docker compose exec web sh, omdat BusyBox voorziet in /bin/sh. Distroless- en scratch-images bevatten helemaal geen shell, dus geen enkel exec-commando zal werken. Schakel over naar de :debug-tag van de image als de uitgever deze aanbiedt, of start een debug-container in de namespaces van het doel met docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot, waarbij $CID afkomstig is van docker compose ps -q web.

Waarom faalt mijn exec-commando met "the input device is not a TTY" in cron?

docker compose exec vraagt standaard om een pseudo-terminal en cron biedt er geen, dus het verzoek faalt voordat uw commando wordt uitgevoerd. Voeg -T toe om dit uit te schakelen: docker compose exec -T db pg_dump -U postgres app. Gebruik -T ook voor omgeleide of gepipete uitvoer, aangezien een TTY de bytestroom wijzigt en een binaire dump beschadigt. Geef in cron ook -f mee met het absolute pad naar uw compose-bestand, anders stopt Compose met no configuration file provided: not found.

Blijven wijzigingen die ik met exec in een container aanbreng behouden na een herstart?

Ze blijven behouden bij docker compose restart, waarbij dezelfde container opnieuw wordt gebruikt. Ze gaan verloren bij docker compose up -d na een wijziging in de image of configuratie, omdat de container dan opnieuw wordt aangemaakt op basis van de image en de beschrijfbare laag wordt verwijderd. Gegevens die naar named volumes of bind mounts zijn geschreven, overleven beide, omdat deze buiten de container leven. Voer diagnostische wijzigingen uit met exec en verwerk de permanente versie vervolgens in de Dockerfile of het compose-bestand.