Отладка контейнеров: exec, attach, nsenter, debug

Рейтинг: 67.6% · 8 голосов
Практический курс по Docker: образы, контейнеры, тома, сети, Compose и продакшен. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
Marina_DevOps
Сообщения: 54
Зарегистрирован: 11 май 2026, 05:31

Отладка контейнеров: exec, attach, nsenter, debug

Сообщение Marina_DevOps »

Оглавление курса (46)
  1. Что такое Docker и какие задачи он решает
  2. Установка Docker и запуск первого контейнера
  3. Образы: слои, теги и реестр Docker Hub
  4. Пишем свой Dockerfile
  5. Тома и хранение данных: volumes и bind mounts
  6. Сети в Docker: связываем контейнеры между собой
  7. Переменные окружения и конфигурация контейнеров
  8. Docker Compose: поднимаем многоконтейнерное приложение
  9. Оптимизация образов: multi-stage сборка, размер и кэш слоёв
  10. Логи, отладка и мониторинг контейнеров
  11. Базовая безопасность контейнеров
  12. Подготовка к продакшену: что важно учесть
  13. Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root
  14. Реестры образов: приватные registry, push и pull, теги и digest, imagePullSecrets
  15. BuildKit и buildx: multi-arch сборки, секреты сборки, экспорт кэша
  16. Docker в CI/CD: автосборка, сканирование образов (Trivy, Docker Scout), публикация
  17. Итоговый проект и куда расти: от Dockerfile до прода, обзор оркестрации (Kubernetes, Podman, OCI)
  18. Архитектура Docker: dockerd, containerd, runc и OCI
  19. Как работает изоляция: namespaces и cgroups
  20. Образы изнутри: слои, OverlayFS и storage driver
  21. Жизненный цикл контейнера и политики перезапуска
  22. Отладка контейнеров: exec, attach, nsenter, debug (вы здесь)
  23. Сети Docker глубже: bridge, overlay, macvlan и iptables
  24. Docker Compose глубже: profiles, healthcheck-зависимости, override
  25. Данные и тома глубже: драйверы, бэкап, права
  26. Секреты и конфигурация: как не светить пароли
  27. Ограничение ресурсов: CPU, память, OOM и cgroups
  28. Здоровье и автозапуск: HEALTHCHECK, init и сигналы
  29. Логирование контейнеров: драйверы, ротация, сбор
  30. Мониторинг контейнеров: stats, cAdvisor, Prometheus
  31. Безопасность глубже: rootless, user namespaces, seccomp, capabilities
  32. Supply chain: сканирование, SBOM и подпись образов
  33. Глубокая оптимизация образов: distroless, scratch, кэш
  34. Multi-arch и сборка под ARM: buildx и эмуляция
  35. Реестры в продакшене: Harbor и облачные registry
  36. Docker без Docker: Podman, containerd, nerdctl, Buildah
  37. От Compose к оркестрации: когда контейнеров становится много
  38. Docker Swarm: встроенная оркестрация
  39. Сборка образов в CI без демона: kaniko, BuildKit, Buildah
  40. Траблшутинг Docker: типичные ошибки и как чинить
  41. Производительность и эксплуатация Docker-хоста
  42. Docker Desktop, WSL2 и альтернативы на Windows и Mac
  43. Контейнеризация приложений: бэкенд, фронтенд, БД
  44. Docker и Kubernetes: containerd, миграция, kompose
  45. Лучшие практики и антипаттерны Docker
  46. Сквозной проект: production-ready стек и путь дальше
Контейнер упал на проде в 3 ночи. Ты заходишь, делаешь docker exec -it api bash, а в ответ - exec: "bash": executable file not found in $PATH. Образ distroless, в нем нет ни bash, ни sh, ни ls. Логи скудные, exit code 137, и непонятно: процесс убили по OOM, упёрлись в лимит cgroup или приложение само вышло. Старые курсы тут заканчиваются на "ну, добавь shell в образ и пересобери". Но на проде ты не пересоберёшь образ на лету, а отладка контейнера нужна прямо сейчас, на живом процессе. В этом уроке разберём весь арсенал: от docker exec и docker attach до входа в namespace с хоста через nsenter docker и эфемерных debug-контейнеров. Главное - понять механику, потому что каждый инструмент лезет в контейнер по-своему, и выбор зависит от того, что внутри есть, а чего нет.

