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
Что происходит:
docker execзапускает процесс внутри namespaces контейнераpg. Это не VM, это просто новый процесс в тех же namespaces — PID, mount, network, user namespaces те же.- Команда —
bash. Это интерактивный shell. -i(keep STDIN open) — без него bash прочитает EOF из stdin и сразу выйдет.-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-процесс просто завершается.
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-alpine | sh (= ash, BusyBox) — нет bash |
nginx:alpine | sh (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 встречается реже. Но знать про эти варианты полезно.
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. Тот же эффект.
Если контейнер запущен в 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 такой:
- Базовый образ собирается из distroless (
gcr.io/distroless/python3). - Для разработки/staging есть второй target в Dockerfile:
FROM gcr.io/distroless/python3:debug. - На staging/dev запускается debug-вариант с busybox внутри.
- На проде — обычный 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.
Попробуй сам
- Запусти
postgres:16. Зайди черезdocker exec -it pg bash. Сделайls /var/lib/postgresql/data, посмотриpg_isready, выйди черезexit. - Запусти
python:3.13-alpineс commandsleep 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— работает. - Запусти контейнер от 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. - На Linux-хосте: узнай PID контейнера
docker inspect pg --format '{{.State.Pid}}', попробуйsudo nsenter -t <pid> -a /bin/sh. - НЕ делай attach без необходимости, но если хочешь почувствовать разницу:
docker attach pg, посмотри как стримятся логи, аккуратно выйди через Ctrl+P Ctrl+Q (НЕ Ctrl+C).