Главная/Observability и диагностика/Урок

13.5. Глубокая диагностика

Цели

После этого материала вы сможете:

  • читать логи daemon и отличать его проблемы от проблем приложения;
  • войти в namespaces работающего container без изменения образа;
  • провести диагностику container'а, в образе которого нет shell;
  • применить strace и tcpdump, зная требуемые привилегии;
  • найти файловую систему container'а на host и прочитать нужный файл;
  • выбрать инструмент под задачу, а не перебирать все подряд.

Предварительные знания

Ключевые термины

ТерминОбъяснение
nsenterВход в namespaces процесса
MergedDirКаталог на host, где видна файловая система container'а
distrolessОбраз без shell и утилит
sidecarContainer, подключённый к namespaces другого
journalctlЧтение журнала systemd

Теория

Логи daemon

Проблемы делятся на две категории, и путать их дорого:

СимптомГде искать
Приложение отвечает ошибкойdocker logs
Container не запускается вовсеЛоги daemon
Образ не скачиваетсяЛоги daemon
Volume не монтируетсяЛоги daemon
Сеть не создаётсяЛоги daemon
bash
sudo journalctl -u docker.service --since '10 min ago'
sudo journalctl -u docker.service -f
sudo journalctl -u docker.service -p err --since today

Признак того, что смотреть нужно именно сюда: docker run завершается ошибкой, а docker logs для этого container'а пуст или container вообще не создан.

Четыре способа попасть внутрь

СпособТребует shell в образеТребует root на hostЧто даёт
docker execДаНетОбычный вход
Sidecar через --pid/--networkНетНетИнструменты соседнего образа
nsenterНетДаПолный доступ ко всем namespaces
Чтение MergedDirНетДаФайлы без входа вовсе

Порядок предпочтения: сначала docker exec, при отсутствии shell — sidecar, при необходимости системных инструментов — nsenter.

Sidecar: инструменты без изменения образа

bash
docker run --rm -it \
    --network container:ЦЕЛЬ \
    --pid container:ЦЕЛЬ \
    --cap-add SYS_PTRACE \
    nicolaka/netshoot sh
ФлагЧто разделяет
--network container:XСетевой namespace: те же интерфейсы, порты, DNS
--pid container:XPID namespace: видны процессы цели
--ipc container:XРазделяемая память
--cap-add SYS_PTRACEНужен для strace, py-spy, gdb

Чего sidecar не даёт: файловую систему цели. Она в другом mount namespace. Файлы видны через /proc/PID/root/ — при наличии SYS_PTRACE и совпадении пользователя.

Это главное отличие от nsenter, который входит и в mount namespace тоже.

nsenter

bash
PID=$(docker inspect ЦЕЛЬ --format '{{.State.Pid}}')
sudo nsenter -t "$PID" -m -u -i -n -p -- sh
ФлагNamespace
-mmount — файловая система container'а
-uUTS — имя хоста
-iIPC
-nnetwork
-pPID

Существенная тонкость: nsenter запускает программу с host, но в namespaces цели. При флаге -m файловая система становится файловой системой container'а — а значит, запускаемая программа должна существовать в container'е.

Практическое решение: входить без -m, если нужны инструменты host:

bash
sudo nsenter -t "$PID" -n -- ss -tlnp

Здесь ss берётся с host, а сетевой стек — из container'а. Это самый полезный вариант для диагностики сети образов без утилит.

Диагностика образа без shell

Distroless-образы не содержат sh, ls, cat. docker exec в них невозможен.

Порядок действий:

ЗадачаРешение
Посмотреть процессыdocker top или sidecar с --pid
Посмотреть сетьnsenter -n с host или sidecar с --network
Прочитать файлdocker cp ЦЕЛЬ:/путь - или MergedDir
Посмотреть окружениеdocker inspect
Посмотреть открытые дескрипторыsudo ls /proc/PID/fd

Отдельно стоит docker cp — он работает без shell в образе, потому что выполняется daemon'ом, а не внутри container'а. Это самый простой способ достать файл из distroless-образа.

Файловая система container'а на host

bash
docker inspect ЦЕЛЬ --format '{{.GraphDriver.Data.MergedDir}}'

Путь вида /var/lib/docker/overlay2/<хеш>/merged — это корень файловой системы container'а, видимый с host.

КаталогСодержимое
MergedDirИтоговое представление: образ плюс изменения
UpperDirТолько изменения, сделанные container'ом
LowerDirСлои образа

UpperDir полезен отдельно: он показывает ровно то, что container записал (урок 3.2). Если приложение неожиданно пишет на диск — здесь видно, что именно.

Предупреждение: запись в эти каталоги напрямую не поддерживается и может нарушить состояние container'а. Читать — можно.

strace и tcpdump

ИнструментТребуетДаёт
straceSYS_PTRACEСистемные вызовы: где процесс завис, что не находит
tcpdumpNET_ADMIN или NET_RAWТрафик: что уходит и приходит
py-spySYS_PTRACEСтек Python-процесса без его остановки

Все три запускают из sidecar, а не устанавливают в образ приложения (урок 12.4).

Привилегии выдают временно, только диагностическому container'у, и убирают после.

Наиболее частые применения strace:

bash
strace -p PID -f -e trace=openat        # что процесс пытается открыть
strace -p PID -f -e trace=network       # сетевые вызовы
strace -p PID -f -c                     # сводка: где время

Первый вариант отвечает на вопрос «почему не находит файл», который иначе решается перебором.


Внутренний механизм

Почему docker exec требует shell, а docker cp — нет

docker exec запускает новый процесс внутри namespaces container'а. Программу для запуска ищет ядро в файловой системе container'а — если sh там нет, запускать нечего.

docker cp выполняется daemon'ом: он читает файловую систему container'а через свой mount namespace и копирует байты. Процесс внутри container'а при этом не создаётся.

Отсюда правило: всё, что делает daemon, работает в distroless; всё, что требует процесса внутри, — нет.

Почему sidecar не видит файлы цели

Флаги --network, --pid, --ipc разделяют соответствующие namespaces. Флага для mount namespace нет — Docker его не предоставляет.

Обходной путь через /proc/PID/root/ работает потому, что ядро предоставляет доступ к корню другого процесса через procfs. Требуется SYS_PTRACE и совпадение владельца.


Команды и примеры

Логи daemon: когда docker logs бесполезен

bash
mkdir -p /tmp/deep && cd /tmp/deep

echo "═══ ошибка, не попадающая в docker logs ═══"
docker run --name broken -v /nonexistent/path:/data:ro alpine:3.21 true 2>&1 \
    | head -3 | sed 's/^/  ошибка команды: /'
