4.3. Exec, logs, inspect
Цели
После этого материала вы сможете:
- входить в работающий container через
execи объяснять отличие отattach; - объяснить, почему процесс из
execне наследует переменные окружения точки входа; - читать логи с фильтрацией по времени, количеству строк и потоку;
- извлекать любое поле состояния через
docker inspect --format; - наблюдать за событиями Docker в реальном времени;
- читать
docker statsиdocker top, понимая происхождение цифр; - диагностировать container, в образе которого нет оболочки.
Предварительные знания
- 4.1. Состояния и переходы;
- 4.2. Режимы запуска — потоки и TTY;
- 2.4. Cgroups — откуда берутся метрики.
Ключевые термины
| Термин | Объяснение |
|---|---|
exec | Запуск дополнительного процесса в namespaces работающего container |
Go template | Синтаксис шаблонов, используемый флагом --format |
logging driver | Компонент, определяющий, куда попадают stdout и stderr container |
events | Поток уведомлений daemon об изменениях состояния объектов |
distroless | Образ без оболочки и системных утилит |
ephemeral container | Временный container, подключаемый к namespaces другого для диагностики |
Теория
exec запускает новый процесс
docker exec не «входит в container» — он запускает дополнительный процесс внутри его namespaces и cgroup.
container "api"
┌────────────────────────────────────┐
│ PID 1 gunicorn ← главный │
│ PID 7 gunicorn worker │
│ PID 8 gunicorn worker │
│ │
│ PID 42 sh ← docker exec
└────────────────────────────────────┘
Из этого следуют важные свойства:
| Свойство | Следствие |
|---|---|
| Процесс не является PID 1 | Его завершение не останавливает container |
| Наследует namespaces и cgroup | Видит ту же файловую систему, сеть, лимиты |
Не наследует переменные из ENTRYPOINT | Переменные, выставленные скриптом точки входа, недоступны |
Наследует ENV из образа | Переменные, объявленные в Dockerfile, доступны |
| Не переживает перезапуск container | При docker restart процессы exec уничтожаются |
Третья строка регулярно ставит в тупик. Если entrypoint-скрипт выполняет export DATABASE_URL=..., то процесс из exec этой переменной не увидит: она принадлежит окружению другого процесса. Видны только переменные из инструкции ENV и заданные флагом -e при запуске container.
exec против attach
docker exec | docker attach | |
|---|---|---|
| Что делает | Запускает новый процесс | Подключается к потокам PID 1 |
| Риск остановить container | Нет | Да — сигналом из терминала |
| Видит предыдущий вывод | Не применимо | Нет, только новый |
| Работает без оболочки в образе | Нет | Да |
| Пригодно для production | Да | Нет |
Практическое правило: exec — рабочий инструмент, attach — почти всегда ошибка.
Как устроены логи
Container пишет в stdout и stderr. Docker перехватывает оба потока и передаёт их logging driver.
процесс ──► stdout ─┐
├──► logging driver ──► хранилище ──► docker logs
──► stderr ─┘ (файл, journald, …)
Ключевые следствия:
1. docker logs работает только с драйверами, поддерживающими чтение. Это json-file, local, journald. При драйверах syslog, fluentd, awslogs команда вернёт ошибку — логи ушли во внешнюю систему.
2. Логи, записанные в файл внутри container, Docker не видит. Приложение, пишущее в /var/log/app.log, не появится в docker logs. Это одна из причин требования писать в stdout (раздел 06).
3. Логи удаляются вместе с container. docker rm уничтожает их. Для сохранения нужна внешняя система или volume.
4. Без ротации логи растут неограниченно. Драйвер json-file по умолчанию не ограничивает размер. Один разговорчивый сервис способен заполнить диск. Настройка разбиралась в уроке 1.3.
inspect и шаблоны Go
docker inspect возвращает полное состояние объекта в JSON — сотни полей. Флаг --format позволяет извлечь нужное, используя синтаксис шаблонов Go.
Базовые конструкции:
| Конструкция | Назначение |
|---|---|
{{.Field}} | Значение поля |
{{.A.B.C}} | Вложенное поле |
{{json .Field}} | Поле как JSON |
{{index .Arr 0}} | Элемент массива по индексу |
{{len .Arr}} | Длина массива или строки |
{{range .Arr}}...{{end}} | Цикл по элементам |
{{if .Cond}}...{{else}}...{{end}} | Условие |
{{printf "%s" .X}} | Форматирование |
Синтаксис общий для inspect, ps, images, stats, version, info — освоив его один раз, вы применяете его везде.
Откуда берутся цифры в stats
docker stats читает статистику из cgroup container (урок 2.4).
| Колонка | Источник | Замечание |
|---|---|---|
CPU % | cpu.stat, дельта между замерами | 100 % = одно ядро полностью |
MEM USAGE | memory.current | Включает кэш страниц |
LIMIT | memory.max | При отсутствии лимита — вся память host |
NET I/O | Статистика сетевого интерфейса | Суммарно за жизнь container |
BLOCK I/O | io.stat | Только блочные устройства, не volumes на tmpfs |
PIDS | pids.current | Число процессов и потоков |
Важная деталь: MEM USAGE включает кэш страниц. Приложение может показывать 80 MB по своим метрикам, а docker stats — 300 MB, потому что ядро закэшировало прочитанные файлы. Этот кэш освобождается под давлением и не является утечкой.
CPU % может превышать 100 %: значение 250 % означает использование двух с половиной ядер.
Внутренний механизм
Что делает docker exec
- CLI отправляет
POST /containers/<id>/execс описанием команды. - Daemon через containerd создаёт дополнительный процесс.
runcвходит в namespaces существующего container черезsetns().- Процесс помещается в ту же cgroup — значит, подчиняется тем же лимитам.
- Применяются capabilities и seccomp-профиль container.
- Выполняется
execve().
Шаг 4 объясняет неочевидное поведение: тяжёлая команда, запущенная через exec, расходует лимиты container и может вызвать OOM основного приложения.
Шаг 3 объясняет отсутствие переменных entrypoint: setns() переносит процесс в namespaces, но окружение формируется заново — из ENV образа и флагов exec.
Как docker logs разделяет потоки
Драйвер json-file сохраняет каждую строку как объект с полем stream:
{"log":"строка вывода\n","stream":"stdout","time":"2026-07-30T10:00:00.123456789Z"}
Поле stream позволяет docker logs разделять потоки при перенаправлении. Драйвер local использует двоичный формат, более эффективный по месту, но с той же семантикой.
При выделенном TTY (-t) поля stream нет — потоки слиты ещё на уровне container (урок 4.2).
Команды и примеры
Базовый exec
docker run -d --name web nginx:alpine > /dev/null
sleep 1
# разовая команда
docker exec web nginx -v
# интерактивная оболочка
docker exec -it web sh -c 'echo "внутри: $(hostname)"; exit'
nginx version: nginx/1.29.2
внутри: 7f8a9b0c1d2e
Процесс exec не является PID 1:
docker exec web ps -o pid,ppid,comm
PID PPID COMMAND
1 0 nginx
30 1 nginx
36 0 ps
ps получил PID 36 и PPID 0 — он порождён не главным процессом, а извне, через setns().
Завершение exec-процесса не влияет на container:
docker exec web sh -c 'exit 1'
echo "exit code exec: $?"
docker ps --filter name=web --format '{{.Names}} {{.Status}}'
exit code exec: 1
web Up 5 seconds
Переменные окружения в exec
cd /tmp && mkdir -p execdemo && cd execdemo
cat > entrypoint.sh <<'SH'
#!/bin/sh
# Переменная выставляется скриптом точки входа
export RUNTIME_TOKEN="создан-в-entrypoint"
echo "entrypoint видит: $RUNTIME_TOKEN"
exec "$@"
SH
chmod +x entrypoint.sh
cat > Dockerfile <<'EOF'
FROM alpine:3.21
ENV FROM_DOCKERFILE="объявлен через ENV"
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["sleep", "300"]
EOF
docker build -q -t envdemo:1 . > /dev/null
docker run -d --name envtest -e FROM_FLAG="передан через -e" envdemo:1 > /dev/null
sleep 1
echo "--- лог entrypoint ---"
docker logs envtest
echo "--- что видит exec ---"
docker exec envtest sh -c '
echo "FROM_DOCKERFILE: ${FROM_DOCKERFILE:-НЕ ВИДЕН}"
echo "FROM_FLAG: ${FROM_FLAG:-НЕ ВИДЕН}"
echo "RUNTIME_TOKEN: ${RUNTIME_TOKEN:-НЕ ВИДЕН}"
'
--- лог entrypoint ---
entrypoint видит: создан-в-entrypoint
--- что видит exec ---
FROM_DOCKERFILE: объявлен через ENV
FROM_FLAG: передан через -e
RUNTIME_TOKEN: НЕ ВИДЕН
Переменная, созданная entrypoint-скриптом, недоступна. Это принадлежность окружения конкретного процесса, а не container.
Обходной путь — прочитать окружение PID 1 напрямую:
docker exec envtest sh -c "tr '\0' '\n' < /proc/1/environ | grep RUNTIME_TOKEN"
RUNTIME_TOKEN=создан-в-entrypoint
Приём полезен при диагностике: он показывает фактическое окружение работающего приложения.
docker rm -f envtest > /dev/null
exec подчиняется лимитам container
docker run -d --name limited --memory=64m alpine sleep 300 > /dev/null
docker exec limited sh -c 'cat /sys/fs/cgroup/memory.max'
docker exec limited sh -c '
# попытка выделить больше лимита
dd if=/dev/zero of=/dev/null bs=1M count=1 2>/dev/null
echo "мелкая операция прошла"
'
docker rm -f limited > /dev/null
67108864
мелкая операция прошла
Процесс exec видит лимит 64 MB — он в той же cgroup. Тяжёлая команда, запущенная так, способна вызвать OOM всего container, включая основное приложение.
На production-сервисе не запускайте через
execресурсоёмкие операции: дампы базы, архивирование больших каталогов, профилирование с записью в память. Они расходуют лимиты работающего приложения.
Чтение логов
docker run -d --name talker alpine sh -c '
i=1
while [ $i -le 20 ]; do
echo "сообщение $i"
[ $((i % 5)) -eq 0 ] && echo "ошибка на шаге $i" >&2
i=$((i + 1))
sleep 0.3
done
sleep 60
' > /dev/null
sleep 8
Основные варианты:
# всё
docker logs talker | head -5
# последние строки
docker logs --tail 5 talker
# с метками времени
docker logs --tail 3 --timestamps talker
# за последние 3 секунды
docker logs --since 3s talker | head -3
# в реальном времени (прервать через Ctrl+C)
# docker logs -f talker
сообщение 1
сообщение 2
сообщение 3
сообщение 4
сообщение 5
сообщение 18
ошибка на шаге 20
сообщение 19
сообщение 20
2026-07-30T10:15:22.481Z сообщение 19
2026-07-30T10:15:22.783Z сообщение 20
2026-07-30T10:15:22.784Z ошибка на шаге 20
Разделение потоков:
echo "--- только stdout ---"
docker logs talker 2>/dev/null | tail -3
echo "--- только stderr ---"
docker logs talker 1>/dev/null | tail -3
--- только stdout ---
сообщение 18
сообщение 19
сообщение 20
--- только stderr ---
ошибка на шаге 15
ошибка на шаге 20
Работает, потому что драйвер сохранил метку потока для каждой строки. При запуске с -t это разделение было бы потеряно.
Полезные комбинации флагов:
| Задача | Команда |
|---|---|
| Что происходит прямо сейчас | docker logs -f --tail 0 <c> |
| Последняя минута | docker logs --since 1m <c> |
| Между двумя моментами | docker logs --since 10:00 --until 10:05 <c> |
| Только ошибки | docker logs <c> 1>/dev/null |
| Поиск по логам | docker logs <c> 2>&1 | grep -i error |
Обратите внимание на 2>&1 в последней строке: без него grep получит только stdout и пропустит ошибки.
inspect и шаблоны
docker run -d --name insp \
-e APP_ENV=production \
-p 8080:80 \
--memory=256m \
--label team=backend \
nginx:alpine > /dev/null
sleep 1
Простые поля:
docker inspect insp --format '{{.State.Status}}'
docker inspect insp --format '{{.Config.Image}}'
# Не .NetworkSettings.IPAddress: на Docker 29 этого поля нет,
# и шаблон падает с «map has no entry for key "IPAddress"».
# Адрес лежит внутри каждой сети, к которой подключён container.
docker inspect insp --format '{{range $net, $conf := .NetworkSettings.Networks}}{{$net}}={{$conf.IPAddress}}{{end}}'
Несколько полей сразу:
docker inspect insp --format \
'имя={{.Name}} статус={{.State.Status}} PID={{.State.Pid}} образ={{.Config.Image}}'
имя=/insp статус=running PID=192841 образ=nginx:alpine
Циклы:
echo "--- переменные окружения ---"
docker inspect insp --format '{{range .Config.Env}} {{.}}{{"\n"}}{{end}}'
echo "--- опубликованные порты ---"
docker inspect insp --format \
'{{range $port, $binds := .NetworkSettings.Ports}}{{range $binds}} {{$port}} -> {{.HostIp}}:{{.HostPort}}{{"\n"}}{{end}}{{end}}'
--- переменные окружения ---
APP_ENV=production
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NGINX_VERSION=1.29.2
...
--- опубликованные порты ---
80/tcp -> 0.0.0.0:8080
80/tcp -> ::0:8080
Условия:
docker inspect insp --format \
'{{if .State.Running}}работает с {{.State.StartedAt}}{{else}}остановлен, код {{.State.ExitCode}}{{end}}'
Форматированный отчёт:
docker inspect insp --format '{{printf "%-14s %s\n" "Имя:" .Name}}{{printf "%-14s %s\n" "Статус:" .State.Status}}{{printf "%-14s %s\n" "Образ:" .Config.Image}}{{printf "%-14s %v\n" "Память:" .HostConfig.Memory}}{{printf "%-14s %s\n" "Рестарт:" .HostConfig.RestartPolicy.Name}}'
Имя: /insp
Статус: running
Образ: nginx:alpine
Память: 268435456
Рестарт: no
Извлечение всего состояния как JSON — удобно для дальнейшей обработки:
docker inspect insp --format '{{json .State}}' | python3 -m json.tool | head -8
Несколько containers сразу:
docker run -d --name insp2 alpine sleep 120 > /dev/null
docker inspect insp insp2 --format '{{.Name}} {{.State.Status}}'
docker rm -f insp2 > /dev/null
Часто используемые шаблоны стоит сохранить как алиасы:
# PID процесса
docker inspect <c> --format '{{.State.Pid}}'
# IP-адрес в конкретной сети
docker inspect <c> --format '{{.NetworkSettings.Networks.bridge.IPAddress}}'
# все монтирования
docker inspect <c> --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
# состояние healthcheck
docker inspect <c> --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}нет healthcheck{{end}}'
События в реальном времени
# в отдельном терминале: docker events
# здесь — с ограничением по времени
timeout 6 docker events --filter 'type=container' --format \
'{{.Time}} {{.Action}} {{.Actor.Attributes.name}}' &
EV=$!
sleep 1
docker run -d --name ev-demo alpine sleep 2 > /dev/null
sleep 4
wait $EV 2>/dev/null
docker rm -f ev-demo > /dev/null 2>&1 || true
1785405623 create ev-demo
1785405623 attach ev-demo
1785405623 start ev-demo
1785405625 die ev-demo
Полезные фильтры:
docker events --filter 'event=die' # только завершения
docker events --filter 'event=oom' # только OOM
docker events --filter 'container=api' # конкретный container
docker events --filter 'event=health_status' # переходы healthcheck
docker events --since '10m' --until '5m' # исторические события
docker events — единственный способ увидеть последовательность происходящего. При диагностике restart loop он показывает цикл start → die → start с интервалами, чего не видно ни в логах, ни в ps.
Метрики
docker run -d --name load --memory=128m alpine \
sh -c 'while true; do :; done' > /dev/null
sleep 3
docker stats --no-stream --format \
'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.PIDs}}' load
NAME CPU % MEM USAGE / LIMIT MEM % PIDS
load 99.87% 528KiB / 128MiB 0.40% 1
Флаг --no-stream делает один замер. Без него команда работает интерактивно и обновляется.
Процессы container с точки зрения host:
docker top load -o pid,ppid,pcpu,rss,cmd
PID PPID %CPU RSS CMD
193044 193022 99.4 528 /bin/sh -c while true; do :; done
docker top показывает PID на host, а не внутри container. Это удобно: полученный PID сразу пригоден для nsenter, strace и других инструментов host (раздел 13).
Сверим MEM USAGE с cgroup напрямую:
CPID="$(docker inspect load --format '{{.State.Pid}}')"
CG="/sys/fs/cgroup$(awk -F: '{print $3}' /proc/"$CPID"/cgroup)"
echo "memory.current: $(cat "$CG/memory.current") байт"
echo "memory.max: $(cat "$CG/memory.max") байт"
memory.current: 540672 байт
memory.max: 134217728 байт
Цифры совпадают с выводом stats — источник тот же.
docker rm -f load talker web > /dev/null
Диагностика container без оболочки
Production-образы часто не содержат оболочки — это правильная практика (раздел 11), но она ломает привычный docker exec -it ... sh.
# образ без оболочки
cat > /tmp/execdemo/Dockerfile.min <<'EOF'
FROM alpine:3.21 AS builder
RUN echo 'ok' > /data.txt
FROM scratch
COPY --from=builder /data.txt /data.txt
COPY --from=builder /bin/sleep /bin/sleep
COPY --from=builder /lib/ld-musl-*.so.1 /lib/
ENTRYPOINT ["/bin/sleep"]
CMD ["300"]
EOF
cd /tmp/execdemo
docker build -q -f Dockerfile.min -t noshell:1 . > /dev/null
docker run -d --name noshell noshell:1 > /dev/null
sleep 1
echo "--- обычный exec не работает ---"
docker exec noshell sh 2>&1 | tail -1
--- обычный exec не работает ---
OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH: unknown
Три рабочих подхода:
1. nsenter с host — использует утилиты host:
CPID="$(docker inspect noshell --format '{{.State.Pid}}')"
echo "файловая система container:"
sudo nsenter -t "$CPID" -m ls /
echo "содержимое файла:"
sudo nsenter -t "$CPID" -m cat /data.txt
файловая система container:
bin data.txt lib
содержимое файла:
ok
2. Временный container в тех же namespaces:
docker run --rm -it \
--pid=container:noshell \
--network=container:noshell \
alpine ps -o pid,comm
PID COMMAND
1 /bin/sleep
14 ps
Мы видим процессы noshell, используя утилиты Alpine. Файловая система при этом своя — для доступа к ней нужен вариант 1 или монтирование.
3. Доступ к файловой системе через /proc:
sudo ls "/proc/$CPID/root/"
sudo cat "/proc/$CPID/root/data.txt"
bin data.txt lib
ok
Каталог /proc/<pid>/root — это корень файловой системы процесса с точки зрения host. Работает без nsenter и без дополнительных container.
Уборка:
docker rm -f noshell > /dev/null
docker rmi noshell:1 envdemo:1 > /dev/null 2>&1 || true
cd /tmp && rm -rf /tmp/execdemo
Практическое упражнение
Задание. Напишите скрипт container-report.sh <container>, который собирает диагностический отчёт по работающему container.
Отчёт должен включать:
- Основное: имя, образ, статус, время работы, PID на host, exit code (если завершён).
- Конфигурацию: команда, рабочий каталог, пользователь, число переменных окружения.
- Ресурсы: лимит памяти, текущее потребление, CPU %, число процессов.
- Сеть: IP-адрес, опубликованные порты.
- Монтирования: тип, источник, назначение.
- Здоровье: статус healthcheck, если он настроен.
- Логи: последние 5 строк с разделением stdout и stderr.
- Признаки проблем: OOM, ненулевой exit code, перезапуски.
Скрипт должен корректно работать и для остановленного container.
Подсказки
Подсказка 1
Проверять наличие healthcheck нужно условно:
docker inspect <c> --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}нет{{end}}'
Подсказка 2
docker stats для остановленного container вернёт ошибку. Вызывайте его только при .State.Running.
Подсказка 3
Число перезапусков доступно в .RestartCount — это прямой признак restart loop.
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# container-report.sh — диагностический отчёт по container.
set -uo pipefail
C="${1:?Использование: $0 <container>}"
docker inspect "$C" > /dev/null 2>&1 || { echo "Container не найден: $C" >&2; exit 1; }
f() { docker inspect "$C" --format "$1" 2>/dev/null; }
row() { printf ' %-18s %s\n' "$1:" "$2"; }
RUNNING="$(f '{{.State.Running}}')"
echo "═══ 1. Основное ═══"
row "Имя" "$(f '{{.Name}}' | sed 's|^/||')"
row "Образ" "$(f '{{.Config.Image}}')"
row "Статус" "$(f '{{.State.Status}}')"
if [ "$RUNNING" = "true" ]; then
row "Запущен" "$(f '{{.State.StartedAt}}')"
row "PID на host" "$(f '{{.State.Pid}}')"
else
row "Завершён" "$(f '{{.State.FinishedAt}}')"
row "Exit code" "$(f '{{.State.ExitCode}}')"
fi
row "Перезапусков" "$(f '{{.RestartCount}}')"
echo
echo "═══ 2. Конфигурация ═══"
row "Entrypoint" "$(f '{{if .Config.Entrypoint}}{{join .Config.Entrypoint " "}}{{else}}(не задан){{end}}')"
row "Cmd" "$(f '{{if .Config.Cmd}}{{join .Config.Cmd " "}}{{else}}(не задан){{end}}')"
row "WorkingDir" "$(f '{{if .Config.WorkingDir}}{{.Config.WorkingDir}}{{else}}/{{end}}')"
row "User" "$(f '{{if .Config.User}}{{.Config.User}}{{else}}root (0){{end}}')"
row "Env" "$(f '{{len .Config.Env}}') переменных"
row "RestartPolicy" "$(f '{{.HostConfig.RestartPolicy.Name}}')"
echo
echo "═══ 3. Ресурсы ═══"
MEMLIMIT="$(f '{{.HostConfig.Memory}}')"
if [ "$MEMLIMIT" = "0" ]; then
row "Лимит памяти" "не задан"
else
row "Лимит памяти" "$(awk -v b="$MEMLIMIT" 'BEGIN{printf "%.0f MB", b/1048576}')"
fi
NANOCPU="$(f '{{.HostConfig.NanoCpus}}')"
[ "$NANOCPU" != "0" ] && row "Лимит CPU" "$(awk -v n="$NANOCPU" 'BEGIN{printf "%.2f ядра", n/1e9}')"
if [ "$RUNNING" = "true" ]; then
stats="$(docker stats --no-stream --format '{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}' "$C" 2>/dev/null)"
row "CPU" "$(cut -f1 <<< "$stats")"
row "Память" "$(cut -f2 <<< "$stats")"
row "Процессов" "$(cut -f3 <<< "$stats")"
fi
echo
echo "═══ 4. Сеть ═══"
nets="$(f '{{range $n, $c := .NetworkSettings.Networks}}{{$n}}={{$c.IPAddress}} {{end}}')"
row "Сети" "${nets:-нет}"
ports="$(f '{{range $p, $b := .NetworkSettings.Ports}}{{range $b}}{{$p}}->{{.HostIp}}:{{.HostPort}} {{end}}{{end}}')"
row "Порты" "${ports:-не опубликованы}"
echo
echo "═══ 5. Монтирования ═══"
mounts="$(f '{{range .Mounts}} {{.Type}}: {{.Source}} -> {{.Destination}}{{if .RW}} (rw){{else}} (ro){{end}}{{"\n"}}{{end}}')"
[ -n "$mounts" ] && echo "$mounts" || echo " нет"
echo
echo "═══ 6. Здоровье ═══"
health="$(f '{{if .State.Health}}{{.State.Health.Status}}{{else}}не настроен{{end}}')"
row "Healthcheck" "$health"
if [ "$health" != "не настроен" ]; then
row "Неудач подряд" "$(f '{{.State.Health.FailingStreak}}')"
last="$(f '{{if .State.Health.Log}}{{with index .State.Health.Log 0}}{{.ExitCode}}{{end}}{{end}}')"
[ -n "$last" ] && row "Код последней" "$last"
fi
echo
echo "═══ 7. Логи ═══"
echo " -- stdout (последние 5) --"
docker logs --tail 5 "$C" 2>/dev/null | sed 's/^/ /' || echo " (пусто)"
echo " -- stderr (последние 5) --"
docker logs --tail 5 "$C" 2>&1 1>/dev/null | sed 's/^/ /' || echo " (пусто)"
echo
echo "═══ 8. Признаки проблем ═══"
issues=0
if [ "$(f '{{.State.OOMKilled}}')" = "true" ]; then
echo " [!] OOMKilled: процесс убит по нехватке памяти"
echo " Проверьте лимит и фактическое потребление"
issues=$((issues+1))
fi
code="$(f '{{.State.ExitCode}}')"
if [ "$RUNNING" != "true" ] && [ "$code" != "0" ]; then
echo " [!] Ненулевой exit code: $code"
case "$code" in
125) echo " Ошибка самого Docker (неверные флаги)" ;;
126) echo " Команда найдена, но не исполняема" ;;
127) echo " Команда не найдена" ;;
137) echo " SIGKILL: OOM или docker kill" ;;
143) echo " PID 1 умер от SIGTERM — обычно это --init" ;;
esac
issues=$((issues+1))
fi
rc="$(f '{{.RestartCount}}')"
if [ "${rc:-0}" -gt 3 ]; then
echo " [!] Перезапусков: $rc — похоже на restart loop"
echo " Смотрите: docker events --filter container=$C"
issues=$((issues+1))
fi
if [ "$health" = "unhealthy" ]; then
echo " [!] Healthcheck failing"
issues=$((issues+1))
fi
err="$(f '{{.State.Error}}')"
[ -n "$err" ] && { echo " [!] Ошибка: $err"; issues=$((issues+1)); }
[ "$issues" -eq 0 ] && echo " [+] проблем не обнаружено"
Проверка:
chmod +x container-report.sh
docker run -d --name demo \
-p 8080:80 --memory=256m \
-e APP_ENV=production \
--health-cmd='wget -qO- http://localhost/ >/dev/null || exit 1' \
--health-interval=5s \
nginx:alpine > /dev/null
sleep 12
./container-report.sh demo
docker rm -f demo > /dev/null
Ожидаемый вывод (сокращённо):
═══ 1. Основное ═══
Имя: demo
Образ: nginx:alpine
Статус: running
Запущен: 2026-07-30T10:31:02.184Z
PID на host: 194207
Перезапусков: 0
═══ 2. Конфигурация ═══
Entrypoint: /docker-entrypoint.sh
Cmd: nginx -g daemon off;
WorkingDir: /
User: root (0)
Env: 3 переменных
RestartPolicy: no
═══ 3. Ресурсы ═══
Лимит памяти: 256 MB
CPU: 0.00%
Память: 8.516MiB / 256MiB
Процессов: 9
═══ 4. Сеть ═══
Сети: bridge=172.17.0.2
Порты: 80/tcp->0.0.0.0:8080
═══ 5. Монтирования ═══
нет
═══ 6. Здоровье ═══
Healthcheck: healthy
Неудач подряд: 0
Код последней: 0
═══ 7. Логи ═══
-- stdout (последние 5) --
172.17.0.1 - - [30/Jul/2026:10:31:14 +0000] "GET / HTTP/1.1" 200 615 "-" "Wget"
-- stderr (последние 5) --
/docker-entrypoint.sh: Configuration complete; ready for start up
═══ 8. Признаки проблем ═══
[+] проблем обнаружено
Что делает отчёт полезным.
Раздел 8 — не украшение, а суть. Он превращает набор фактов в диагноз: exit code 137 сам по себе означает лишь «убит сигналом 9», а вместе с полем OOMKilled уже различает нехватку памяти и внешний docker kill. Счётчик RestartCount больше трёх — прямой признак restart loop, который не виден ни в логах, ни в docker ps.
Обратите внимание на строку User: root (0) для nginx. Официальный образ запускается от root — это то, что в разделе 11 требуется изменить.
Скрипт корректно работает для остановленного container: вместо метрик показывает время завершения и exit code, а docker stats не вызывается вовсе.
Проверка результата
docker run -d --name verify alpine sleep 60 > /dev/null
docker inspect verify --format '{{.Name}} {{.State.Status}} {{.State.Pid}}'
docker exec verify ps -o pid,comm
docker rm -f verify > /dev/null
Убедитесь, что понимаете, почему процесс ps в выводе имеет PID больше единицы.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
docker attach вместо exec | Кажется способом «войти внутрь» | attach подключается к PID 1 и может остановить сервис |
Переменная из entrypoint не видна в exec | Она принадлежит окружению другого процесса | Читать /proc/1/environ или задавать через ENV |
Тяжёлая команда через exec в production | Не учтено, что процесс в той же cgroup | Расходует лимиты приложения, возможен OOM |
| Логи приложения не видны | Приложение пишет в файл внутри container | Писать в stdout и stderr |
docker logs | grep error не находит ошибок | grep получает только stdout | Добавить 2>&1 |
| Диск заполнен логами | json-file без ротации | Настроить max-size и max-file |
Ручной разбор JSON из inspect | Не освоен --format | Шаблоны Go работают во всех командах Docker |
MEM USAGE считают утечкой | В значение входит кэш страниц | Сравнивать с метриками приложения |
| Нет способа диагностировать distroless-образ | Нет оболочки | nsenter, --pid=container:, /proc/<pid>/root |
Контрольные вопросы
На понимание:
- Почему процесс из
docker execне является PID 1 и что из этого следует? - Почему переменная, экспортированная в entrypoint-скрипте, не видна в
exec? - Почему логи приложения, пишущего в файл, не появляются в
docker logs? - Почему
docker logs <c> | grep errorможет ничего не найти, хотя ошибки есть? - Почему
MEM USAGEвdocker statsбольше, чем показывает само приложение?
На применение:
- Как посмотреть фактическое окружение работающего приложения, включая переменные из entrypoint?
- Как получить только строки stderr из логов container?
- Как одной командой вывести имя, статус и IP-адрес нескольких containers?
На диагностику:
- Container перезапускается каждые 30 секунд. Какие три команды дадут больше всего информации?
- Нужно посмотреть файловую систему container, в образе которого нет ни
sh, ниls. Назовите два способа.
Краткое резюме
docker execзапускает новый процесс в namespaces и cgroup container, а не «входит» в него.- Процесс
execне является PID 1; его завершение не влияет на container. - Переменные, созданные entrypoint-скриптом, в
execне видны — читайте/proc/1/environ. - Процесс
execподчиняется лимитам container и может вызвать OOM приложения. attachподключается к главному процессу и опасен в production.docker logsработает только с драйверами, поддерживающими чтение, и только со stdout и stderr.- Логи удаляются вместе с container; без ротации они растут неограниченно.
--formatс шаблонами Go работает вinspect,ps,images,statsи других командах.docker eventsпоказывает последовательность происходящего — главный инструмент при restart loop.- Container без оболочки диагностируется через
nsenter,--pid=container:или/proc/<pid>/root.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| docker exec reference | https://docs.docker.com/reference/cli/docker/container/exec/ | Запуск дополнительного процесса, флаги -i, -t, -e, -u, -w |
| docker logs reference | https://docs.docker.com/reference/cli/docker/container/logs/ | Флаги --tail, --since, --until, --timestamps, -f; ограничения по драйверам |
| docker inspect reference | https://docs.docker.com/reference/cli/docker/inspect/ | Структура вывода, шаблоны --format |
| Format command and log output | https://docs.docker.com/engine/cli/formatting/ | Синтаксис шаблонов Go: range, if, json, printf, index |
| docker stats reference | https://docs.docker.com/reference/cli/docker/container/stats/ | Колонки вывода, --no-stream, источник метрик |
| docker top reference | https://docs.docker.com/reference/cli/docker/container/top/ | Процессы container с PID host |
| docker events reference | https://docs.docker.com/reference/cli/docker/system/events/ | Типы событий, фильтры, форматирование |
| Configure logging drivers | https://docs.docker.com/engine/logging/configure/ | Какие драйверы поддерживают docker logs |
proc(5) man page | https://man7.org/linux/man-pages/man5/proc.5.html | /proc/<pid>/environ, /proc/<pid>/root |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Сигналы и graceful shutdown
Главное оглавление