Learning Platform
Глоссарий Troubleshooting
Урок 15.02 · 22 мин
Средний
dockerdebugexec

exec в контейнер

После docker logs второй главный debug-инструмент — docker exec. Это способ запустить произвольный процесс внутри уже работающего контейнера, в тех же namespaces, с теми же volume’ами, в той же сети. Через exec ты попадаешь «внутрь» контейнера и можешь смотреть файлы, проверять конфиги, дёргать команды.

В этом уроке: bash vs sh, как зайти в distroless без shell вообще, разница между exec и attach, как стать root в контейнере, который запущен от не-root юзера.


Тулзы для процессов — ps, pgrep, kill, /proc

docker exec -it bash

Стандартный вход в контейнер:

docker exec -it pg bash

Что происходит:

  1. docker exec запускает процесс внутри namespaces контейнера pg. Это не VM, это просто новый процесс в тех же namespaces — PID, mount, network, user namespaces те же.
  2. Команда — bash. Это интерактивный shell.
  3. -i (keep STDIN open) — без него bash прочитает EOF из stdin и сразу выйдет.
  4. -t (allocate TTY) — без него bash не покажет prompt и не будет реагировать на Ctrl+C нормально.

Внутри:

root@5fa3:/# whoami
root

root@5fa3:/# ls /var/lib/postgresql/data
PG_VERSION  base  global  pg_commit_ts  pg_dynshmem  ...

root@5fa3:/# ps aux | head
USER  PID  ...
postgres  1  ...  postgres -c config_file=/etc/postgresql/postgresql.conf
postgres  56 ...  postgres: checkpointer
postgres  57 ...  postgres: background writer
...

root@5fa3:/# exit

Выход — exit или Ctrl+D. Контейнер сам не останавливается — exec-процесс просто завершается.

NOTE

PID 1 внутри контейнера — это главный процесс (postgres, redis, nginx). Когда ты делаешь docker exec — это новый процесс внутри тех же namespaces, но НЕ дочерний к PID 1. У него свой PID (обычно >100), он не привязан к жизни PID 1 как обычный fork — ты можешь убить bash через exit, postgres продолжит работать.


bash vs sh — что доступно

Не во всех образах есть bash. Картина для типичных DE-образов:

ОбразДоступные shells
postgres:16 (Debian)bash, sh (= dash)
python:3.13 (Debian)bash, sh
python:3.13-slim (Debian)bash, sh
python:3.13-alpinesh (= ash, BusyBox) — нет bash
nginx:alpinesh (ash) — нет bash
gcr.io/distroless/python3никаких shells

Если docker exec -it pg-alpine bash падает с executable file not found in $PATH: unknown, попробуй sh:

docker exec -it pg-alpine sh

В alpine это будет ash (BusyBox), синтаксис почти POSIX. Большинство команд (ls, cat, grep) работают, но без bash-фичей типа [[ ... ]] или массивов.

Для скриптов универсальный приём:

docker exec -it container_name sh -c 'command here'

sh -c есть везде, где есть shell вообще.


Distroless: shell’а нет

Distroless-образы (gcr.io/distroless/...) — это образы без package manager’а, без shell, без coreutils. Только бинарь приложения + минимальные библиотеки. Безопасность: меньше attack surface. Размер: меньше слоёв.

Минусы для дебага: ни docker exec -it ... bash, ни sh не работают:

docker exec -it my-app bash
# OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH

docker exec -it my-app sh
# То же самое.

Что делать. Есть несколько вариантов:

Вариант 1: distroless:debug образ. Google публикует gcr.io/distroless/python3-debian12:debug — тот же образ, но с BusyBox внутри. Для разработки берёшь :debug, для прода — обычный.

Вариант 2: nsenter с хоста. На Linux-хосте можно «впрыгнуть» в namespaces контейнера через nsenter:

# Узнать PID процесса контейнера на хосте
docker inspect my-app --format '{{.State.Pid}}'
# 12345

