SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor

Як відкрити shell у запущеному сервісі Docker Compose

Дізнайтеся, як відкрити shell у запущеному сервісі через docker compose exec і коли застосувати run --rm для зупиненого сервісу без змін.

Отримайте інтерактивну оболонку за допомогою docker compose exec

docker compose exec web bash відкриває інтерактивну оболонку всередині контейнера, який уже запущений як сервіс web. Ім’я після exec — це ім’я сервісу з вашого compose.yaml, а не ім’я контейнера. Якщо в образі немає bash, замість нього вкажіть sh.

docker compose ps
docker compose exec web bash

Спочатку виконайте docker compose ps. Команда має показати web зі станом running. Потім друга команда відкриє запрошення оболонки всередині контейнера, а exit або Ctrl-D поверне вас на хост. Після виходу сервіс продовжує працювати, оскільки exec запускає другий процес поруч з основним. Закриття оболонки не впливає на PID 1 (ідентифікатор процесу 1) — процес, для запуску якого було створено контейнер.

Це один із двох способів підключитися. exec підключається до контейнера, який уже існує. docker compose run створює новий контейнер на основі того самого визначення сервісу. Майже все інше в цьому посібнику випливає з цієї єдиної відмінності.

Чому -it є необов’язковим у Compose, але потрібен у звичайному docker

Два прапорці керують інтерактивною частиною сеансу. -i залишає stdin відкритим, тому введені вами дані надходять до процесу. -t виділяє псевдотермінал, який називається TTY, тому оболонка виводить запрошення та обробляє клавіші зі стрілками. Звичайний docker exec за замовчуванням вимикає обидва параметри, тому в кожному наведеному прикладі вказано docker exec -it. docker compose exec вмикає обидва параметри, тому docker compose exec -it web bash і docker compose exec web bash виконують ту саму дію. Compose також приймає -it, щоб зберегти звичний синтаксис для старих команд.

Відсутність TTY помітна за кілька секунд. Оболонка запускається, але не виводить запрошення, а Ctrl-C не надходить до процесу. Зворотний випадок, коли потрібно попросити Compose не виділяти TTY, має окремий прапорець і власний розділ нижче.

Що робити, якщо в образі немає bash

Якщо попросити bash в образі на основі Alpine, виконання exec завершиться помилкою:

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

Це повідомлення не означає проблему з exec. Воно означає, що потрібного бінарного файла немає в образі. Alpine постачається з BusyBox, який надає ash як /bin/sh, але не містить bash. Тому слід використовувати sh:

docker compose exec web sh

В образах на основі Debian і Ubuntu, зокрема в тегах -slim, bash є. Він надає історію команд і розширене автодоповнення. Тому спочатку спробуйте bash, а якщо його немає, використайте sh. sh є майже в кожному образі загального призначення.

У деяких образах немає оболонки взагалі. Distroless-образи та образи, зібрані FROM scratch, навмисно містять лише бінарний файл застосунку і його бібліотеки. Оболонка, якої немає в образі, не може бути використана проти нього. У таких образах sh завершується тим самим повідомленням, і більше немає чого спробувати. Є два робочі підходи. Google публікує distroless-образи з тегами :debug, які додають оболонку BusyBox. Тимчасова заміна тегу дає змогу отримати доступ до контейнера. Або запустіть окремий контейнер у просторах імен цільового контейнера:

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

Тепер інструменти netshoot працюють у мережевому просторі імен застосунку, тому curl localhost:8080 і ss -lntp поводяться так, ніби ви перебуваєте всередині нього. Файлова система, яку ви бачите, належить netshoot, а не застосунку. Оскільки простір імен процесів спільний, ls /proc/1/root/ отримує доступ до власних файлів цільового контейнера, якщо команду запущено від root.

Якщо сервіс не запущений, використовуйте docker compose run --rm

Для роботи exec потрібен запущений контейнер. Якщо вказати зупинений сервіс, команда завершиться з помилкою:

service "web" is not running

Команда не запустить нічого автоматично. Це зробить docker compose run:

docker compose run --rm web bash

Команда run створює новий контейнер на основі визначення сервісу web, використовуючи той самий образ, змінні середовища, томи та мережі, і замінює команду сервісу на введену вами. --rm видаляє цей контейнер після завершення роботи. Якщо не вказати --rm, невидалені контейнери накопичуватимуться під іменами на кшталт myproject-web-run-4f1c2b. Їх покаже docker compose ps -a, але жодна інша команда їх не видалить.

Дві особливості run часто дивують користувачів. Команда не публікує порти сервісу, якщо не додати --service-ports. Це зроблено навмисно: другий контейнер не зможе прив’язати порт 8080 на хості, доки його використовує перший контейнер, і завершиться з помилкою bind: address already in use. Команда також запускає все, що сервіс визначає в depends_on, перш ніж ви отримаєте оболонку. Тому навіть швидка перевірка контейнера може запустити базу даних і кеш. --no-deps вимикає цю поведінку.

Команда run проходить через ENTRYPOINT образу, а exec — ні. Команда exec запускає вказану команду безпосередньо в наявному контейнері, тому скрипт entrypoint її не отримує. Під час використання run ваша bash надходить до цього скрипту як аргументи. Багато офіційних образів завершують entrypoint конструкцією exec "$@", тому команда передається без змін, і ви отримуєте оболонку. Скрипт, який сам обробляє власні аргументи, поводитиметься інакше. У такому разі для цього запуску замініть entrypoint:

docker compose run --rm --entrypoint sh web

Це найпоширеніша причина, через яку команда, що працює в exec, поводиться інакше в run. Розділ між command і entrypoint пояснює, яку саме частину конфігурації образу ви щоразу замінюєте.

exec або run: як вибрати

  • Для exec потрібен запущений контейнер. run цього не потребує та може запустити залежності.
  • exec бачить поточний список процесів і файли в їхньому фактичному стані, зокрема все, що застосунок записав після запуску. run отримує чисту копію image, тому цих змін там немає.
  • exec пропускає entrypoint. run його запускає.
  • Після run контейнер залишається, якщо не передати --rm.

Використовуйте exec, щоб перевірити, що відбувається насправді. Використовуйте run --rm для тимчасової копії того самого середовища, одноразової команди міграції або коли основний сервіс не працює достатньо довго, щоб підключитися до нього через exec.

Корисні параметри exec: користувач, робочий каталог і репліки

Більшість образів переходять на непривілейованого користувача, тому встановлення діагностичного інструмента в оболонці exec зупиняється на цьому етапі:

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

-u root відкриває root-оболонку в тому самому контейнері:

docker compose exec -u root web sh

-w /srv/app задає робочий каталог лише для цієї команди. -e KEY=value додає змінну середовища до вашого сеансу, але не до сервісу. Якщо сервіс працює з кількома репліками, --index 2 визначає, до якого контейнера ви підключитеся. Якщо ви з’ясовуєте причину проблеми з власником файлів у змонтованому каталозі, у матеріалі PUID і PGID в образах контейнерів пояснюється, чому саме числові ідентифікатори, а не імена користувачів, визначають, хто може записувати дані в цей каталог.

Отримайте оболонку psql або mysql усередині контейнера бази даних

Клієнт уже є в образі бази даних, тому він не потрібен на хості. Також не потрібно публікувати порт:

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

Образи Postgres містять psql, образи MySQL — mysql, а образи MariaDB — mariadb. Підключення виконується з контейнера, тому це працює, навіть якщо файл compose взагалі не публікує порт бази даних. Це безпечніший варіант: через інтернет неможливо підключитися до порту, який ви не публікували.

Є одна поширена помилка, яка може забрати кілька годин. Оболонка розгортає змінні на хості ще до того, як Docker отримує команду, тому -U "$POSTGRES_USER" передає порожній рядок, якщо ця змінна існує лише всередині контейнера. Одинарні лапки та оболонка всередині контейнера розгортають її в правильному місці:

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

Не запускайте docker compose run --rm db без команди в цьому випадку. Це запускає другий сервер Postgres із тим самим томом даних, і він відмовляється запускатися:

FATAL:  lock file "postmaster.pid" already exists

Файл блокування виконує свою функцію, оскільки два сервери, які записують дані в один каталог, пошкодили б його. Поки база даних працює, підключайтеся до запущеного контейнера через exec. Чи має база даних узагалі працювати в Compose — окреме питання. У матеріалі база даних у Docker або на хості наведено основні компроміси.

Сервіси, яким потрібна консоль під час запуску: stdin_open і tty

exec і run застосовують для оболонок, які ви відкриваєте вручну. Сервіс, основний процес якого за своєю природою є інтерактивним, потребує двох ключів у compose-файлі:

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

stdin_open: true — це docker run -i, а tty: true — це docker run -t. Без них контейнер запускається та одразу завершує роботу з кодом 0, а docker compose ps -a показує Exited (0). Збою не сталося. python без термінала у stdin одразу отримує кінець файлу й нормально завершує роботу. Це правильна поведінка для програми, з якою ніхто не взаємодіє через введення.

Якщо встановити обидва ключі, підключіться до запущеного процесу:

docker attach $(docker compose ps -q console)

Щоб від’єднатися, натисніть Ctrl-P, потім Ctrl-Q. Процес продовжить працювати. Ця послідовність працює лише тоді, коли контейнер має TTY і відкритий stdin. Ctrl-C натомість надсилає переривання PID 1 і зупиняє сервіс.

Для звичайних сервісів залишайте обидва ключі вимкненими. Вебсервер ніколи не читає stdin, а tty: true змушує багато програм перемикатися на кольорове виведення та буферизацію за рядками, оскільки вони вважають, що за ними стежить користувач. Через це docker compose logs заповнюється escape-кодами.

Чому виконання скриптів не працює в cron і CI: прапорець -T

Команда exec, яка працює у вашому терміналі, завершується помилкою в завданні cron або runner системи безперервної інтеграції (CI):

the input device is not a TTY

