Docker Compose exec: jak otworzyć interaktywną powłokę
Dowiedz się, jak użyć komendy docker compose exec do otwarcia powłoki w działającym kontenerze. Poznaj różnicę między exec a run oraz dlaczego flaga -it jest opcjonalna.
Uzyskiwanie interaktywnej powłoki za pomocą docker compose exec
docker compose exec web bash otwiera interaktywną powłokę wewnątrz kontenera, który jest już uruchomiony jako usługa web. Nazwa po exec to nazwa usługi z pliku compose.yaml, a nie nazwa kontenera. Jeśli obraz nie zawiera bash, należy wywołać sh.
docker compose ps
docker compose exec web bashNajpierw należy uruchomić docker compose ps. Powinno to wyświetlić web ze stanem running. Następnie drugie polecenie przenosi użytkownika do wiersza poleceń wewnątrz kontenera, a exit lub Ctrl-D przywraca sesję na hoście. Usługa działa nadal po wyjściu, ponieważ exec uruchomił drugi proces obok głównego. Zamknięcie powłoki nie wpływa na PID 1 (process id 1), czyli proces, dla którego kontener został utworzony.
Jest to jeden z dwóch sposobów uzyskania dostępu. Polecenie exec dołącza się do już istniejącego kontenera. docker compose run tworzy nowy kontener na podstawie tej samej definicji usługi. Prawie wszystkie pozostałe informacje w tym przewodniku wynikają z tej jednej różnicy.
Dlaczego flaga -it jest opcjonalna w Compose, a wymagana w zwykłym docker
Dwie flagi sterują interaktywnością sesji. -i utrzymuje otwarte wejście stdin, dzięki czemu wpisywane znaki docierają do procesu. -t przydziela pseudo-terminal, zwany TTY, co pozwala powłoce wyświetlać znak zachęty i obsługiwać klawisze strzałek. Zwykły docker exec domyślnie wyłącza obie te opcje, dlatego w każdym przykładzie widać zapis docker exec -it. docker compose exec włącza obie opcje automatycznie, więc docker compose exec -it web bash oraz docker compose exec web bash wykonują to samo zadanie. Compose nadal akceptuje -it, aby zachować kompatybilność z przyzwyczajeniami użytkowników.
Brak TTY staje się zauważalny w ciągu kilku sekund. Powłoka uruchamia się, ale nie wyświetla znaku zachęty, a sygnał Ctrl-C nie dociera do procesu. Przypadek odwrotny, w którym należy polecić Compose, aby nie przydzielał TTY, posiada własną flagę i został opisany w dalszej części dokumentacji.
Co zrobić, gdy obraz nie zawiera bash
Wywołanie bash w obrazie opartym na Alpine kończy się błędem:
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknownTen komunikat nie oznacza problemu z wykonaniem polecenia. Informuje on, że bash nie znajduje się w katalogach wymienionych w zmiennej PATH kontenera, co w przypadku powłoki niemal zawsze oznacza, że obraz jej nie zawiera. Alpine dostarcza BusyBox, który udostępnia ash jako /bin/sh, ale nie zawiera bash, dlatego należy wywołać sh:
docker compose exec web shObrazy oparte na Debianie i Ubuntu, w tym tagi -slim, zawierają bash, który oferuje historię poleceń oraz lepsze uzupełnianie składni. Należy zatem najpierw próbować użyć bash, a w razie braku przejść na sh. sh występuje w niemal każdym ogólnym obrazie.
Niektóre obrazy nie posiadają żadnej powłoki. Obrazy typu distroless oraz obrazy budowane FROM scratch zawierają wyłącznie plik binarny aplikacji wraz z bibliotekami. Jest to celowe działanie, ponieważ nieobecna powłoka nie może zostać wykorzystana do ataku. W takich przypadkach sh kończy się tym samym błędem i nie ma innych opcji. Dostępne są dwa rozwiązania. Obrazy distroless od Google udostępniają tagi :debug, które dodają powłokę BusyBox, więc tymczasowa zmiana tagu umożliwia dostęp. Alternatywnie można uruchomić osobny kontener wewnątrz przestrzeni nazw (namespaces) docelowego kontenera:
CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshootNarzędzia z netshoot są teraz skierowane na sieć aplikacji, więc curl localhost:8080 oraz ss -lntp zachowują się tak, jakby użytkownik znajdował się wewnątrz niej. Widoczny system plików należy do netshoot, a nie do aplikacji. Ponieważ przestrzeń nazw procesów jest współdzielona, /proc/1/root/ wskazuje na system plików procesu docelowego, jednak ls /proc/1/root/ musi przejść weryfikację uprawnień ptrace w jądrze. Uruchomienie obu procesów jako root jest niewystarczające: weryfikacja uwzględnia również poświadczenia, uprawnienia (capabilities), atrybut dumpable obiektu docelowego oraz moduły bezpieczeństwa. Wartość dumpable inna niż 1 blokuje dostęp, chyba że wywołujący posiada CAP_SYS_PTRACE w przestrzeni nazw użytkownika (user namespace) obiektu docelowego. Docker domyślnie nie nadaje SYS_PTRACE. Dodanie --cap-add SYS_PTRACE do linii docker run rozwiązuje problemy z poświadczeniami i weryfikacją dumpable, jednak moduły bezpieczeństwa nadal mogą odmówić dostępu.
Gdy usługa nie działa, użyj docker compose run --rm
Polecenie exec wymaga uruchomionego kontenera. Wskazanie zatrzymanej usługi spowoduje błąd:
service "web" is not runningPolecenie to nie uruchomi żadnego procesu. Zrobi to docker compose run:
docker compose run --rm web bashPolecenie run tworzy nowy kontener na podstawie definicji usługi web, używając tego samego obrazu, zmiennych środowiskowych, wolumenów i sieci, zastępując przy tym domyślne polecenie usługi wpisanym przez użytkownika. Flaga --rm usuwa kontener po zakończeniu pracy. Pominięcie --rm spowoduje gromadzenie się pozostałości pod nazwami takimi jak myproject-web-run-4f1c2b. Polecenie docker compose ps -a wyświetli je, a docker compose rm usunie je wraz z innymi zatrzymanymi kontenerami usługi.
Dwa zachowania polecenia run często zaskakują użytkowników. Nie publikuje ono portów usługi, chyba że dodasz --service-ports lub zmapujesz je ręcznie za pomocą -p. Jest to działanie celowe: próba powiązania drugiego kontenera z portem hosta 8080, gdy pierwszy nadal go zajmuje, zakończy się błędem bind: address already in use. Polecenie to uruchamia również wszystkie zależności wymienione w sekcji depends_on przed wyświetleniem powłoki, więc szybki podgląd wnętrza kontenera może zainicjować bazę danych i pamięć podręczną. Flaga --no-deps pomija ten krok.
Polecenie run przechodzi przez punkt wejścia (ENTRYPOINT) obrazu, natomiast exec tego nie robi. Polecenie exec uruchamia komendę bezpośrednio w istniejącym kontenerze, więc skrypt punktu wejścia jej nie przetwarza. W przypadku run, argument bash trafia do tego skryptu jako parametr. Wiele oficjalnych obrazów kończy swój skrypt punktu wejścia poleceniem exec "$@", dzięki czemu argumenty są przekazywane dalej i użytkownik uzyskuje dostęp do powłoki. Skrypt, który samodzielnie interpretuje swoje argumenty, zareaguje inaczej, dlatego w takim przypadku należy zastąpić punkt wejścia dla pojedynczego uruchomienia:
docker compose run --rm --entrypoint sh webJest to najczęstsza przyczyna, dla której polecenie działające w exec zachowuje się inaczej w run, a podział między command a entrypoint wyjaśnia, którą część konfiguracji obrazu zastępujesz w każdym z tych przypadków.
exec czy run: jak dokonać wyboru
- exec wymaga działającego kontenera. run nie wymaga działającego kontenera i może uruchomić zależności.
- exec widzi bieżącą listę procesów oraz pliki w ich aktualnym stanie, w tym dane zapisane przez aplikację od momentu uruchomienia. run tworzy czystą warstwę zapisu na podstawie obrazu, jednak nazwane wolumeny i bind mounts nadal udostępniają istniejące dane usługi.
- exec pomija entrypoint. run wykonuje go.
- run pozostawia kontener po zakończeniu pracy, chyba że zostanie użyta flaga
--rm.
Użyj exec, aby sprawdzić, co faktycznie dzieje się wewnątrz. Użyj run --rm dla jednorazowej kopii tego samego środowiska, do wykonania polecenia migracji lub w sytuacjach, gdy właściwa usługa nie działa wystarczająco długo, aby można było wykonać exec.
Przydatne flagi polecenia exec: użytkownik, katalog roboczy i repliki
Większość obrazów przełącza się na użytkownika innego niż root, dlatego instalacja narzędzi diagnostycznych wewnątrz powłoki exec kończy się niepowodzeniem:
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)-u root pozwala uzyskać powłokę z uprawnieniami root w tym samym kontenerze:
docker compose exec -u root web sh-w /srv/app ustawia katalog roboczy tylko dla tego konkretnego polecenia. -e KEY=value dodaje zmienną środowiskową do bieżącej sesji, a nie do samej usługi. Gdy usługa działa w więcej niż jednej replice, --index 2 decyduje, do którego kontenera zostanie uzyskany dostęp. Jeśli problemem jest uprawnienie do plików w zamontowanym katalogu, sekcja PUID i PGID w obrazach kontenerów wyjaśnia, dlaczego to numeryczne identyfikatory, a nie nazwy użytkowników, decydują o możliwości zapisu.
Uzyskanie powłoki psql lub mysql wewnątrz kontenera bazy danych
Klient znajduje się już wewnątrz obrazu bazy danych, więc nie jest wymagany na hoście i nie trzeba publikować portu:
docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -pObrazy Postgres zawierają psql, obrazy MySQL zawierają mysql, a obrazy MariaDB zawierają mariadb. Połączenie jest nawiązywane z wnętrza kontenera, więc działa to nawet wtedy, gdy plik compose w ogóle nie publikuje portu bazy danych. Jest to bezpieczniejsze rozwiązanie w domyślnej sieci bridge w Dockerze: hosty zewnętrzne nie mogą bezpośrednio uzyskać dostępu do nieopublikowanego portu bazy danych, chociaż host Docker i kontenery w tej samej sieci nadal mają taką możliwość.
Jedna pułapka kosztuje użytkowników wiele czasu. Powłoka rozwija zmienne na hoście, zanim Docker zobaczy polecenie, więc -U "$POSTGRES_USER" wysyła pusty ciąg znaków, gdy zmienna ta istnieje tylko wewnątrz kontenera. Pojedyncze cudzysłowy i powłoka wewnątrz kontenera rozwijają ją we właściwym miejscu:
docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'Nie należy używać docker compose run --rm db bez podania polecenia. Uruchamia to drugi serwer Postgres korzystający z tego samego wolumenu danych, co kończy się odmową startu:
FATAL: lock file "postmaster.pid" already existsPlik blokady spełnia swoje zadanie, ponieważ dwa serwery zapisujące dane w tym samym katalogu doprowadziłyby do jego uszkodzenia. Gdy baza danych działa, należy użyć exec, aby wejść do uruchomionego kontenera. Kwestia tego, czy baza danych powinna w ogóle znajdować się w Compose, jest odrębną decyzją, a uruchamianie bazy danych w Dockerze lub na hoście przedstawia wady i zalety tego rozwiązania.
Usługi wymagające konsoli przy starcie: stdin_open i tty
Polecenia exec i run obsługują powłoki otwierane ręcznie. Usługa, której główny proces jest z natury interaktywny, wymaga dwóch kluczy w pliku compose:
services:
console:
image: python:3.12-slim
command: python
stdin_open: true
tty: truestdin_open: true to docker run -i, a tty: true to docker run -t. Bez nich kontener uruchamia się i natychmiast kończy działanie z kodem 0, a docker compose ps -a pokazuje Exited (0). Nic nie uległo awarii. python bez terminala na stdin natychmiast odczytuje koniec pliku i kończy pracę w sposób standardowy, co jest poprawnym zachowaniem dla programu, w którym nikt nie pisze.
Po ustawieniu obu kluczy można podłączyć się do uruchomionego procesu:
docker attach $(docker compose ps -q console)Odłączenie następuje po naciśnięciu Ctrl-P, a następnie Ctrl-Q, co pozostawia proces w stanie uruchomionym. Ta sekwencja działa tylko wtedy, gdy kontener ma otwarte zarówno TTY, jak i stdin. Z kolei Ctrl-C wysyła sygnał przerwania (SIGINT) do PID 1. Zatrzymuje to usługę tylko wtedy, gdy program został napisany tak, aby kończyć działanie po otrzymaniu SIGINT, ponieważ Linux nie zapewnia domyślnej obsługi sygnałów dla PID 1, więc nie należy używać tej kombinacji do odłączania.
W przypadku zwykłych usług należy pozostawić oba klucze wyłączone. Serwer WWW nigdy nie odczytuje stdin, a tty: true powoduje, że wiele programów przełącza się na kolorowe wyjście i buforowanie liniowe, ponieważ "uważają", że są obserwowane przez użytkownika, co zapełnia docker compose logs kodami sterującymi.
Dlaczego skryptowe polecenie exec zawodzi w cron i CI: flaga -T
Polecenie exec, które działa w terminalu, może zawieść podczas uruchamiania przez skrypt, w zadaniu cron, w środowisku continuous integration (CI) lub w potoku (pipe):
the input device is not a TTYCompose żąda pseudo-terminala, gdy jego własne wyjście trafia do terminala, a żądanie to kończy się niepowodzeniem przed uruchomieniem polecenia, jeśli stdin również nie jest terminalem. Nowsze wersje Docker CLI określają ten sam błąd jako cannot attach stdin to a TTY-enabled container because stdin is not a terminal. Obecny Compose automatycznie pomija to żądanie, gdy wyjście jest przekierowane lub przesyłane potokiem, co jest standardem w cron. Niektóre wcześniejsze wydania żądały go niezależnie od warunków, a cat dump.sql | docker compose exec db psql wpisane w wierszu poleceń nadal wywołuje to zachowanie. -T wyłącza to żądanie w każdym przypadku, dlatego należy stosować tę flagę w skryptach zamiast polegać na automatycznym wykrywaniu:
0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump-T jest istotne z drugiego powodu. TTY nadpisuje strumień bajtów podczas przesyłania, więc skompresowany zrzut danych przechodzący przez terminal dociera uszkodzony. Należy jawnie przekazać -T dla każdego przekierowanego lub przesyłanego potokiem wyjścia.
Dwie dodatkowe uwagi dotyczące cron. Należy przekazać -f wraz ze ścieżką bezwzględną, ponieważ cron uruchamia zadanie z katalogu domowego, w którym nie ma pliku compose, co powoduje zatrzymanie Compose z komunikatem no configuration file provided: not found. Ponadto exec zwraca kod wyjścia uruchomionego polecenia, więc nieudane pg_dump spowoduje przerwanie skryptu przy włączonym set -e, zamiast zapisu pustej kopii zapasowej i zgłoszenia sukcesu. Pozostałe codzienne polecenia zostały zebrane w ściądze poleceń Compose, którą warto mieć pod ręką podczas pracy ze skryptami.
Dlaczego zmiany wprowadzone wewnątrz kontenera znikają
Instalacja narzędzia za pomocą exec, edycja pliku konfiguracyjnego i rozwiązanie problemu kończą się utratą zmian po tygodniu. Jest to zgodne z założeniami działania warstwy zapisu kontenera. docker compose up -d po każdej zmianie tagu obrazu lub definicji usługi niszczy stary kontener i tworzy nowy na podstawie obrazu, przez co każda ręczna edycja zostaje usunięta wraz ze starym kontenerem.
docker compose restart działa inaczej. Zatrzymuje i uruchamia ten sam kontener, więc ręczne edycje przetrwają tę operację. Dlatego ręczna poprawka może wydawać się trwała przez tygodnie, a następnie zniknąć podczas niezwiązanej z nią aktualizacji. Wolumeny nazwane oraz bind mounts przetrwają obie operacje, ponieważ ich dane znajdują się poza kontenerem, a bind mounts i wolumeny nazwane wyjaśnia, co wybrać dla danych, które mają zostać zachowane.
Należy traktować powłokę exec jako miejsce do odczytu i testowania. Po ustaleniu poprawki należy ją wprowadzić w sposób trwały: pakiet w Dockerfile, ustawienie w pliku compose. Następnie należy wykonać docker compose up -d, aby zastosować zmiany, i potwierdzić za pomocą kolejnego exec, że nowy kontener faktycznie je zawiera.
FAQ
Jaka jest różnica między docker compose exec a docker compose run?
Polecenie exec uruchamia komendę wewnątrz działającego kontenera, obok głównego procesu, i pomija entrypoint obrazu. Polecenie run tworzy nowy kontener na podstawie tej samej definicji usługi, używając tego samego obrazu, zmiennych środowiskowych, wolumenów i sieci, przepuszcza komendę przez entrypoint oraz uruchamia najpierw usługi depends_on. Polecenie run nie publikuje portów usługi, chyba że dodasz --service-ports lub mapowanie -p. Używaj exec do inspekcji działającej usługi. Używaj run --rm, gdy usługa jest zatrzymana lub gdy potrzebujesz osobnego kontenera, pamiętając jednak, że nazwane wolumeny i bind mounts są współdzielone.
Dlaczego docker compose exec zgłasza, że usługa nie działa?
Polecenie exec łączy się z istniejącym kontenerem i nie może go utworzyć, więc zatrzymana lub awaryjnie zakończona usługa zwraca service "web" is not running. Sprawdź docker compose ps -a, które wyświetla listę kontenerów z kodem wyjścia, takim jak Exited (1), i odczytaj docker compose logs web, aby poznać przyczynę zatrzymania. Aby mimo to uzyskać powłokę, uruchom docker compose run --rm --entrypoint sh web. Buduje to nowy kontener na podstawie tej samej definicji usługi, nie uruchamiając przy tym uszkodzonego polecenia startowego.
Jak otworzyć powłokę, gdy obraz nie zawiera bash?
Błąd docker compose exec web bash z komunikatem exec: "bash": executable file not found in $PATH oznacza, że w katalogach zdefiniowanych w PATH kontenera nie znaleziono bash, co niemal zawsze oznacza, że obraz go nie zawiera. Jest to typowe dla obrazów opartych na Alpine. Użyj docker compose exec web sh, ponieważ BusyBox udostępnia /bin/sh. W obrazie bez powłoki polecenie exec nie może jej otworzyć, ale nadal może uruchamiać pliki wykonywalne zawarte w obrazie. Przełącz się na tag :debug obrazu, jeśli wydawca go oferuje, lub uruchom kontener debugujący w przestrzeniach nazw celu za pomocą docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot, gdzie $CID pochodzi z docker compose ps -q web.
Dlaczego moje polecenie exec kończy się błędem "the input device is not a TTY" w cron?
Polecenie docker compose exec żąda pseudo-terminala, gdy jego wyjście jest kierowane do terminala, a żądanie to kończy się niepowodzeniem, jeśli stdin również nie jest terminalem. Obecne wersje Compose automatycznie pomijają to żądanie przy przekierowaniu wyjścia, co jest typowe dla cron, jednak niektóre wcześniejsze wydania żądały go niezależnie, a przesyłanie wejścia do exec z interaktywnej powłoki nadal wywołuje ten problem. Dodaj -T, aby wyłączyć to zachowanie: docker compose exec -T db pg_dump -U postgres app. Używaj -T również dla każdego przekierowanego lub przesyłanego wyjścia, ponieważ TTY zmienia strumień bajtów i uszkadza zrzut binarny. W cron należy również przekazać -f wraz z pełną ścieżką do pliku compose, w przeciwnym razie Compose zakończy działanie z błędem no configuration file provided: not found.
Czy zmiany wprowadzone wewnątrz kontenera za pomocą exec przetrwają restart?
Zmiany przetrwają docker compose restart, który używa ponownie tego samego kontenera. Zostaną utracone przy docker compose up -d po każdej zmianie obrazu lub konfiguracji, ponieważ proces ten odtwarza kontener z obrazu i odrzuca jego warstwę zapisu. Dane zapisane w nazwanych wolumenach lub bind mounts przetrwają oba te procesy, ponieważ znajdują się poza kontenerem. Wprowadzaj zmiany diagnostyczne za pomocą exec, a następnie umieść trwałą wersję w Dockerfile lub pliku compose.