# Войти во все namespaces (-a), запустить /bin/sh
sudo nsenter -t 12345 -a -n /bin/sh

Но shell должен быть на хосте, и nsenter пробросит его внутрь namespaces. Это работает только на Linux-хосте, не на Docker Desktop/OrbStack (там хост — это VM, до которой не дотянуться).

Вариант 3: ephemeral debug container (как kubectl debug):

docker run -it --pid=container:my-app --net=container:my-app --rm \
  busybox sh

Запускает busybox в тех же pid и net namespaces, что и my-app. Внутри видишь те же процессы и сеть, но не mount namespace (т.е. файловую систему my-app напрямую не увидишь).

Для типичного DE-работы (Python ETL, Postgres, MinIO) обычно есть shell в образе — distroless встречается реже. Но знать про эти варианты полезно.

Способы попасть внутрь контейнера
есть bashpostgres, python:slim, ubuntu, debian — debian-based образы
docker exec -it bashСтандартный вход. -i stdin, -t pty
только shalpine-образы: BusyBox ash вместо bash. Большинство команд работают, но без bash-фичей
docker exec -it shFallback. Для скриптов sh -c command
distrolessНет shell вообще. Только приложение и runtime libs
nsenter / debug imageИспользовать :debug-вариант distroless, или nsenter с Linux-хоста, или ephemeral контейнер с busybox в тех же namespaces

exec —user root

Контейнеры часто запускаются от не-root user (best practice безопасности). Пример: USER 1000 в Dockerfile или user: "1000:1000" в compose. Внутри ты — UID 1000, и многих файлов не прочитать.

Чтобы стать root для debug — --user 0 (или --user root):

docker exec -it --user root my-app sh

Это работает, потому что Docker daemon крутится от root и может стартануть любой UID внутри контейнера. Это не security risk для уже-работающего контейнера — но root-флаг не означает, что приложение само работает от root. Только твой exec-процесс.

Альтернатива через --user 0:0:

docker exec -it --user 0:0 my-app sh

0:0 — UID 0, GID 0. Тот же эффект.

WARNING

Если контейнер запущен в rootless Docker, --user root работает только для UID 0 внутри user namespace — на хосте это уже маппится в обычного пользователя. Это безопаснее, но dollarые «debug as root» не дают полный доступ к хосту.


exec vs attach

Есть похожая команда — docker attach. Они делают разное.

  • docker exec -it pg bash — запускает новый процесс bash внутри контейнера. У него отдельный stdin/stdout.
  • docker attach pg — подключается к существующему stdin/stdout процесса PID 1.

attach полезен для контейнеров с интерактивным процессом — например, Postgres, запущенный с -it и стандартным input. Но в 99% случаев DE-работы PID 1 — это сервис (postgres, redis, app), а не shell. attach к нему = смотрим логи в режиме «follow», и Ctrl+C остановит PID 1 = контейнер целиком.

docker attach pg
# логи postgres стримятся...
^C
# контейнер остановлен!

Чтобы безопасно отсоединиться от attach — Ctrl+P Ctrl+Q (detach sequence). Это запоминается тяжело, и поэтому 99% времени проще использовать docker logs -f для просмотра логов и docker exec для интерактивной работы.


Полезные exec-однострочники

# Проверить конфиг внутри
docker exec pg cat /etc/postgresql/postgresql.conf | head -20

# Дёрнуть SQL без интерактивной psql
docker exec pg psql -U postgres -c "SELECT 1"

# Список файлов в volume через контейнер
docker exec pg ls -la /var/lib/postgresql/data/

# Проверить env-переменные внутри
docker exec pg env | grep PG

# Запустить bash, но не уходить интерактивно (полезно в скриптах)
docker exec pg bash -c "psql -U postgres -c 'SELECT 1' && echo OK"

# top-like мониторинг
docker exec -it pg top

# Скопировать файл из контейнера наружу (альтернатива docker cp)
docker exec pg cat /var/log/postgresql.log > postgres.log

Distroless debug pattern

