Главная/Containers и lifecycle/Урок

4.3. Exec, logs, inspect

Цели

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

  • входить в работающий container через exec и объяснять отличие от attach;
  • объяснить, почему процесс из exec не наследует переменные окружения точки входа;
  • читать логи с фильтрацией по времени, количеству строк и потоку;
  • извлекать любое поле состояния через docker inspect --format;
  • наблюдать за событиями Docker в реальном времени;
  • читать docker stats и docker top, понимая происхождение цифр;
  • диагностировать container, в образе которого нет оболочки.

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

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

ТерминОбъяснение
execЗапуск дополнительного процесса в namespaces работающего container
Go templateСинтаксис шаблонов, используемый флагом --format
logging driverКомпонент, определяющий, куда попадают stdout и stderr container
eventsПоток уведомлений daemon об изменениях состояния объектов
distrolessОбраз без оболочки и системных утилит
ephemeral containerВременный container, подключаемый к namespaces другого для диагностики

Теория

exec запускает новый процесс

docker exec не «входит в container» — он запускает дополнительный процесс внутри его namespaces и cgroup.

text
   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 execdocker attach
Что делаетЗапускает новый процессПодключается к потокам PID 1
Риск остановить containerНетДа — сигналом из терминала
Видит предыдущий выводНе применимоНет, только новый
Работает без оболочки в образеНетДа
Пригодно для productionДаНет

Практическое правило: exec — рабочий инструмент, attach — почти всегда ошибка.

Как устроены логи

Container пишет в stdout и stderr. Docker перехватывает оба потока и передаёт их logging driver.

text
   процесс ──► 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 USAGEmemory.currentВключает кэш страниц
LIMITmemory.maxПри отсутствии лимита — вся память host
NET I/OСтатистика сетевого интерфейсаСуммарно за жизнь container
BLOCK I/Oio.statТолько блочные устройства, не volumes на tmpfs
PIDSpids.currentЧисло процессов и потоков

Важная деталь: MEM USAGE включает кэш страниц. Приложение может показывать 80 MB по своим метрикам, а docker stats — 300 MB, потому что ядро закэшировало прочитанные файлы. Этот кэш освобождается под давлением и не является утечкой.

CPU % может превышать 100 %: значение 250 % означает использование двух с половиной ядер.


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

Что делает docker exec

  1. CLI отправляет POST /containers/<id>/exec с описанием команды.
  2. Daemon через containerd создаёт дополнительный процесс.
  3. runc входит в namespaces существующего container через setns().
  4. Процесс помещается в ту же cgroup — значит, подчиняется тем же лимитам.
  5. Применяются capabilities и seccomp-профиль container.
  6. Выполняется execve().

Шаг 4 объясняет неочевидное поведение: тяжёлая команда, запущенная через exec, расходует лимиты container и может вызвать OOM основного приложения.

Шаг 3 объясняет отсутствие переменных entrypoint: setns() переносит процесс в namespaces, но окружение формируется заново — из ENV образа и флагов exec.

Как docker logs разделяет потоки

Драйвер json-file сохраняет каждую строку как объект с полем stream:

json
{"log":"строка вывода\n","stream":"stdout","time":"2026-07-30T10:00:00.123456789Z"}

Поле stream позволяет docker logs разделять потоки при перенаправлении. Драйвер local использует двоичный формат, более эффективный по месту, но с той же семантикой.

При выделенном TTY (-t) поля stream нет — потоки слиты ещё на уровне container (урок 4.2).


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

Базовый exec

bash
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'
text
nginx version: nginx/1.29.2
внутри: 7f8a9b0c1d2e

Процесс exec не является PID 1:

bash
docker exec web ps -o pid,ppid,comm
text
PID   PPID  COMMAND
    1     0 nginx
   30     1 nginx
   36     0 ps

ps получил PID 36 и PPID 0 — он порождён не главным процессом, а извне, через setns().

Завершение exec-процесса не влияет на container:

bash
docker exec web sh -c 'exit 1'
echo "exit code exec: $?"
docker ps --filter name=web --format '{{.Names}} {{.Status}}'
text
exit code exec: 1
web Up 5 seconds

Переменные окружения в exec

bash
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:-НЕ ВИДЕН}"
'
text
--- лог entrypoint ---
entrypoint видит: создан-в-entrypoint
--- что видит exec ---
FROM_DOCKERFILE: объявлен через ENV
FROM_FLAG:       передан через -e
RUNTIME_TOKEN:   НЕ ВИДЕН

Переменная, созданная entrypoint-скриптом, недоступна. Это принадлежность окружения конкретного процесса, а не container.

Обходной путь — прочитать окружение PID 1 напрямую:

bash
docker exec envtest sh -c "tr '\0' '\n' < /proc/1/environ | grep RUNTIME_TOKEN"
text
RUNTIME_TOKEN=создан-в-entrypoint

Приём полезен при диагностике: он показывает фактическое окружение работающего приложения.

bash
docker rm -f envtest > /dev/null

exec подчиняется лимитам container

bash
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
text
67108864
мелкая операция прошла

Процесс exec видит лимит 64 MB — он в той же cgroup. Тяжёлая команда, запущенная так, способна вызвать OOM всего container, включая основное приложение.

На production-сервисе не запускайте через exec ресурсоёмкие операции: дампы базы, архивирование больших каталогов, профилирование с записью в память. Они расходуют лимиты работающего приложения.

Чтение логов

bash
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

Основные варианты:

bash
# всё
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
text
сообщение 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

Разделение потоков:

bash
echo "--- только stdout ---"
docker logs talker 2>/dev/null | tail -3
echo "--- только stderr ---"
docker logs talker 1>/dev/null | tail -3
text
--- только 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 и шаблоны

bash
docker run -d --name insp \
    -e APP_ENV=production \
    -p 8080:80 \
    --memory=256m \
    --label team=backend \
    nginx:alpine > /dev/null
sleep 1

Простые поля:

bash
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}}'

Несколько полей сразу:

bash
docker inspect insp --format \
  'имя={{.Name}} статус={{.State.Status}} PID={{.State.Pid}} образ={{.Config.Image}}'
text
имя=/insp статус=running PID=192841 образ=nginx:alpine

Циклы:

bash
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}}'
text
--- переменные окружения ---
  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

Условия:

bash
docker inspect insp --format \
  '{{if .State.Running}}работает с {{.State.StartedAt}}{{else}}остановлен, код {{.State.ExitCode}}{{end}}'

Форматированный отчёт:

bash
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}}'
text
Имя:           /insp
Статус:        running
Образ:         nginx:alpine
Память:        268435456
Рестарт:       no

Извлечение всего состояния как JSON — удобно для дальнейшей обработки:

bash
docker inspect insp --format '{{json .State}}' | python3 -m json.tool | head -8

Несколько containers сразу:

bash
docker run -d --name insp2 alpine sleep 120 > /dev/null
docker inspect insp insp2 --format '{{.Name}} {{.State.Status}}'
docker rm -f insp2 > /dev/null

Часто используемые шаблоны стоит сохранить как алиасы:

bash
# 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}}'

События в реальном времени

bash
# в отдельном терминале: 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
text
1785405623 create ev-demo
1785405623 attach ev-demo
1785405623 start ev-demo
1785405625 die ev-demo

Полезные фильтры:

bash
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.

Метрики

bash
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
text
NAME   CPU %     MEM USAGE / LIMIT   MEM %     PIDS
load   99.87%    528KiB / 128MiB     0.40%     1

Флаг --no-stream делает один замер. Без него команда работает интерактивно и обновляется.

Процессы container с точки зрения host:

bash
docker top load -o pid,ppid,pcpu,rss,cmd
text
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 напрямую:

bash
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") байт"
text
memory.current: 540672 байт
memory.max:     134217728 байт

Цифры совпадают с выводом stats — источник тот же.

bash
docker rm -f load talker web > /dev/null

Диагностика container без оболочки

Production-образы часто не содержат оболочки — это правильная практика (раздел 11), но она ломает привычный docker exec -it ... sh.

bash
# образ без оболочки
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
text
--- обычный 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:

bash
CPID="$(docker inspect noshell --format '{{.State.Pid}}')"
echo "файловая система container:"
sudo nsenter -t "$CPID" -m ls /
echo "содержимое файла:"
sudo nsenter -t "$CPID" -m cat /data.txt
text
файловая система container:
bin  data.txt  lib
содержимое файла:
ok

2. Временный container в тех же namespaces:

bash
docker run --rm -it \
    --pid=container:noshell \
    --network=container:noshell \
    alpine ps -o pid,comm
text
PID   COMMAND
    1 /bin/sleep
   14 ps

Мы видим процессы noshell, используя утилиты Alpine. Файловая система при этом своя — для доступа к ней нужен вариант 1 или монтирование.

3. Доступ к файловой системе через /proc:

bash
sudo ls "/proc/$CPID/root/"
sudo cat "/proc/$CPID/root/data.txt"
text
bin  data.txt  lib
ok

Каталог /proc/<pid>/root — это корень файловой системы процесса с точки зрения host. Работает без nsenter и без дополнительных container.

Уборка:

bash
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.

Отчёт должен включать:

  1. Основное: имя, образ, статус, время работы, PID на host, exit code (если завершён).
  2. Конфигурацию: команда, рабочий каталог, пользователь, число переменных окружения.
  3. Ресурсы: лимит памяти, текущее потребление, CPU %, число процессов.
  4. Сеть: IP-адрес, опубликованные порты.
  5. Монтирования: тип, источник, назначение.
  6. Здоровье: статус healthcheck, если он настроен.
  7. Логи: последние 5 строк с разделением stdout и stderr.
  8. Признаки проблем: OOM, ненулевой exit code, перезапуски.

Скрипт должен корректно работать и для остановленного container.

Подсказки

Подсказка 1

Проверять наличие healthcheck нужно условно:

bash
docker inspect <c> --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}нет{{end}}'
Подсказка 2

docker stats для остановленного container вернёт ошибку. Вызывайте его только при .State.Running.

Подсказка 3

Число перезапусков доступно в .RestartCount — это прямой признак restart loop.

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/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 "  [+] проблем не обнаружено"

Проверка:

bash
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

Ожидаемый вывод (сокращённо):

text
═══ 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 не вызывается вовсе.

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

bash
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

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

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

  1. Почему процесс из docker exec не является PID 1 и что из этого следует?
  2. Почему переменная, экспортированная в entrypoint-скрипте, не видна в exec?
  3. Почему логи приложения, пишущего в файл, не появляются в docker logs?
  4. Почему docker logs <c> | grep error может ничего не найти, хотя ошибки есть?
  5. Почему MEM USAGE в docker stats больше, чем показывает само приложение?

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

  1. Как посмотреть фактическое окружение работающего приложения, включая переменные из entrypoint?
  2. Как получить только строки stderr из логов container?
  3. Как одной командой вывести имя, статус и IP-адрес нескольких containers?

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

  1. Container перезапускается каждые 30 секунд. Какие три команды дадут больше всего информации?
  2. Нужно посмотреть файловую систему container, в образе которого нет ни sh, ни ls. Назовите два способа.

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

  1. docker exec запускает новый процесс в namespaces и cgroup container, а не «входит» в него.
  2. Процесс exec не является PID 1; его завершение не влияет на container.
  3. Переменные, созданные entrypoint-скриптом, в exec не видны — читайте /proc/1/environ.
  4. Процесс exec подчиняется лимитам container и может вызвать OOM приложения.
  5. attach подключается к главному процессу и опасен в production.
  6. docker logs работает только с драйверами, поддерживающими чтение, и только со stdout и stderr.
  7. Логи удаляются вместе с container; без ротации они растут неограниченно.
  8. --format с шаблонами Go работает в inspect, ps, images, stats и других командах.
  9. docker events показывает последовательность происходящего — главный инструмент при restart loop.
  10. Container без оболочки диагностируется через nsenter, --pid=container: или /proc/<pid>/root.

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

ИсточникСсылкаЧто подтверждает
docker exec referencehttps://docs.docker.com/reference/cli/docker/container/exec/Запуск дополнительного процесса, флаги -i, -t, -e, -u, -w
docker logs referencehttps://docs.docker.com/reference/cli/docker/container/logs/Флаги --tail, --since, --until, --timestamps, -f; ограничения по драйверам
docker inspect referencehttps://docs.docker.com/reference/cli/docker/inspect/Структура вывода, шаблоны --format
Format command and log outputhttps://docs.docker.com/engine/cli/formatting/Синтаксис шаблонов Go: range, if, json, printf, index
docker stats referencehttps://docs.docker.com/reference/cli/docker/container/stats/Колонки вывода, --no-stream, источник метрик
docker top referencehttps://docs.docker.com/reference/cli/docker/container/top/Процессы container с PID host
docker events referencehttps://docs.docker.com/reference/cli/docker/system/events/Типы событий, фильтры, форматирование
Configure logging drivershttps://docs.docker.com/engine/logging/configure/Какие драйверы поддерживают docker logs
proc(5) man pagehttps://man7.org/linux/man-pages/man5/proc.5.html/proc/<pid>/environ, /proc/<pid>/root

Навигация

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

Markdown на GitHub ↗