echo "  docker logs для этого container'а:"
docker logs broken 2>&1 | head -2 | sed 's/^/    /' || echo "    (container не создан)"
docker rm -f broken > /dev/null 2>&1

echo "═══ где искать такие ошибки ═══"
if sudo journalctl -u docker.service --since '2 min ago' --no-pager -p warning 2>/dev/null \
   | tail -5 | grep -q .; then
    sudo journalctl -u docker.service --since '2 min ago' --no-pager -p warning 2>/dev/null \
        | tail -5 | sed 's/^/  /'
else
    cat <<'TXT'
  (journalctl требует прав root или systemd недоступен)

  Команды для чтения логов daemon:
    sudo journalctl -u docker.service --since '10 min ago'
    sudo journalctl -u docker.service -f
    sudo journalctl -u docker.service -p err --since today
TXT
fi

echo "═══ как различать ═══"
cat <<'TXT'
  docker logs ПУСТ или container не создан → смотреть логи daemon
  docker logs содержит трассировку Python  → проблема в приложении

  Типичные проблемы daemon:
    не смонтирован volume, не скачался образ, конфликт портов,
    нет места на диске, ошибка сетевого драйвера
TXT

Ожидаемый вывод:

text
═══ ошибка, не попадающая в docker logs ═══
  ошибка команды: docker: Error response from daemon: error while creating mount source path
  ошибка команды: '/nonexistent/path': mkdir /nonexistent: read-only file system
  docker logs для этого container'а:
    (container не создан)
═══ где искать такие ошибки ═══
  (journalctl требует прав root или systemd недоступен)

  Команды для чтения логов daemon:
    sudo journalctl -u docker.service --since '10 min ago'
    sudo journalctl -u docker.service -f
    sudo journalctl -u docker.service -p err --since today
═══ как различать ═══
  docker logs ПУСТ или container не создан → смотреть логи daemon
  docker logs содержит трассировку Python  → проблема в приложении

  Типичные проблемы daemon:
    не смонтирован volume, не скачался образ, конфликт портов,
    нет места на диске, ошибка сетевого драйвера

Ключевой признак — container не создан вовсе. Искать его логи бессмысленно: их нет.

Sidecar: диагностика сети без утилит в образе

bash
cd /tmp/deep
echo "═══ запускаем сервис на минимальном образе ═══"
cat > srv.py <<'PY'
import http.server, os, threading, time
class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200); self.end_headers(); self.wfile.write(b"ok")
    def log_message(self, *a): pass
print(f"слушаю 8000, pid={os.getpid()}", flush=True)
threading.Thread(
    target=http.server.ThreadingHTTPServer(("0.0.0.0", 8000), H).serve_forever,
    daemon=True).start()
while True: time.sleep(1)
PY
docker run -d --name svc -e PYTHONUNBUFFERED=1 \
    -v "$PWD/srv.py:/s.py:ro" python:3.13-slim python /s.py > /dev/null
sleep 3

echo "═══ в образе нет сетевых утилит ═══"
docker exec svc sh -c 'command -v ss || command -v netstat || echo "  ss и netstat отсутствуют"' \
    2>/dev/null | sed 's/^/  /'

echo "═══ sidecar с общим сетевым namespace ═══"
docker run --rm --network container:svc python:3.13-slim python -c "
import socket, json

# Тот же сетевой стек, что у цели
info = {}
s = socket.socket()
try:
    s.connect(('127.0.0.1', 8000))
    info['порт 8000 на localhost'] = 'открыт'
except OSError as e:
    info['порт 8000 на localhost'] = f'закрыт: {e.strerror}'
finally:
    s.close()

info['имя хоста'] = socket.gethostname()
try:
    info['разрешение svc'] = socket.gethostbyname('svc')
except OSError:
    info['разрешение svc'] = 'не разрешается'

for k, v in info.items():
    print(f'  {k:<26} {v}')
" 2>/dev/null

echo "═══ почему это работает ═══"
cat <<'TXT'
  --network container:svc помещает sidecar в ТОТ ЖЕ сетевой
  namespace. Его localhost — это localhost цели, его интерфейсы —
  интерфейсы цели.

  Значит, любой сетевой инструмент из образа sidecar работает
  так, как если бы он был установлен в целевой образ.

  Образ с полным набором утилит:
    docker run --rm -it --network container:svc nicolaka/netshoot
    (внутри: ss, tcpdump, dig, curl, ping, iperf, nmap)
TXT

Ожидаемый вывод:

text
═══ запускаем сервис на минимальном образе ═══
═══ в образе нет сетевых утилит ═══
    ss и netstat отсутствуют
═══ sidecar с общим сетевым namespace ═══
  порт 8000 на localhost      открыт
  имя хоста                   3f8a2b1c4d5e
  разрешение svc              не разрешается
═══ почему это работает ═══
  --network container:svc помещает sidecar в ТОТ ЖЕ сетевой
  namespace. Его localhost — это localhost цели, его интерфейсы —
  интерфейсы цели.

  Значит, любой сетевой инструмент из образа sidecar работает
  так, как если бы он был установлен в целевой образ.

  Образ с полным набором утилит:
    docker run --rm -it --network container:svc nicolaka/netshoot
    (внутри: ss, tcpdump, dig, curl, ping, iperf, nmap)

«Имя хоста» совпадает с идентификатором целевого container'а — подтверждение того, что namespace действительно общий.

«Разрешение svc: не разрешается» ожидаемо: в сети bridge по умолчанию встроенный DNS не работает по именам (урок 8.4).

nsenter: инструменты host в namespace container'а

bash
cd /tmp/deep
pid="$(docker inspect svc --format '{{.State.Pid}}')"
echo "═══ PID цели на host: $pid ═══"

echo "═══ сетевой namespace цели, утилита с host ═══"
if sudo test -d "/proc/$pid/ns" 2>/dev/null; then
    sudo nsenter -t "$pid" -n -- ss -tlnp 2>/dev/null | head -5 | sed 's/^/  /' \
        || sudo nsenter -t "$pid" -n -- netstat -tlnp 2>/dev/null | head -5 | sed 's/^/  /' \
        || echo "  ss и netstat отсутствуют и на host"
else
    cat <<'TXT'
  (nsenter требует прав root)

  При наличии доступа:
    PID=$(docker inspect svc --format '{{.State.Pid}}')
    sudo nsenter -t "$PID" -n -- ss -tlnp

  Вывод был бы:
    State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
    LISTEN  0       5       0.0.0.0:8000        0.0.0.0:*          users:(("python3",pid=1,fd=3))

  Ключевое: ss берётся С HOST, сетевой стек — ИЗ CONTAINER'а.
  Устанавливать что-либо в образ не нужно.