exec против attach: новый процесс или PID 1

Это первое, что путают, и путаница дорого стоит. Контейнер - это в первую очередь его главный процесс, PID 1 внутри своего pid-namespace. Когда ты запускаешь docker run nginx, nginx и есть PID 1. Жив PID 1 - жив контейнер; умер - контейнер остановлен.

docker exec запускает новый, отдельный процесс внутри уже существующих namespace контейнера. Ты как будто открываешь второе окно в ту же квартиру: то же сетевое окружение, та же файловая система, те же лимиты, но процесс другой, со своим PID (например, PID 7 внутри контейнера). Это рабочая лошадь отладки.

Код: Выделить всё

docker exec -it api sh
Флаги -i (interactive, держим stdin открытым) и -t (выделяем псевдо-TTY, чтобы работали стрелки, Ctrl+C, перерисовка). Без -t shell будет вести себя странно: нет промпта, ввод не эхоится. Полезные флаги exec, которые редко показывают:
  • -u 0 или -u root - войти от конкретного UID. Контейнер бежит под непривилегированным юзером, а тебе нужно посмотреть файлы root. exec умеет сменить пользователя для нового процесса.
  • -w /tmp - задать рабочую директорию.
  • -e KEY=val - добавить переменную окружения только для этого процесса отладки.
  • --privileged - дать новому процессу повышенные права (например, для strace, которому нужен CAP_SYS_PTRACE).
Запусти без -it разовую команду и сразу получишь вывод - удобно для скриптов:

Код: Выделить всё

docker exec api cat /etc/resolv.conf
docker exec api ps -ef
docker exec api env
docker attach - совсем другое. Он подключает твой терминал к stdin/stdout/stderr уже работающего PID 1. Новый процесс не создаётся. Ты буквально смотришь на консоль главного процесса.

Код: Выделить всё

docker attach api
Когда это нужно? Если приложение пишет интерактивный REPL в свой stdout (Node.js inspector, python -i, консоль БД, запущенная как CMD) - attach даёт тебе эту консоль. Но тут две ловушки, на которых горят все.

Грабля номер один: нажал Ctrl+C в attach - и ты послал SIGINT в PID 1. Приложение умрёт, контейнер остановится. Это не "выход из сессии", это сигнал главному процессу. Безопасно отцепиться без убийства - последовательность Ctrl+P, затем Ctrl+Q (detach keys). Запомни её, иначе будешь ронять прод.

Грабля номер два: если у PID 1 нет интерактивного ввода (тот же nginx), attach покажет просто поток логов и зависнет - делать там нечего. Для 95% отладки тебе нужен exec, а не attach. Простое правило: attach - поговорить с главным процессом, exec - запустить рядом свой инструмент.

Изображение

Когда в образе нет shell: nsenter с хоста

Distroless, scratch, минимальный образ на основе Chainguard - внутри нет /bin/sh, нет coreutils, ничего. Это сделано нарочно: меньше поверхность атаки, меньше CVE, меньше вес. Но docker exec бессилен - ему нечего там запустить. И вот тут вспоминаем, что контейнер - это не магия, а процесс на хосте, обёрнутый в namespace и cgroup. Раз процесс есть на хосте, мы можем войти в его namespace инструментами хоста, а не контейнера.

Этим занимается nsenter (enter namespace) из пакета util-linux. Сначала узнаём PID главного процесса контейнера на хосте:

Код: Выделить всё

PID=$(docker inspect --format '{{.State.Pid}}' api)
echo $PID
# 34567
Это PID на хосте, не внутри контейнера (внутри он 1). Теперь входим в namespace этого процесса со всеми инструментами хоста:

Код: Выделить всё