Если у тебя в проде distroless и нужно дебажить — workflow такой:

  1. Базовый образ собирается из distroless (gcr.io/distroless/python3).
  2. Для разработки/staging есть второй target в Dockerfile: FROM gcr.io/distroless/python3:debug.
  3. На staging/dev запускается debug-вариант с busybox внутри.
  4. На проде — обычный distroless без shell.
FROM python:3.13-slim AS builder
RUN pip install -r requirements.txt
# ...

FROM gcr.io/distroless/python3-debian12 AS runtime
COPY --from=builder /app /app
USER nonroot
ENTRYPOINT ["python", "/app/main.py"]

FROM gcr.io/distroless/python3-debian12:debug AS runtime-debug
COPY --from=builder /app /app
USER nonroot
ENTRYPOINT ["python", "/app/main.py"]

docker build --target runtime -> production. docker build --target runtime-debug -> debuggable image.

Что происходит при docker exec
docker exec my-app shCLI шлёт запрос в daemon
daemon резолвит namespacesБерёт PID 1 контейнера, читает /proc/<pid>/ns/ — там симлинки на namespace inodes
setns(pid_ns, mnt_ns, net_ns, ...)containerd через runc делает setns() для каждого namespace, привязывая новый процесс к тем же namespaces, что PID 1
fork + execve shФорк и запуск sh — теперь это процесс внутри namespaces контейнера, видит ту же файловую систему, сеть, процессы

Попробуй сам

  1. Запусти postgres:16. Зайди через docker exec -it pg bash. Сделай ls /var/lib/postgresql/data, посмотри pg_isready, выйди через exit.
  2. Запусти python:3.13-alpine с command sleep 1000: docker run -d --name py-alpine python:3.13-alpine sleep 1000. Попробуй docker exec -it py-alpine bash — упадёт. Сделай docker exec -it py-alpine sh — работает.
  3. Запусти контейнер от non-root user: docker run -d --name pg2 --user 1000:1000 -e POSTGRES_PASSWORD=x postgres:16 (упадёт, но не суть). Подними обычный pg, потом войди через docker exec -it --user 1000 pg sh, сравни с docker exec -it pg bash.
  4. На Linux-хосте: узнай PID контейнера docker inspect pg --format '{{.State.Pid}}', попробуй sudo nsenter -t <pid> -a /bin/sh.
  5. НЕ делай attach без необходимости, но если хочешь почувствовать разницу: docker attach pg, посмотри как стримятся логи, аккуратно выйди через Ctrl+P Ctrl+Q (НЕ Ctrl+C).

Проверка знанийKnowledge check
Чем docker exec отличается от docker attach, и почему 99% Junior DE-работы по дебагу должны идти через exec, а не attach?
ОтветAnswer
docker exec -it pg bash — запускает НОВЫЙ процесс bash внутри namespaces контейнера. У него отдельный stdin/stdout, exit из него не влияет на PID 1 контейнера. Это правильный инструмент для интерактивного входа. docker attach pg — подключается к stdin/stdout/stderr ЕСТЬ процесса PID 1. То есть ты «слушаешь» главный процесс, который уже работает. Опасность: Ctrl+C в attach уйдёт в PID 1 как SIGINT -> контейнер остановится. Чтобы безопасно отсоединиться от attach, нужно специальный escape Ctrl+P Ctrl+Q. В 99% DE-работы PID 1 — это сервис (postgres, redis, ETL-app), а не shell. Делать к нему attach — значит рисковать случайно убить контейнер Ctrl+C. Лучшие практики: - Смотришь логи -> docker logs -f - Заходишь дебажить -> docker exec -it bash (или sh для alpine) - Запускаешь одноразовую команду -> docker exec pg psql -c "SELECT 1" attach остаётся для редких случаев типа интерактивного процесса в контейнере (запустил python REPL как PID 1) — но даже там обычно проще остановить контейнер и запустить заново через docker run -it.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. docker exec -it pg-alpine bash падает с 'executable file not found in $PATH'. В чём причина и как починить?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5