TXT
fi

echo "═══ различие флагов ═══"
cat <<'TXT'
  БЕЗ -m (только сетевой namespace):
    sudo nsenter -t PID -n -- ss -tlnp
    → программа берётся с host, сеть из container'а
    → работает для образов без утилит

  С -m (включая mount namespace):
    sudo nsenter -t PID -m -n -- sh
    → файловая система становится файловой системой container'а
    → sh должен существовать В CONTAINER'е; в distroless не сработает

  Практическое правило: для диагностики сети используйте
  nsenter БЕЗ -m — так доступны утилиты host.
TXT

Ожидаемый вывод:

text
═══ PID цели на host: 52841 ═══
═══ сетевой namespace цели, утилита с host ═══
  (nsenter требует прав root)

  При наличии доступа:
    PID=$(docker inspect svc --format '{{.State.Pid}}')
    sudo nsenter -t "$PID" -n -- ss -tlnp

  Вывод был бы:
    State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
    LISTEN  0       5       0.0.0.0:8000        0.0.0.0:*          users:(("python3",pid=1,fd=3))

  Ключевое: ss берётся С HOST, сетевой стек — ИЗ CONTAINER'а.
  Устанавливать что-либо в образ не нужно.
═══ различие флагов ═══
  БЕЗ -m (только сетевой namespace):
    sudo nsenter -t PID -n -- ss -tlnp
    → программа берётся с host, сеть из container'а
    → работает для образов без утилит

  С -m (включая mount namespace):
    sudo nsenter -t PID -m -n -- sh
    → файловая система становится файловой системой container'а
    → sh должен существовать В CONTAINER'е; в distroless не сработает

  Практическое правило: для диагностики сети используйте
  nsenter БЕЗ -m — так доступны утилиты host.
TXT

Различие флагов — то, на чём чаще всего спотыкаются. nsenter -m в distroless-образ не войдёт: там нет sh, который он пытается запустить.

Container без shell

bash
cd /tmp/deep
echo "═══ собираем образ без shell ═══"
cat > Dockerfile.noshell <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS build
COPY srv.py /srv.py

FROM gcr.io/distroless/python3-debian12
COPY --from=build /srv.py /srv.py
ENV PYTHONUNBUFFERED=1
CMD ["/srv.py"]
EOF

if docker build -f Dockerfile.noshell -t noshell:1 . > build.log 2>&1; then
    docker run -d --name ns-svc noshell:1 > /dev/null 2>&1
    sleep 3
    built=1
else
    echo "  (образ distroless недоступен — используем эквивалент)"
    docker run -d --name ns-svc -e PYTHONUNBUFFERED=1 \
        -v "$PWD/srv.py:/srv.py:ro" --entrypoint python \
        python:3.13-slim /srv.py > /dev/null 2>&1
    sleep 3
    built=0
fi

echo "═══ что НЕ работает без shell ═══"
docker exec ns-svc sh -c 'echo привет' 2>&1 | head -1 | sed 's/^/  docker exec sh: /'
docker exec ns-svc ls / 2>&1 | head -1 | sed 's/^/  docker exec ls:  /'

echo "═══ что работает ═══"
printf '  docker top:        '
docker top ns-svc 2>/dev/null | tail -1 | awk '{print $NF}'
printf '  docker inspect:    '
docker inspect ns-svc --format '{{json .Config.Env}}' 2>/dev/null | cut -c1-46
printf '  docker cp:         '
docker cp ns-svc:/srv.py - 2>/dev/null | tar -xO 2>/dev/null | head -1 | cut -c1-40 \
    || echo "файл не извлечён"
printf '  docker logs:       '
docker logs ns-svc 2>&1 | head -1

echo "═══ сеть через sidecar ═══"
docker run --rm --network container:ns-svc python:3.13-slim python -c "
import socket
s = socket.socket()
try:
    s.connect(('127.0.0.1', 8000))
    print('  порт 8000: открыт')
except OSError as e:
    print(f'  порт 8000: {e.strerror}')
finally:
    s.close()
" 2>/dev/null

echo "═══ порядок диагностики образа без shell ═══"
cat <<'TXT'
  1. docker logs              вывод приложения
  2. docker inspect           конфигурация, состояние, здоровье
  3. docker top               процессы
  4. docker cp ЦЕЛЬ:/путь -   любой файл, БЕЗ shell в образе
  5. sidecar --network        диагностика сети
  6. sidecar --pid + SYS_PTRACE  стек процесса через py-spy
  7. sudo nsenter -t PID -n   утилиты host в сети container'а
  8. MergedDir на host        файловая система целиком

  Пункт 4 работает потому, что копирование выполняет DAEMON,
  а не процесс внутри container'а.
TXT
docker rm -f ns-svc > /dev/null 2>&1

Ожидаемый вывод:

text
═══ собираем образ без shell ═══
═══ что НЕ работает без shell ═══
  docker exec sh: OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
  docker exec ls:  OCI runtime exec failed: exec failed: unable to start container process: exec: "ls": executable file not found in $PATH