sudo nsenter -t 34567 -m -u -i -n -p
Разбор флагов - именно тут вся суть:
  • -t 34567 - target, целевой PID, чей namespace берём.
  • -m (mount) - войти в mount-namespace: видишь файловую систему контейнера.
  • -n (net) - сетевой namespace: видишь его интерфейсы, можешь сделать ss или tcpdump по сети контейнера.
  • -p (pid) - pid-namespace: видишь процессы контейнера с их внутренней нумерацией.
  • -u (uts), -i (ipc) - hostname и IPC.
После nsenter ты внутри окружения контейнера, но запускаешь бинарь хоста. То есть в distroless без shell ты делаешь nsenter ... /bin/bash и получаешь bash хоста, работающий в namespace контейнера. Можно сразу подсунуть команду:

Код: Выделить всё

sudo nsenter -t 34567 -n ss -tlnp
sudo nsenter -t 34567 -n -p strace -p 1
Первая показывает слушающие сокеты внутри сетевого namespace контейнера (видно, что реально слушает приложение). Вторая цепляет strace к PID 1 контейнера, чтобы увидеть системные вызовы упавшего/висящего процесса. nsenter docker - это твой запасной парашют, когда штатный API не помогает: он лезет в контейнер на уровне ядра, в обход docker daemon.

Просмотр файлов контейнера прямо с хоста - ещё один трюк без всякого exec. Ядро даёт корень файловой системы любого процесса через /proc:

Код: Выделить всё

sudo ls -la /proc/34567/root/
sudo cat /proc/34567/root/app/config.yaml
/proc/PID/root - это вход в mount-namespace процесса через файловую систему. Удобно вытащить конфиг или лог из контейнера, у которого нет даже cat. Заметь: это работает на Linux-хосте. На Docker Desktop (Mac/Windows) контейнеры живут внутри VM, поэтому /proc/PID/root и nsenter с твоего ноутбука напрямую не сработают - нужно либо зайти в саму VM, либо использовать docker debug (о нём ниже).

docker debug и эфемерные debug-контейнеры

Подход "тащи инструменты хоста через nsenter" мощный, но требует доступа к хосту и root. Появились более цивилизованные способы.

docker debug (в Docker Desktop, начиная с 4.27) - команда, которая side-загружает в целевой контейнер полноценное окружение с набором инструментов, не меняя сам образ. Ты получаешь shell и привычные утилиты даже в distroless:

Код: Выделить всё

docker debug api
Внутри доступны ls, cat, ps, curl, vim - всё временно, и при выходе образ остаётся нетронутым. Это закрывает кейс distroless без возни с VM и nsenter. Минус - это фича Docker Desktop (и подписочного Docker), на голом Docker Engine на сервере её нет, там твой путь - nsenter или эфемерный контейнер.

Эфемерный debug-контейнер - универсальный приём, работающий на любом движке. Идея: поднять отдельный контейнер с жирным toolset (busybox, nicolaka/netshoot) и подсунуть ему namespace целевого контейнера. Тогда инструменты бегут в окружении жертвы, хотя сами лежат в другом образе.

Код: Выделить всё

docker run -it --rm \
  --pid container:api \
  --network container:api \
  --cap-add SYS_PTRACE \
  nicolaka/netshoot
Разбор:
  • --pid container:api - разделить pid-namespace с контейнером api. Сделаешь ps внутри netshoot - увидишь процессы api, включая его PID 1.
  • --network container:api - тот же сетевой namespace: tcpdump, dig, curl localhost попадают точно в сеть api. netshoot для того и собран - в нём есть всё для сетевой диагностики.
  • --cap-add SYS_PTRACE - даёт право трассировать чужие процессы (strace, gdb).
При желании добавь --volumes-from api, чтобы дотянуться до томов, или просто читай файлы через /proc/<pid>/root, раз pid-namespace общий. Это тот же принцип, что в Kubernetes реализован как ephemeral containers (kubectl debug) - там debug-контейнер втыкается в Pod и делит namespace с целевым. Механика одна, просто обёртки разные.