Compose за замовчуванням запитує псевдотермінал, а cron не надає завданню термінал, тому запит завершується помилкою ще до запуску команди. -T вимикає цей запит:

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

-T важливий ще з однієї причини. TTY змінює потік байтів під час виведення, тому стиснений дамп, який проходить через нього, надходить пошкодженим. Для будь-якого перенаправленого або конвеєрного виведення потрібен -T.

Є ще дві особливості cron. Передавайте -f з абсолютним шляхом, оскільки cron запускає завдання з домашнього каталогу, де немає compose-файлу, і Compose завершує роботу з помилкою no configuration file provided: not found. Крім того, exec повертає код завершення виконаної команди, тому помилка pg_dump завершує роботу вашого скрипту з помилкою через set -e, а не створює порожню резервну копію та повідомляє про успішне виконання. Інші команди для повсякденного використання зібрано в шпаргалці команд Compose, яку варто тримати поруч із цими скриптами.

Чому зміни, внесені всередині контейнера, зникають

Ви встановлюєте інструмент за допомогою exec, редагуєте файл конфігурації, усуваєте проблему, а через тиждень виправлення зникає. Це writable layer контейнера, яка працює саме так. docker compose up -d після будь-якої зміни image tag або service definition видаляє старий контейнер і створює новий на основі image, тому всі ручні зміни залишаються у старому контейнері.

docker compose restart працює інакше. Вона зупиняє та запускає той самий контейнер, тому ручні зміни зберігаються. Через це може здаватися, що ручне виправлення працює тижнями, а потім воно зникає під час іншого оновлення. Named volumes і bind mounts переживають обидві операції, оскільки їхні дані зберігаються поза контейнером; у матеріалі bind mounts і named volumes описано, який варіант вибрати для даних, які потрібно зберегти.

Тому сприймайте shell, запущений через exec, як місце для перевірки та тестування. Коли ви визначили потрібне виправлення, запишіть його там, де воно зберігатиметься: пакет — у Dockerfile, параметр — у compose file. Потім docker compose up -d, щоб застосувати зміни, і виконайте ще один exec, щоб переконатися, що новий контейнер справді містить це виправлення.

FAQ

У чому різниця між docker compose exec і docker compose run?

exec виконує команду в уже запущеному контейнері паралельно з основним процесом і пропускає entrypoint образу. run створює новий контейнер на основі того самого визначення сервісу з тим самим образом, змінними середовища, томами та мережами, передає вашу команду через entrypoint і спочатку запускає всі depends_on сервіси. run також не публікує порти сервісу, якщо не додати --service-ports. Використовуйте exec для перевірки активного сервісу. Використовуйте run --rm, якщо сервіс зупинений або ви не хочете його переривати.

Чому docker compose exec повідомляє, що сервіс не запущений?

exec підключається до наявного контейнера й не може створити новий, тому для зупиненого або аварійно завершеного сервісу з’являється service "web" is not running. Перевірте docker compose ps -a. Ця команда показує контейнери, роботу яких завершено, зі статусом на кшталт Exited (1). Прочитайте docker compose logs web, щоб визначити причину зупинки. Щоб у будь-якому разі отримати оболонку, виконайте docker compose run --rm --entrypoint sh web. Команда створює новий контейнер на основі того самого визначення сервісу, не запускаючи несправну команду старту.

Як відкрити оболонку, якщо в образі немає bash?

Якщо docker compose exec web bash завершується з exec: "bash": executable file not found in $PATH, це означає, що в образі відсутній bash. Для образів на основі Alpine це нормально. Використовуйте docker compose exec web sh, оскільки BusyBox надає /bin/sh. Distroless-образи та образи scratch узагалі не містять оболонки, тому жодна команда exec не спрацює. Перейдіть на тег :debug цього образу, якщо видавець його надає, або запустіть debug-контейнер у просторах імен цільового контейнера за допомогою docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot, де $CID походить із docker compose ps -q web.

Чому моя команда exec у cron завершується з помилкою "пристрій введення не є TTY"?

docker compose exec за замовчуванням запитує псевдотермінал, а cron його не надає, тому запит завершується помилкою ще до запуску вашої команди. Додайте -T, щоб вимкнути цю функцію: docker compose exec -T db pg_dump -U postgres app. Також використовуйте -T для будь-якого перенаправленого або конвеєрного виводу, оскільки TTY змінює потік байтів і пошкоджує бінарний дамп. У cron також передайте -f з абсолютним шляхом до compose-файлу, інакше Compose завершиться з no configuration file provided: not found.

Чи зберігаються зміни, внесені в контейнер за допомогою exec, після перезапуску?

Вони зберігаються після docker compose restart, оскільки використовується той самий контейнер. Після docker compose up -d вони втрачаються за будь-якої зміни образу або конфігурації, оскільки контейнер створюється заново з образу, а його доступний для запису шар видаляється. Дані, записані в іменовані томи або bind mounts, зберігаються в обох випадках, оскільки вони розміщені за межами контейнера. Використовуйте exec для діагностичних змін, а постійний варіант додавайте до Dockerfile або compose-файлу.