═══ что работает ═══
  docker top:        /srv.py
  docker inspect:    ["PYTHONUNBUFFERED=1","PATH=/usr/bin:/bin
  docker cp:         import http.server, os, threading, time
  docker logs:       слушаю 8000, pid=1
═══ сеть через sidecar ═══
  порт 8000: открыт
═══ порядок диагностики образа без shell ═══
  1. docker logs              вывод приложения
  2. docker inspect           конфигурация, состояние, здоровье
  3. docker top               процессы
  4. docker cp ЦЕЛЬ:/путь -   любой файл, БЕЗ shell в образе
  5. sidecar --network        диагностика сети
  6. sidecar --pid + SYS_PTRACE  стек процесса через py-spy
  7. sudo nsenter -t PID -n   утилиты host в сети container'а
  8. MergedDir на host        файловая система целиком

  Пункт 4 работает потому, что копирование выполняет DAEMON,
  а не процесс внутри container'а.

docker cp извлёк содержимое файла из образа, в котором нет ни sh, ни cat. Это самый недооценённый инструмент в списке.

Файловая система container'а на host

bash
cd /tmp/deep
echo "═══ пути к файловой системе ═══"
docker inspect svc --format 'MergedDir: {{.GraphDriver.Data.MergedDir}}' 2>/dev/null | sed 's/^/  /'
docker inspect svc --format 'UpperDir:  {{.GraphDriver.Data.UpperDir}}' 2>/dev/null | sed 's/^/  /'

echo "═══ что записал container ═══"
docker exec svc sh -c 'echo "данные приложения" > /tmp/written.txt; mkdir -p /var/cache/app; echo x > /var/cache/app/f' 2>/dev/null
upper="$(docker inspect svc --format '{{.GraphDriver.Data.UpperDir}}' 2>/dev/null)"
if sudo test -d "$upper" 2>/dev/null; then
    echo "  содержимое UpperDir (только изменения):"
    sudo find "$upper" -type f 2>/dev/null | head -8 | sed "s|$upper|  ...|" | sed 's/^/    /'
else
    cat <<'TXT'
  (чтение требует прав root)

  При наличии доступа UpperDir содержал бы РОВНО то,
  что container записал сверх образа:
    .../tmp/written.txt
    .../var/cache/app/f

  Это самый прямой ответ на вопрос «что пишет приложение
  в свою файловую систему»: не размер, а конкретные файлы.
TXT
fi

echo "═══ три каталога overlay2 ═══"
cat <<'TXT'
  LowerDir   слои образа, только чтение
  UpperDir   изменения container'а — ТОЛЬКО ТО, ЧТО ОН ЗАПИСАЛ
  MergedDir  итоговое представление: образ плюс изменения

  Для диагностики полезнее всего UpperDir: он отвечает
  на вопрос «что именно приложение пишет на диск».

  ПРЕДУПРЕЖДЕНИЕ: запись в эти каталоги напрямую не поддерживается
  и может нарушить состояние container'а. Только чтение.
TXT

Ожидаемый вывод:

text
═══ пути к файловой системе ═══
  MergedDir: /var/lib/docker/overlay2/a3f2.../merged
  UpperDir:  /var/lib/docker/overlay2/a3f2.../diff
═══ что записал container ═══
  (чтение требует прав root)

  При наличии доступа UpperDir содержал бы РОВНО то,
  что container записал сверх образа:
    .../tmp/written.txt
    .../var/cache/app/f

  Это самый прямой ответ на вопрос «что пишет приложение
  в свою файловую систему»: не размер, а конкретные файлы.
═══ три каталога overlay2 ═══
  LowerDir   слои образа, только чтение
  UpperDir   изменения container'а — ТОЛЬКО ТО, ЧТО ОН ЗАПИСАЛ
  MergedDir  итоговое представление: образ плюс изменения

  Для диагностики полезнее всего UpperDir: он отвечает
  на вопрос «что именно приложение пишет на диск».

  ПРЕДУПРЕЖДЕНИЕ: запись в эти каталоги напрямую не поддерживается
  и может нарушить состояние container'а. Только чтение.

Есть и способ без прав root, дающий тот же ответ:

bash
cd /tmp/deep
echo "═══ docker diff: изменения без доступа к host ═══"
docker diff svc 2>/dev/null | head -10 | sed 's/^/  /'

echo "═══ обозначения ═══"
cat <<'TXT'
  A  добавлен
  C  изменён
  D  удалён

  docker diff даёт тот же ответ, что и просмотр UpperDir,
  но НЕ требует прав root. Для быстрой проверки
  «что пишет приложение» это лучший инструмент.

  Ограничение: показывает пути, но не размеры.
  Размер writable layer целиком — docker ps -s.
TXT

Ожидаемый вывод:

text
═══ docker diff: изменения без доступа к host ═══
  C /tmp
  A /tmp/written.txt
  C /var
  C /var/cache
  A /var/cache/app
  A /var/cache/app/f
═══ обозначения ═══
  A  добавлен
  C  изменён
  D  удалён

  docker diff даёт тот же ответ, что и просмотр UpperDir,
  но НЕ требует прав root. Для быстрой проверки
  «что пишет приложение» это лучший инструмент.

  Ограничение: показывает пути, но не размеры.
  Размер writable layer целиком — docker ps -s.

docker diff — недооценённая команда: она отвечает на тот же вопрос, что чтение UpperDir, но доступна без прав root.

strace: почему процесс не находит файл

bash
cd /tmp/deep
cat > seeker.py <<'PY'
"""Ищет конфигурацию в нескольких местах и сообщает результат."""
import os
import time
from pathlib import Path

CANDIDATES = [
    "/etc/app/config.yaml",
    "/config/app.yaml",
    "/app/config.yaml",
    os.path.expanduser("~/.app/config.yaml"),
]

while True:
    found = None
    for path in CANDIDATES:
        if Path(path).exists():
            found = path
            break
    print(f"конфигурация: {found or 'НЕ НАЙДЕНА'}", flush=True)
    time.sleep(2)
PY

docker run -d --name seeker -e PYTHONUNBUFFERED=1 \
    -v "$PWD/seeker.py:/s.py:ro" python:3.13-slim python /s.py > /dev/null
sleep 3
echo "═══ что говорит приложение ═══"
docker logs seeker 2>&1 | tail -1 | sed 's/^/  /'

echo "═══ где именно оно искало — через strace ═══"
if docker run --rm --pid container:seeker --cap-add SYS_PTRACE \
    --security-opt seccomp=unconfined alpine:3.21 sh -c '
    apk add --no-cache strace > /dev/null 2>&1
    timeout 4 strace -p 1 -f -e trace=openat,newfstatat,statx 2>&1 \
        | grep -oE "\"[^\"]*config[^\"]*\"" | sort -u | head -6
' 2>/dev/null | grep -q .; then
    docker run --rm --pid container:seeker --cap-add SYS_PTRACE \
        --security-opt seccomp=unconfined alpine:3.21 sh -c '
        apk add --no-cache strace > /dev/null 2>&1
        timeout 4 strace -p 1 -f -e trace=openat,newfstatat,statx 2>&1 \
            | grep -oE "\"[^\"]*config[^\"]*\"" | sort -u | head -6
    ' 2>/dev/null | sed 's/^/  /'
else
    cat <<'TXT'
  (strace недоступен: нужен SYS_PTRACE и разрешение ядра ptrace)

  При наличии доступа было бы видно:
    "/etc/app/config.yaml"
    "/config/app.yaml"
    "/app/config.yaml"
    "/root/.app/config.yaml"

  Это ТОЧНЫЙ список путей, которые проверило приложение,
  вместе с кодом возврата каждой попытки (ENOENT).

  Без strace этот список получают чтением исходного кода —
  что невозможно для чужого приложения.
TXT
fi

echo "═══ полезные варианты strace ═══"
cat <<'TXT'
  strace -p PID -f -e trace=openat     что пытается открыть
  strace -p PID -f -e trace=network    сетевые вызовы
  strace -p PID -f -c                  сводка: где время
  strace -p PID -f -e trace=%file      все файловые операции

  Запускать из sidecar:
    docker run --rm --pid container:ЦЕЛЬ --cap-add SYS_PTRACE \
        nicolaka/netshoot strace -p 1 -f -e trace=openat

  ВАЖНО: SYS_PTRACE даёт чтение памяти всех процессов
  в общем PID namespace. Выдавать временно ([урок 12.4]).
TXT
docker rm -f seeker svc > /dev/null 2>&1
cd /tmp && rm -rf /tmp/deep

Ожидаемый вывод:

text
═══ что говорит приложение ═══
  конфигурация: НЕ НАЙДЕНА
═══ где именно оно искало — через strace ═══
  (strace недоступен: нужен SYS_PTRACE и разрешение ядра ptrace)

  При наличии доступа было бы видно:
    "/etc/app/config.yaml"
    "/config/app.yaml"
    "/app/config.yaml"
    "/root/.app/config.yaml"

  Это ТОЧНЫЙ список путей, которые проверило приложение,
  вместе с кодом возврата каждой попытки (ENOENT).

  Без strace этот список получают чтением исходного кода —
  что невозможно для чужого приложения.
═══ полезные варианты strace ═══
  strace -p PID -f -e trace=openat     что пытается открыть
  strace -p PID -f -e trace=network    сетевые вызовы
  strace -p PID -f -c                  сводка: где время
  strace -p PID -f -e trace=%file      все файловые операции

  Запускать из sidecar:
    docker run --rm --pid container:ЦЕЛЬ --cap-add SYS_PTRACE \
        nicolaka/netshoot strace -p 1 -f -e trace=openat

  ВАЖНО: SYS_PTRACE даёт чтение памяти всех процессов
  в общем PID namespace. Выдавать временно ([урок 12.4]).

Сообщение приложения — «НЕ НАЙДЕНА». Оно не говорит, где искали. strace отвечает на этот вопрос точно, включая пути, о которых не сказано в документации.


Практическое упражнение

Задание. Продиагностируйте container, в образе которого нет shell.

Требования:

  1. Собрать образ без shell и убедиться, что docker exec в него невозможен.
  2. Получить из него: логи, конфигурацию, список процессов и содержимое файла.
  3. Проверить сеть цели, не устанавливая ничего в её образ.
  4. Показать, что именно приложение записало в свою файловую систему.
  5. Показать, где приложение искало отсутствующий файл.
  6. Написать инструмент диагностики, работающий и с shell, и без него.

Подсказки

Подсказка 1

docker cp выполняется daemon'ом и не требует процесса внутри container'а — работает в distroless.

Подсказка 2

Для пункта 4 есть два пути: docker diff (без root) и чтение UpperDir (с root). Начните с первого.

Подсказка 3

Инструмент должен определять наличие shell, а не предполагать его. Проверка: docker exec ЦЕЛЬ true.

Решение

Показать решение
bash
mkdir -p /tmp/nolab && cd /tmp/nolab

# ─── Приложение, ищущее отсутствующий файл ────────────────────────────
cat > service.py <<'PY'
"""Сервис, который ищет конфигурацию в нескольких местах и не находит.

Сообщение «не найдена» не содержит списка проверенных путей —
это типичная ситуация, ради которой применяют strace.
"""
from __future__ import annotations

import http.server
import json
import os
import threading
import time
from pathlib import Path

CANDIDATES = [
    "/etc/svc/settings.yaml",
    "/config/settings.yaml",
    "/opt/svc/settings.yaml",
]
PORT = 8000


class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self) -> None:
        found = next((p for p in CANDIDATES if Path(p).exists()), None)
        body = json.dumps({"конфигурация": found, "pid": os.getpid()},
                          ensure_ascii=False).encode()
        self.send_response(200 if found else 503)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args: object) -> None:
        pass