Когда контейнер уже упал: exit code, логи, cp, diff

Половина отладки - это не живой контейнер, а труп. Контейнер вышел, и надо понять почему. Первым делом - код выхода и причина:

Код: Выделить всё

docker inspect api --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'
# 137 true
Как читать exit code:
  • 0 - штатный выход, всё ок.
  • 137 = 128 + 9 - процесс убит сигналом SIGKILL (9). Чаще всего это OOM-killer (смотри поле OOMKilled: true) или docker stop, не дождавшийся graceful-остановки. 137 без OOM - значит превысил время на остановку и его добили.
  • 143 = 128 + 15 - SIGTERM, корректная остановка по сигналу.
  • 139 = 128 + 11 - SIGSEGV, segfault, баг в коде/библиотеке.
  • 125 - ошибка самого docker run (например, неверный флаг). 126 - команда найдена, но не исполняемая. 127 - команда не найдена (типичная опечатка в ENTRYPOINT).
Дальше - логи, в том числе у остановленного контейнера (пока его не удалили docker rm):

Код: Выделить всё

docker logs --tail 100 --timestamps api
docker logs --since 5m api
--timestamps добавит время к каждой строке (видно, что было прямо перед падением), --tail и --since режут шум. Помни: docker logs показывает только то, что приложение писало в stdout/stderr. Если оно пишет в файл внутри контейнера - в логах пусто, доставай файл.

Достать файлы из мёртвого или живого контейнера - docker cp, ему не нужен ни shell, ни запущенный контейнер:

Код: Выделить всё

docker cp api:/app/logs/error.log ./error.log
docker cp api:/etc/nginx/nginx.conf ./nginx.conf
Работает в обе стороны: docker cp ./fix.conf api:/etc/nginx/ закинет файл внутрь. Идеально для distroless - копируешь конфиг наружу, читаешь спокойно у себя.

docker diff показывает, что контейнер наизменял в файловой системе относительно образа:

Код: Выделить всё

docker diff api
# C /app
# A /app/cache/session.lock
# C /tmp
# A /tmp/core.1234
# D /etc/ld.so.cache
A - добавлено (Added), C - изменено (Changed), D - удалено (Deleted). Так ловят "приложение пишет в неожиданное место", левые core-dump (вот он, /tmp/core.1234 после segfault), раздувание writable-слоя. Связка docker diff + docker cp вытащить артефакт - быстрый разбор инцидента без входа внутрь.

Связка с диагностикой Linux и типичные грабли

Контейнер - это процесс, поэтому работают все привычные инструменты, если зайти в нужный namespace. Сетевую проблему гонишь через netshoot (--network container:): ss -tlnp - кто слушает, ip a - какие адреса, tcpdump -i any - реальный трафик, dig - резолвинг. Зависший процесс - strace -p 1 (на каком syscall встал), cat /proc/1/status (состояние, память, треды). Память и лимиты - в cgroup; через /proc/PID/root и хостовые /sys/fs/cgroup видно реальные limit и usage, которые и привели к 137.

Грабли, на которых теряют часы:
  • Ctrl+C в attach роняет контейнер. Всегда выходи через Ctrl+P, Ctrl+Q. Лучше вообще не attach, а exec.
  • exec модифицирует контейнер, но это не сохраняется в образе. Поставил пакет через exec - после рестарта контейнера он пропадёт. exec лечит здесь и сейчас, фикс правь в Dockerfile.
  • Удалил контейнер docker rm - потерял логи и diff. Сначала сними logs/inspect/diff/cp, потом удаляй. С --rm контейнер исчезает мгновенно при выходе - для отладки запускай без --rm.
  • strace без SYS_PTRACE. Получишь "Operation not permitted". Добавь --cap-add SYS_PTRACE (или --privileged в exec).
  • nsenter без sudo / не на том хосте. В кластере процесс живёт на конкретной ноде - PID есть только там. На Docker Desktop процессы внутри VM, с ноутбука их PID не виден.
  • Перепутал PID хоста и PID контейнера. docker inspect .State.Pid - это хостовый PID для nsenter и /proc. Внутри контейнера тот же процесс - PID 1.
