Debugging checklist
Последовательности проверок по симптомам. Каждая — упорядоченный список команд, сужающих область поиска, а не перебирающих всё подряд.
Правило, общее для всех списков: проверять командой, а не рассуждением. Гипотеза, не проверенная командой, стоит дешевле, чем время, потраченное на её обдумывание.
Как пользоваться
- Найти симптом в таблице входов.
- Пройти список по порядку: каждый шаг отсекает часть гипотез.
- Остановиться, когда причина найдена, — остальные шаги не выполнять.
Если симптома нет в таблице — начать с общего входа.
Таблица входов
| Симптом | Список |
|---|---|
| Container сразу завершается | 1 |
| Container исчез через часы работы | 2 |
| Логи пусты | 3 |
| Сервис недоступен снаружи | 4 |
| Один сервис не видит другой | 5 |
| Healthcheck красный | 6 |
| Отказ в правах при записи | 7 |
| Сборка стала медленной | 8 |
| Кончилось место на диске | 9 |
| Работает не тот код | 10 |
| Остановка занимает 10 секунд | 11 |
Общий вход
Три команды, с которых начинается любое расследование:
docker ps -a --filter name=ИМЯ --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker inspect ИМЯ --format '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}}'
docker logs --tail 50 ИМЯ
Они отвечают на вопросы «работает ли», «как завершился», «что сказал». Дальше — по симптому.
1. Container сразу завершается
# 1. С ошибкой или сделал работу и вышел?
docker inspect ИМЯ --format '{{.State.ExitCode}}'
| Код | Дальше |
|---|---|
| 0 | Процесс закончил работу. Это не отказ — см. шаг 2 |
| 1, 2 | Ошибка приложения — шаг 3 |
| 125 | Ошибка самого docker run — проверить аргументы |
| 126 | Команда найдена, но не исполняемая — права на файл |
| 127 | Команда не найдена — опечатка или нет в образе |
| 137 | SIGKILL, часто OOM — см. список 2 |
# 2. Код 0: приложение действительно завершилось само
docker logs ИМЯ
# 3. Ненулевой код: что сказало приложение
docker logs ИМЯ 2>&1 | tail -30
# 4. Точка входа: что вообще запускается
docker image inspect ОБРАЗ --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}'
# 5. Проверить руками
docker run --rm -it --entrypoint sh ОБРАЗ
Частые причины: скрипт без бесконечной работы; опечатка в команде; отсутствие файла, ожидаемого приложением; неверная форма ENTRYPOINT.
2. Container исчез через часы работы
# 1. Убит ядром?
docker inspect ИМЯ --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
Если 137 true — OOM killer. Логов приложения не будет: SIGKILL не перехватывается.
# 2. Подтверждение со стороны ядра
dmesg | grep -i 'killed process' | tail -5
docker events --filter 'event=oom' --since 24h
# 3. Давление памяти до отказа
docker exec ИМЯ cat /sys/fs/cgroup/memory.events
max 4213 ← процесс упирался в предел 4213 раз
oom_kill 3 ← убит 3 раза
# 4. Какой лимит применён на самом деле
docker exec ИМЯ cat /sys/fs/cgroup/memory.max
docker inspect ИМЯ --format '{{.HostConfig.Memory}}'
Расхождение между этими двумя означает, что запрос не выполнился.
Три гипотезы и как их различить:
| Гипотеза | Признак |
|---|---|
| Утечка | Потребление растёт монотонно между перезапусками |
| Лимит занижен | Выходит на плато выше лимита |
| Всплеск на редком запросе | Стабильно, скачок перед отказом |
Поднимать лимит, не различив, — обычная ошибка: при утечке это лишь отодвигает отказ.
3. Логи пусты
# 1. Приложение вообще работает?
docker inspect ИМЯ --format '{{.State.Status}}'
docker top ИМЯ
# 2. Буферизация вывода — самая частая причина у Python
docker inspect ИМЯ --format '{{json .Config.Env}}' | grep -o PYTHONUNBUFFERED=. || echo "не задан"
# 3. Проверить гипотезу
docker run --rm -e PYTHONUNBUFFERED=1 ОБРАЗ
# 4. Приложение пишет в файл вместо stdout?
docker diff ИМЯ | grep -i log
# 5. Драйвер журналирования сохраняет вывод?
docker inspect ИМЯ --format '{{.HostConfig.LogConfig.Type}}'
Драйвер none или внешний сборщик означает, что docker logs не покажет ничего по устройству.
4. Сервис недоступен снаружи
# 1. Порт действительно опубликован?
docker port ИМЯ
# 2. Слушает ли приложение — и на каком адресе
docker exec ИМЯ python -c "
import socket
for addr in ('127.0.0.1', '0.0.0.0'):
s = socket.socket(); s.settimeout(1)
print(addr, 'открыт' if s.connect_ex((addr, 8000)) == 0 else 'закрыт')"
| Результат | Вывод |
|---|---|
127.0.0.1 открыт, 0.0.0.0 нет | Слушает петлю — исправить адрес привязки |
| оба закрыты | Приложение не слушает этот порт |
| оба открыты | Проблема в публикации или правилах хоста |
# 3. Что приложение сказало при старте
docker logs --tail 30 ИМЯ | grep -i -E 'listen|bind|serving|started'
# 4. Дошёл ли запрос до container'а
docker logs -f ИМЯ &
curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/
Запрос не появился в логах — не дошёл; появился с ошибкой — дошёл, проблема в приложении.
Подробнее: networking cheat sheet.
5. Один сервис не видит другой
# 1. В одной ли сети
docker inspect ПЕРВЫЙ --format '{{json .NetworkSettings.Networks}}'
docker inspect ВТОРОЙ --format '{{json .NetworkSettings.Networks}}'
# 2. Разрешается ли имя
docker exec ПЕРВЫЙ getent hosts ВТОРОЙ
# 3. Доступен ли порт
docker exec ПЕРВЫЙ python -c "
import socket
s = socket.create_connection(('ВТОРОЙ', 5432), 2); s.close(); print('доступен')"
# 4. Что в строке подключения
docker inspect ПЕРВЫЙ --format '{{json .Config.Env}}' | tr ',' '\n' | grep -i url
| Что нашлось | Причина |
|---|---|
| Разные сети | Подключить к общей |
Сеть по умолчанию bridge | В ней нет разрешения имён |
| Имя разрешается, порт закрыт | Сосед не слушает или ещё не готов |
В строке localhost | Указывает на сам container |
6. Healthcheck красный
# 1. Что именно вернула проверка
docker inspect ИМЯ --format '{{json .State.Health}}' | python3 -m json.tool
ExitCode | Причина |
|---|---|
| 127 | Команды нет в образе — обычно curl в slim |
| 124 | Истёк timeout |
| 1 | Проверка выполнилась и вернула ошибку |
# 2. Выполнить проверку вручную
docker exec ИМЯ python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8000/healthz').status)"
# 3. Достаточно ли start-period
docker image inspect ОБРАЗ --format '{{json .Config.Healthcheck}}'
docker logs ИМЯ | head -20 # сколько занимает старт
Четвёртая причина, не видная в кодах: проверка обращается к зависимости. Тогда отказ базы делает нездоровым приложение, и перезапуск ничего не чинит (урок 11.3).
7. Отказ в правах при записи
# 1. От кого работает процесс
docker exec ИМЯ id
# 2. Кому принадлежит каталог
docker exec ИМЯ ls -ldn /путь
# 3. Что задано в образе
docker image inspect ОБРАЗ --format '{{.Config.User}}'
# 4. Что смонтировано
docker inspect ИМЯ --format '{{json .Mounts}}' | python3 -m json.tool
| Что нашлось | Исправление |
|---|---|
Владелец 0 0, процесс не root | Создать каталог в образе с нужным владельцем |
| Том создан раньше со старыми правами | docker volume rm либо разовый chown |
| Bind mount, UID хоста не совпадает | -u "$(id -u):$(id -g)" |
read_only без tmpfs | Добавить tmpfs для путей записи |
Подробнее: storage cheat sheet.
8. Сборка стала медленной
# 1. Где кэш перестаёт работать
docker build -t проба . 2>&1 | grep -n -E 'CACHED|^#[0-9]+ \[' | head -30
Первая инструкция без CACHED — та, что сбросила кэш.
# 2. Проверка по времени
docker build -t проба . && touch src/*.py && time docker build -t проба .
Секунды — кэш работает; минуты — нет.
# 3. Размер контекста сборки
docker build . 2>&1 | head -3 # строка «transferring context»
cat .dockerignore || echo ".dockerignore отсутствует"
| Причина | Исправление |
|---|---|
COPY . . до установки зависимостей | Разделить копирование |
Нет .dockerignore | Исключить .git, кэши, окружения |
Меняется ARG до FROM | Переместить ниже |
В CI mode=min | mode=max в cache-to |
9. Кончилось место на диске
# 1. Что занимает
docker system df -v | head -20
# 2. Логи container'ов — их в system df НЕТ
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail -5
# 3. Кэш сборки
docker builder du 2>/dev/null || docker system df --format '{{.BuildCache}}'
Очистка — по возрастанию опасности:
docker builder prune # кэш сборки: безопасно
docker image prune # безымянные образы: почти безопасно
docker container prune # остановленные container'ы
docker volume prune # ДАННЫЕ: посмотреть список до запуска
Не выполнять в эксплуатации: docker system prune -a удаляет образы, на которых можно откатиться.
Подробнее: cleanup checklist.
10. Работает не тот код
# 1. На каком образе работает container
docker inspect ИМЯ --format '{{.Image}}'
# 2. На что сейчас указывает тег
docker image inspect ОБРАЗ:ТЕГ --format '{{.Id}}'
Разные значения — container не пересоздан после pull.
# 3. Какой digest у локального образа
docker image inspect ОБРАЗ:ТЕГ --format '{{index .RepoDigests 0}}'
# 4. Какой digest в registry сейчас
docker buildx imagetools inspect ОБРАЗ:ТЕГ | grep -i digest
| Что нашлось | Причина |
|---|---|
| Образ container'а ≠ образ тега | pull был, пересоздания не было |
| Локальный digest ≠ digest в registry | Тег перезаписан после pull |
| Оба совпадают, код всё равно не тот | Опубликовано не то, что собрано |
Устраняет весь класс: развёртывание по digest (урок 14.4).
11. Остановка занимает 10 секунд
# 1. Кто такой PID 1
docker exec ИМЯ ps -eo pid,ppid,cmd
PID PPID CMD
1 0 /bin/sh -c python app.py ← оболочка: сигнал не дойдёт
7 1 python app.py
# 2. Форма точки входа в образе
docker image inspect ОБРАЗ --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}'
# 3. Замер
docker run -d --name t ОБРАЗ && sleep 2 && time docker stop t
| Что нашлось | Причина |
|---|---|
PID 1 — sh | Shell-форма CMD или ENTRYPOINT |
| PID 1 — приложение | Нет обработчика SIGTERM в коде |
| Точка входа — скрипт-обёртка | Нет exec перед вызовом приложения |
Различить первое и второе можно только шагом 1 — симптом одинаков.
Что делать, если ничего не помогло
| Приём | Команда |
|---|---|
| Полный набор сетевых утилит | docker run --rm -it --network container:ИМЯ nicolaka/netshoot |
| Что записано в слой | docker diff ИМЯ |
| Все события за час | docker events --since 1h --filter container=ИМЯ |
| Показания ядра изнутри | docker exec ИМЯ cat /sys/fs/cgroup/memory.events |
| Сравнить с запуском без container'а | Запустить то же приложение на хосте |
Последняя строка недооценена: если вне container'а симптом воспроизводится, искать нужно в приложении, а не в Docker. Это отсекает половину гипотез одной проверкой.
Навигация
Вернуться к справочникам
Docker CLI cheat sheet
Networking cheat sheet
Debugging challenges
Диагностическая таблица
Главное оглавление