def write_state() -> None:
    """Пишет в файловую систему container'а — то, что найдёт docker diff."""
    Path("/tmp/svc").mkdir(parents=True, exist_ok=True)
    n = 0
    while True:
        n += 1
        Path("/tmp/svc/heartbeat.txt").write_text(f"итерация {n}\n")
        Path("/tmp/svc/state.json").write_text(
            json.dumps({"итераций": n}, ensure_ascii=False))
        time.sleep(2)


if __name__ == "__main__":
    print(f"сервис запущен, pid={os.getpid()}", flush=True)
    found = next((p for p in CANDIDATES if Path(p).exists()), None)
    print(f"конфигурация: {found or 'НЕ НАЙДЕНА'}", flush=True)
    threading.Thread(target=write_state, daemon=True).start()
    server = http.server.ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
    threading.Thread(target=server.serve_forever, daemon=True).start()
    while True:
        # Периодически перечитываем — даёт повторяющиеся вызовы для strace
        for p in CANDIDATES:
            Path(p).exists()
        time.sleep(1)
PY

cat > Dockerfile.distroless <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS build
COPY service.py /service.py

FROM gcr.io/distroless/python3-debian12
COPY --from=build /service.py /service.py
ENV PYTHONUNBUFFERED=1
CMD ["/service.py"]
EOF

# ─── Инструмент диагностики ───────────────────────────────────────────
cat > probe.sh <<'SH'
#!/usr/bin/env bash
# Диагностика container'а: определяет наличие shell и выбирает путь.
set -uo pipefail

TARGET="${1:?укажите container}"
SIDECAR="${SIDECAR_IMAGE:-python:3.13-slim}"

docker inspect "$TARGET" > /dev/null 2>&1 || { echo "  container не найден"; exit 3; }

printf '\n  Диагностика: %s\n\n' "$TARGET"

# 0. Есть ли shell — определяем, а не предполагаем
if docker exec "$TARGET" true > /dev/null 2>&1; then
    HAS_EXEC=1
    if docker exec "$TARGET" sh -c 'exit 0' > /dev/null 2>&1; then
        HAS_SHELL=1
    else
        HAS_SHELL=0
    fi
else
    HAS_EXEC=0
    HAS_SHELL=0
fi
printf '  shell в образе: %s\n\n' \
    "$([ "$HAS_SHELL" = "1" ] && echo "есть" || echo "НЕТ — используем обходные пути")"

# 1. Логи — работают всегда
printf '  [1] Логи (docker logs):\n'
docker logs "$TARGET" 2>&1 | tail -3 | sed 's/^/      /'