Современный движок (Docker на базе containerd/runc) ничего из этого не ломает: namespace и cgroup создаёт runc по стандарту OCI, а exec/attach - это вызовы к daemon, который дёргает containerd. nsenter и /proc работают на уровне ядра и от выбора рантайма не зависят, поэтому те же приёмы переносятся на nerdctl, Podman и любой OCI-совместимый стек.

Мини-лаба: отладить контейнер без shell

Повтори руками, это 10 минут и даёт мышечную память.
  • Подними distroless-приложение (или просто docker run -d --name api --memory 64m nginx). Проверь docker exec -it api sh - убедись, что для distroless shell нет.
  • Узнай хостовый PID: docker inspect --format '{{.State.Pid}}' api. Войди через sudo nsenter -t PID -n ss -tlnp и посмотри слушающие порты.
  • Прочитай конфиг контейнера прямо с хоста: sudo ls /proc/PID/root/ и sudo cat /proc/PID/root/etc/hostname.
  • Подними рядом netshoot с общими namespace: docker run -it --rm --pid container:api --network container:api nicolaka/netshoot, внутри сделай ps -ef (увидишь PID 1 от api) и curl -s localhost.
  • Достань файл наружу: docker cp api:/etc/nginx/nginx.conf . Затем docker diff api - посмотри изменённые пути.
  • Убей приложение (docker kill api), потом docker inspect api --format '{{.State.ExitCode}} {{.State.OOMKilled}}' и docker logs --tail 20 api. Сопоставь код выхода с сигналом.
Контрольные вопросы
  • Чем docker exec принципиально отличается от docker attach по тому, какой процесс ты трогаешь, и почему Ctrl+C в attach опасен?
  • В образе нет ни sh, ни ls. Опиши два разных способа всё равно заглянуть внутрь работающего контейнера и прочитать его файлы.
  • Контейнер вышел с кодом 137 и OOMKilled: false. Что это значит и где искать причину?
  • Зачем при запуске эфемерного debug-контейнера нужны флаги --pid container: и --network container:, и что без них сломается?
Итог

Отладка контейнера - это работа с процессом и его namespace, а не с чёрным ящиком. docker exec запускает новый процесс рядом и решает большинство задач; docker attach цепляется к самому PID 1 и нужен редко (и осторожно). Когда в образе нет shell, ты входишь в namespace инструментами хоста через nsenter docker, читаешь файлы через /proc/PID/root, поднимаешь эфемерный debug-контейнер с общими --pid/--network или используешь docker debug в Desktop. А если контейнер уже мёртв - exit code, OOMKilled, docker logs, docker cp и docker diff расскажут историю падения без всякого входа внутрь.
👍4 ❤️2 🔥 😄 🤔1
Аватара пользователя
pasha3
Сообщения: 1
Зарегистрирован: 14 май 2026, 02:14

Re: Отладка контейнеров: exec, attach, nsenter, debug

Сообщение pasha3 »

Про Ctrl+P Ctrl+Q вообще не знал, всегда выходил из attach через Ctrl+C и удивлялся почему контейнер падает. Спасибо, теперь понятно.
👍1 ❤️1 🔥 😄 🤔
Аватара пользователя
python_maker
Сообщения: 1
Зарегистрирован: 25 май 2026, 10:04

Re: Отладка контейнеров: exec, attach, nsenter, debug

Сообщение python_maker »

А можно ли через nsenter зайти в контейнер на ноде кубера, или там только kubectl debug? У нас как раз distroless образы и приложение виснет иногда.
👍 ❤️1 🔥1 😄 🤔
Ответить
← Предыдущая глава
Жизненный цикл контейнера и политики перезапуска
Следующая глава →
Сети Docker глубже: bridge, overlay, macvlan и iptables

Все главы курса «Docker: контейнеризация от основ до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: docker network как связать контейнеры между собой по имени

Вернуться в «Docker с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость