Как открыть интерактивную оболочку в 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 команда exec завершается с такой ошибкой:
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknownЭто сообщение не указывает на проблему с самим exec. Оно означает, что файл bash не найден в каталогах, перечисленных в переменной PATH контейнера. Для оболочки это почти всегда означает, что образ её не содержит. Alpine поставляется с BusyBox, который предоставляет ash в виде /bin/sh, но не содержит bash. В этом случае используйте sh:
docker compose exec web shОбразы на базе Debian и Ubuntu, включая теги -slim, содержат bash. Bash предоставляет историю команд и более удобное автодополнение. Поэтому сначала пробуйте bash, а если он отсутствует — переходите на sh. sh присутствует практически в любом образе общего назначения.
В некоторых образах оболочка отсутствует вовсе. Образы Distroless и образы, собранные FROM scratch, содержат только бинарный файл приложения и необходимые библиотеки. Это сделано намеренно: отсутствующую оболочку нельзя использовать для атаки. В таких образах sh завершается с той же ошибкой, и пробовать больше нечего. Существует два способа решения. Образы Distroless от Google имеют теги :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 и приложение также запущено от root. Если приложение запущено от другого пользователя, проверка разрешений ptrace в ядре отклонит запрос, так как Docker по умолчанию не предоставляет SYS_PTRACE. В этом случае добавьте --cap-add SYS_PTRACE в строку docker run.
Если сервис не запущен, используйте 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 покажет их, а docker compose rm удалит их вместе с другими остановленными контейнерами сервиса.
Два аспекта работы run часто вызывают вопросы. Она не публикует порты сервиса, если вы не добавите --service-ports или не пробросите порт вручную с помощью -p. Это сделано намеренно: попытка запустить второй контейнер с привязкой к порту 8080, который уже занят первым, приведет к ошибке bind: address already in use. Кроме того, run запускает все сервисы, указанные в 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: truestdin_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, напротив, отправляет сигнал прерывания (SIGINT) процессу с PID 1. Это остановит сервис только в том случае, если программа написана так, чтобы завершаться по SIGINT, поскольку Linux не предоставляет PID 1 обработку сигналов по умолчанию, поэтому не используйте этот способ для отключения.
Для обычных сервисов оставьте оба ключа выключенными. Веб-сервер никогда не читает stdin, а tty: true заставляет многие программы переключаться на цветной вывод и построчную буферизацию, так как они «думают», что за ними наблюдает человек, что заполняет docker compose logs управляющими последовательностями.
Почему скриптовый exec завершается ошибкой в cron и CI: флаг -T
Команда exec, которая работает в терминале, может завершиться ошибкой при запуске из скрипта, задания cron, исполнителя CI (continuous integration) или через конвейер (pipe):
the input device is not a TTYCompose запрашивает псевдотерминал (pseudo terminal), если его собственный вывод направлен в терминал. Запрос завершается ошибкой до выполнения вашей команды, если stdin также не является терминалом. В исходном коде актуальной версии Docker CLI эта ошибка описывается как cannot attach stdin to a TTY-enabled container because stdin is not a terminal. Современный Compose автоматически пропускает этот запрос, если вывод перенаправлен или передан через конвейер (как это обычно происходит в cron) — эта проверка присутствует в исходном коде с марта 2022 года. Некоторые более ранние версии запрашивали терминал в любом случае, и cat dump.sql | docker compose exec db psql, введенная в командной строке, до сих пор вызывает этот запрос. Флаг -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 или маппинг -p. Используйте 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 не найден в директориях из 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 запрашивает псевдотерминал, когда вывод направляется в терминал, и запрос завершается ошибкой, если stdin также не является терминалом. Современные версии Compose автоматически пропускают этот запрос при перенаправлении вывода, как это обычно бывает в cron, но некоторые версии до марта 2022 года запрашивали его в любом случае; передача данных в exec через конвейер из интерактивной оболочки также вызывает эту ошибку. Добавьте -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.