# 2. Конфигурация — работает всегда
printf '\n  [2] Конфигурация (docker inspect):\n'
docker inspect "$TARGET" --format \
'      статус={{.State.Status}} код={{.State.ExitCode}} перезапусков={{.RestartCount}}
      команда={{json .Config.Cmd}}
      пользователь={{if .Config.User}}{{.Config.User}}{{else}}root (не задан){{end}}' 2>/dev/null

# 3. Процессы — работают всегда
printf '\n  [3] Процессы (docker top):\n'
docker top "$TARGET" 2>/dev/null | tail -3 | awk '{printf "      %s  %s\n", $2, $NF}'

# 4. Файл — docker cp работает БЕЗ shell
printf '\n  [4] Извлечение файла (docker cp — не требует shell):\n'
if docker cp "$TARGET:/service.py" - 2>/dev/null | tar -xO 2>/dev/null | head -2 \
   | sed 's/^/      /' | grep -q .; then
    :
else
    printf '      файл /service.py не извлечён\n'
fi

# 5. Что записал container — docker diff без root
printf '\n  [5] Записано в файловую систему (docker diff):\n'
diff_out="$(docker diff "$TARGET" 2>/dev/null | grep '^A' | head -6)"
if [ -n "$diff_out" ]; then
    echo "$diff_out" | sed 's/^/      /'
    printf '      всего изменений: %s\n' "$(docker diff "$TARGET" 2>/dev/null | grep -c .)"
else
    printf '      изменений нет\n'
fi

# 6. Сеть — через sidecar, без установки в целевой образ
printf '\n  [6] Сеть (sidecar с общим namespace):\n'
docker run --rm --network "container:$TARGET" "$SIDECAR" python -c "
import socket, json
out = []
for port in (8000, 8080, 5000):
    s = socket.socket(); s.settimeout(1)
    try:
        s.connect(('127.0.0.1', port))
        out.append(f'порт {port}: ОТКРЫТ')
    except OSError:
        pass
    finally:
        s.close()
print('      ' + ('; '.join(out) if out else 'открытых портов из проверенных нет'))
print(f'      имя хоста в namespace: {socket.gethostname()}')
" 2>/dev/null || printf '      sidecar не запустился\n'

# 7. Файловая система на host — только с правами
printf '\n  [7] Файловая система на host:\n'
upper="$(docker inspect "$TARGET" --format '{{.GraphDriver.Data.UpperDir}}' 2>/dev/null)"
if [ -n "$upper" ] && sudo test -d "$upper" 2>/dev/null; then
    printf '      UpperDir: %s\n' "$upper"
    sudo find "$upper" -type f 2>/dev/null | head -4 | sed "s|$upper|      ...|"
else
    printf '      UpperDir: %s\n' "${upper:-неизвестен}"
    printf '      (чтение требует прав root — данные выше получены через docker diff)\n'
fi

printf '\n  Все семь проверок выполнены '
if [ "$HAS_SHELL" = "1" ]; then
    printf 'через обычные средства.\n\n'
else
    printf 'БЕЗ shell в образе.\n\n'
fi
exit 0
SH
chmod +x probe.sh

fail=0
ok()  { printf '  ✓ %s\n' "$1"; }
bad() { printf '  ✗ %s\n' "$1"; fail=1; }

printf '\n═══ Требование 1: образ без shell ═══\n'
if docker build -f Dockerfile.distroless -t nolab:distroless . > build.log 2>&1; then
    docker run -d --name nl-target nolab:distroless > /dev/null
    DISTROLESS=1
    printf '    образ distroless собран\n'
else
    printf '    distroless недоступен (%s)\n' "$(tail -1 build.log | cut -c1-50)"
    printf '    эквивалент: запуск python напрямую, PATH без shell\n'
    docker run -d --name nl-target -e PYTHONUNBUFFERED=1 \
        -v "$PWD/service.py:/service.py:ro" --entrypoint python \
        python:3.13-slim /service.py > /dev/null
    DISTROLESS=0
fi
sleep 5

exec_sh="$(docker exec nl-target sh -c 'echo есть' 2>&1 | head -1)"
printf '    docker exec sh: %s\n' "$(echo "$exec_sh" | cut -c1-62)"
if [ "$DISTROLESS" = "1" ]; then
    case "$exec_sh" in
        *"not found"*|*"executable file"*) ok "shell в образе отсутствует, exec невозможен" ;;
        *) bad "shell неожиданно доступен: $exec_sh" ;;
    esac
else
    ok "distroless недоступен; проверки выполняются на эквиваленте (shell есть)"
fi

printf '\n═══ Требование 2: логи, конфигурация, процессы, файл ═══\n'
printf '    логи:        %s\n' "$(docker logs nl-target 2>&1 | tail -1)"
printf '    конфигурация: %s\n' \
    "$(docker inspect nl-target --format '{{.State.Status}} cmd={{json .Config.Cmd}}')"
printf '    процессы:    %s\n' "$(docker top nl-target 2>/dev/null | tail -1 | awk '{print $NF}')"
file_head="$(docker cp nl-target:/service.py - 2>/dev/null | tar -xO 2>/dev/null | head -1)"
printf '    файл (cp):   %s\n' "${file_head:0:56}"
[ -n "$file_head" ] \
    && ok "четыре факта получены; docker cp извлёк файл без shell" \
    || bad "docker cp не извлёк файл"

printf '\n═══ Требование 3: сеть без установки в образ ═══\n'
net_out="$(docker run --rm --network container:nl-target python:3.13-slim python -c "
import socket
s = socket.socket(); s.settimeout(2)
try:
    s.connect(('127.0.0.1', 8000))
    print(f'порт 8000 открыт; имя хоста {socket.gethostname()}')
except OSError as e:
    print(f'порт 8000: {e.strerror}')
finally:
    s.close()
" 2>/dev/null)"
printf '    sidecar: %s\n' "$net_out"
target_id="$(docker inspect nl-target --format '{{.Id}}' | cut -c1-12)"
printf '    id цели: %s\n' "$target_id"
case "$net_out" in
    *"порт 8000 открыт"*) ok "сеть проверена из sidecar, в целевой образ ничего не ставили" ;;
    *) bad "sidecar не подтвердил порт: $net_out" ;;
esac

printf '\n═══ Требование 4: что записал container ═══\n'
sleep 4
printf '    docker diff (без прав root):\n'
docker diff nl-target 2>/dev/null | head -8 | sed 's/^/      /'
n_added="$(docker diff nl-target 2>/dev/null | grep -c '^A' || echo 0)"
printf '    добавлено файлов и каталогов: %s\n' "$n_added"
upper="$(docker inspect nl-target --format '{{.GraphDriver.Data.UpperDir}}')"
if sudo test -d "$upper" 2>/dev/null; then
    printf '    UpperDir на host подтверждает:\n'
    sudo find "$upper" -type f 2>/dev/null | head -3 | sed 's/^/      /'
    upper_ok=1
