SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Как открыть интерактивную оболочку в Docker Compose

Используйте команду docker compose exec для подключения к работающему контейнеру или docker compose run --rm для запуска новой сессии. Узнайте, как правильно применять флаги -it.

Получение интерактивной оболочки через 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, выполнение команды завершается ошибкой:

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

Это сообщение не означает проблему с выполнением команды. Оно указывает на то, что запрашиваемый бинарный файл отсутствует в образе. В Alpine используется BusyBox, который предоставляет ash в виде /bin/sh, но не содержит bash. В этом случае используйте sh:

docker compose exec web sh

Образы на базе Debian и Ubuntu, включая теги -slim, содержат bash. 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 запускает вашу команду напрямую в существующем контейнере, поэтому скрипт точки входа её не видит. При использовании run ваш bash передается как аргументы этому скрипту. Многие официальные образы завершают свой скрипт точки входа командой exec "$@", поэтому аргументы передаются дальше, и вы получаете доступ к оболочке. Скрипт, который интерпретирует свои аргументы самостоятельно, поступит с ними иначе; в таком случае для конкретного запуска следует заменить точку входа:

docker compose run --rm --entrypoint sh web

Это самая частая причина, по которой команда, работающая через exec, ведет себя иначе при использовании run, а разделение между command и entrypoint объясняет, какую часть конфигурации образа вы заменяете в каждом случае.

exec или run: как сделать выбор

  • Команда exec требует работающего контейнера. Команда run не требует, и она может запускать зависимости.
  • Команда exec видит текущий список процессов и файлы в их актуальном состоянии, включая всё, что приложение записало с момента запуска. Команда run получает чистую копию образа, поэтому этих данных там нет.
  • Команда exec пропускает entrypoint. Команда run выполняет его.
  • Команда run оставляет после себя контейнер, если вы не используете флаг --rm.

Используйте exec, чтобы увидеть, что происходит в действительности. Используйте run --rm для создания временной копии той же среды, для выполнения разовой команды миграции или в случаях, когда основной сервис не работает достаточно долго, чтобы подключиться к нему через exec.

Полезные флаги exec: пользователь, рабочая директория и реплики

Большинство образов переключаются на пользователя без прав root, поэтому установка диагностических инструментов внутри оболочки 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 управляющими последовательностями.

Почему скриптовый exec завершается ошибкой в cron и CI: флаг -T

Команда exec, которая работает в терминале, завершается ошибкой при запуске в задании cron или в системе непрерывной интеграции (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, редактируете файл конфигурации, устраняете проблему, а через неделю исправление пропадает. Это штатное поведение записываемого слоя контейнера. docker compose up -d после любого изменения тега образа или определения сервиса уничтожает старый контейнер и создает новый из образа, поэтому все ручные правки удаляются вместе со старым контейнером.

docker compose restart работает иначе. Он останавливает и запускает тот же самый контейнер, поэтому ручные правки сохраняются. Именно поэтому ручное исправление может держаться неделями, а затем исчезнуть во время несвязанного обновления. Именованные тома и bind mounts переживают обе операции, так как их данные хранятся вне контейнера, а в bind mounts и именованных томах описано, что именно выбрать для данных, которые вы хотите сохранить.

Поэтому используйте оболочку exec только для чтения и тестирования. Как только вы нашли решение, внесите его туда, где оно сохранится: пакет — в Dockerfile, настройку — в compose-файл. Затем выполните 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, если автор образа его предоставляет, или запустите отладочный контейнер в пространствах имен целевого контейнера с помощью docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot, где $CID берется из docker compose ps -q web.

Почему моя команда exec в cron завершается с ошибкой "the input device is not a 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.