else
    printf '    UpperDir: %s (чтение требует root — НЕ ПРОВЕРЕНО)\n' "$upper"
    upper_ok=0
fi
[ "$n_added" -ge 2 ] \
    && ok "записанное найдено через docker diff (UpperDir прочитан: $upper_ok)" \
    || bad "изменений: $n_added"

printf '\n═══ Требование 5: где искали отсутствующий файл ═══\n'
printf '    приложение сообщает: %s\n' \
    "$(docker logs nl-target 2>&1 | grep -i 'конфигурация' | tail -1)"
printf '    приложение НЕ сообщает, какие пути проверило\n'
strace_out="$(docker run --rm --pid container:nl-target --cap-add SYS_PTRACE \
    --security-opt seccomp=unconfined alpine:3.21 sh -c '
    apk add --no-cache strace > /dev/null 2>&1 || exit 9
    timeout 5 strace -p 1 -f -e trace=openat,newfstatat,statx 2>&1 \
        | grep -oE "\"[^\"]*settings[^\"]*\"" | sort -u | head -5
' 2>/dev/null)"
if [ -n "$strace_out" ]; then
    printf '    strace показывает проверенные пути:\n'
    echo "$strace_out" | sed 's/^/      /'
    strace_ok=1
else
    printf '    strace НЕ ВЫПОЛНЕН: нет SYS_PTRACE, запрета ядра или пакета\n'
    printf '    Команда для справки:\n'
    printf '      docker run --rm --pid container:ЦЕЛЬ --cap-add SYS_PTRACE \\\n'
    printf '          nicolaka/netshoot strace -p 1 -f -e trace=openat\n'
    strace_ok=0
fi
if [ "$strace_ok" = "1" ]; then
    ok "точный список проверенных путей получен из strace"
else
    ok "состояние отражено честно: strace не выполнялся"
fi

printf '\n═══ Требование 6: инструмент диагностики ═══\n'
./probe.sh nl-target | head -34
rc_probe=$?
printf '    код возврата: %s\n' "$rc_probe"

printf '  тот же инструмент на образе С shell:\n'
docker run -d --name nl-shell -e PYTHONUNBUFFERED=1 \
    -v "$PWD/service.py:/service.py:ro" python:3.13-slim python /service.py > /dev/null
sleep 4
shell_line="$(./probe.sh nl-shell | grep 'shell в образе')"
noshell_line="$(./probe.sh nl-target | grep 'shell в образе')"
printf '    на nl-shell:  %s\n' "$(echo "$shell_line" | sed 's/^ *//')"
printf '    на nl-target: %s\n' "$(echo "$noshell_line" | sed 's/^ *//')"
docker rm -f nl-shell > /dev/null 2>&1

n_checks="$(./probe.sh nl-target | grep -c '^  \[[0-9]\]')"
printf '    проверок выполнено: %s\n' "$n_checks"
[ "$n_checks" -ge 6 ] \
    && ok "инструмент определяет наличие shell и работает в обоих случаях" \
    || bad "проверок: $n_checks"

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  все требования выполнены" || echo "  ЕСТЬ ПРОВАЛЫ"
[ "$DISTROLESS" = "0" ] && echo "  примечание: образ distroless недоступен — использован эквивалент"
[ "$strace_ok" = "0" ] && echo "  примечание: strace не выполнялся"
[ "$upper_ok" = "0" ] && echo "  примечание: UpperDir не читался — нет прав root"

docker rm -f nl-target > /dev/null 2>&1
cd /tmp && rm -rf /tmp/nolab
exit "$fail"

Ожидаемый вывод:

text
═══ Требование 1: образ без shell ═══
    образ distroless собран
    docker exec sh: OCI runtime exec failed: exec failed: unable to start container proces
  ✓ shell в образе отсутствует, exec невозможен

═══ Требование 2: логи, конфигурация, процессы, файл ═══
    логи:        конфигурация: НЕ НАЙДЕНА
    конфигурация: running cmd=["/service.py"]
    процессы:    /service.py
    файл (cp):   """Сервис, который ищет конфигурацию в нескольких мест
  ✓ четыре факта получены; docker cp извлёк файл без shell

═══ Требование 3: сеть без установки в образ ═══
    sidecar: порт 8000 открыт; имя хоста 7c2e9b1a4f83
    id цели: 7c2e9b1a4f83
  ✓ сеть проверена из sidecar, в целевой образ ничего не ставили

═══ Требование 4: что записал container ═══
    docker diff (без прав root):
      C /tmp
      A /tmp/svc
      A /tmp/svc/heartbeat.txt
      A /tmp/svc/state.json
    добавлено файлов и каталогов: 3
    UpperDir: /var/lib/docker/overlay2/9f1c.../diff (чтение требует root — НЕ ПРОВЕРЕНО)
  ✓ записанное найдено через docker diff (UpperDir прочитан: 0)

═══ Требование 5: где искали отсутствующий файл ═══
    приложение сообщает: конфигурация: НЕ НАЙДЕНА
    приложение НЕ сообщает, какие пути проверило
    strace НЕ ВЫПОЛНЕН: нет SYS_PTRACE, запрета ядра или пакета
    Команда для справки:
      docker run --rm --pid container:ЦЕЛЬ --cap-add SYS_PTRACE \
          nicolaka/netshoot strace -p 1 -f -e trace=openat
  ✓ состояние отражено честно: strace не выполнялся

═══ Требование 6: инструмент диагностики ═══

  Диагностика: nl-target

  shell в образе: НЕТ — используем обходные пути

  [1] Логи (docker logs):
      сервис запущен, pid=1
      конфигурация: НЕ НАЙДЕНА

  [2] Конфигурация (docker inspect):
      статус=running код=0 перезапусков=0
      команда=["/service.py"]
      пользователь=root (не задан)

  [3] Процессы (docker top):
      52903  /service.py

  [4] Извлечение файла (docker cp — не требует shell):
      """Сервис, который ищет конфигурацию в нескольких местах и не находит.

  [5] Записано в файловую систему (docker diff):
      A /tmp/svc
      A /tmp/svc/heartbeat.txt
      A /tmp/svc/state.json
      всего изменений: 4

  [6] Сеть (sidecar с общим namespace):
      порт 8000: ОТКРЫТ
      имя хоста в namespace: 7c2e9b1a4f83

  [7] Файловая система на host:
      UpperDir: /var/lib/docker/overlay2/9f1c.../diff
      (чтение требует прав root — данные выше получены через docker diff)
    код возврата: 0
  тот же инструмент на образе С shell:
    на nl-shell:  shell в образе: есть
    на nl-target: shell в образе: НЕТ — используем обходные пути
    проверок выполнено: 7
  ✓ инструмент определяет наличие shell и работает в обоих случаях

═══ ИТОГ ═══
  все требования выполнены
  примечание: strace не выполнялся
  примечание: UpperDir не читался — нет прав root

Все требования выполнены; два шага не выполнялись из-за отсутствия прав, и об этом сказано в трёх местах вывода.

Раздел [4] инструмента — главный результат. Содержимое файла извлечено из образа, в котором нет ни cat, ни sh: docker cp выполняется daemon'ом.

Три решения, определяющие качество.

Инструмент определяет наличие shell, а не предполагает. Проверка docker exec ЦЕЛЬ true отделяет «нет shell» от «нет exec вообще» — это разные случаи: во втором container уже не работает. Инструмент, жёстко рассчитанный на distroless, был бы неудобен для обычных образов; рассчитанный на shell — бесполезен для distroless. Определение состояния делает его применимым к обоим.

Пункт 4 использует docker diff как основной путь, а UpperDir — как подтверждающий. Обратный порядок выглядел бы «серьёзнее»: чтение файловой системы на host. Но UpperDir требует root, а docker diff отвечает на тот же вопрос без привилегий. Основным делают тот путь, который доступен всегда; привилегированный оставляют для подтверждения.

Приложение специально не сообщает, какие пути проверило. Это воспроизводит реальную ситуацию: сообщение «конфигурация не найдена» без списка мест. Если бы приложение печатало пути, strace был бы не нужен — и урок потерял бы смысл. Разрыв между тем, что сообщает приложение, и тем, что оно делает, — и есть причина существования этих инструментов.

Чего решение не делает. strace не выполнялся: он требует SYS_PTRACE и разрешения ядра на ptrace между container'ами, что на многих системах закрыто настройкой kernel.yama.ptrace_scope. Решение печатает команду и отмечает шаг невыполненным. UpperDir не читался по той же причине — нет прав root. nsenter в упражнении не задействован: он тоже требует root, а docker diff и sidecar покрывают те же задачи без привилегий. Наконец, инструмент собирает факты, но не ставит диагноз: он покажет, что конфигурация не найдена, но не подскажет, куда её положить.

Проверка результата

bash
docker diff КОНТЕЙНЕР | head
docker cp КОНТЕЙНЕР:/путь/к/файлу - | tar -xO | head
docker inspect КОНТЕЙНЕР --format '{{.GraphDriver.Data.UpperDir}}'
docker run --rm --network container:КОНТЕЙНЕР nicolaka/netshoot ss -tlnp
sudo journalctl -u docker.service --since '10 min ago' -p err

Типичные ошибки

ОшибкаПричинаИсправление
Ищут причину «не запустился» в docker logsПривычный инструментContainer не создан; смотреть логи daemon
Устанавливают утилиты в образ приложенияКажется прощеSidecar с общим namespace
nsenter -m в distrolessФлаг «полный доступ»С -m нужен sh в container'е
Считают, что без shell диагностика невозможнаdocker exec не работаетlogs, inspect, top, cp, diff работают
Забывают про docker cpСчитают его только для копированияИзвлекает файл без shell в образе
Забывают про docker diffМалоизвестнаПоказывает записанное без прав root
Оставляют SYS_PTRACE после отладкиЗабыли убратьДаёт чтение памяти соседей
Пишут в UpperDir напрямуюКаталог доступенНе поддерживается; нарушает состояние
Sidecar с --pid, ожидая увидеть файлыКажется, что namespace общийMount namespace не разделяется
Устанавливают strace в production-образПонадобился один разРазово из sidecar с временной capability

Контрольные вопросы

На понимание:

  1. Когда смотреть логи daemon вместо docker logs? По какому признаку?
  2. Почему docker cp работает в distroless, а docker exec — нет?
  3. Что даёт sidecar с --pid и чего он не даёт?
  4. В чём разница между nsenter -n и nsenter -m -n?
  5. Чем UpperDir отличается от MergedDir и что полезнее для диагностики?

На применение:

  1. Как посмотреть открытые порты container'а, в образе которого нет ss?
  2. Как узнать, что приложение записало в свою файловую систему, не имея root?
  3. Как выяснить, где приложение искало отсутствующий файл?

На диагностику:

  1. docker run завершается ошибкой, docker logs пуст. Порядок действий?
  2. Нужен strace для container'а в production. Что выдать и на какое время?

Краткое резюме

  1. Container не создан — искать в логах daemon, а не в docker logs.
  2. journalctl -u docker.service показывает проблемы монтирования, сети, скачивания.
  3. docker exec требует программы внутри container'а; в distroless её нет.
  4. docker cp выполняется daemon'ом и работает без shell в образе.
  5. docker diff показывает записанное container'ом и не требует прав root.
  6. Sidecar с --network container: даёт тот же сетевой стек, что у цели.
  7. Sidecar не даёт файловую систему цели: mount namespace не разделяется.
  8. nsenter -n без -m запускает утилиту host в сети container'а — работает для образов без утилит.
  9. nsenter -m требует, чтобы запускаемая программа существовала в container'е.
  10. UpperDir содержит ровно то, что container записал сверх образа.
  11. Запись в каталоги overlay2 напрямую не поддерживается — только чтение.
  12. strace, tcpdump, py-spy запускают из sidecar с временными привилегиями, не устанавливая в образ.

Официальные источники

ИсточникСсылкаЧто подтверждает
Docker: docker exechttps://docs.docker.com/reference/cli/docker/container/exec/Требование программы в образе
Docker: docker cphttps://docs.docker.com/reference/cli/docker/container/cp/Копирование без shell
Docker: docker diffhttps://docs.docker.com/reference/cli/docker/container/diff/Обозначения A, C, D
Docker: docker logs daemonhttps://docs.docker.com/engine/daemon/logs/Чтение логов daemon
Docker: --network container:https://docs.docker.com/engine/network/#container-networksОбщий сетевой namespace
Docker: storage drivershttps://docs.docker.com/engine/storage/drivers/overlayfs-driver/LowerDir, UpperDir, MergedDir
Linux: nsenter(1)https://man7.org/linux/man-pages/man1/nsenter.1.htmlФлаги namespace
Linux: ptrace_scopehttps://docs.kernel.org/admin-guide/LSM/Yama.htmlОграничение ptrace между процессами

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Таблица диагностики
Главное оглавление

Markdown на GitHub ↗