Debug checklist
В прошлых уроках мы по отдельности разобрали logs, exec, inspect, stats. Этот урок — сводный чек-лист «контейнер не работает, что делать», который ты можешь распечатать и повесить рядом с монитором.
Сигналы и kill: как правильно убивать процессы
Алгоритм: 7 шагов
Когда DAG в Airflow не запускается, app не отвечает на запросы, или Postgres-контейнер постоянно рестартится — иди по шагам:
Шаг 1: docker ps -a — статус и exit code
docker ps -a --filter name=app
CONTAINER ID IMAGE COMMAND STATUS NAMES
7f3e1a2b3c4d my-etl ... Exited (1) 5 seconds ago app
Что смотрим:
- Up — контейнер работает, проблема внутри (приложение в нём не отвечает).
Exited (<code>) <time> ago— упал, время и код подсказывают причину.Restarting (<code>)— крутится в цикле рестартов.- Created — был создан, но не запущен.
Шаг 2: docker logs --tail 100 — последние строки
docker logs app --tail 100 --timestamps
Ищешь:
- Traceback / Error / Exception в последних строках.
- Сообщение «died» или «killed».
- Postgres-сообщения типа
FATAL: password authentication failed.
90% Junior-проблем закрывается на этом шаге. Если в логах ничего полезного — идём дальше.
Шаг 3: docker inspect для деталей State
docker inspect app --format 'Status: {{.State.Status}}, Exit: {{.State.ExitCode}}, OOM: {{.State.OOMKilled}}, Error: {{.State.Error}}'
Получаем что-то вроде:
Status: exited, Exit: 137, OOM: true, Error:
OOM: true = это был OOM. Exit: 1 без OOM = ошибка в приложении. Exit: 125 = ошибка Docker daemon (например, —memory указан некорректно). Error: ... = низкоуровневая проблема runtime.
Шаг 4: docker exec — если контейнер запущен
Если статус Up, но приложение «не отвечает» — лезем внутрь:
docker exec -it app sh
Что проверяем:
- Главный процесс крутится?
ps aux | head - Сеть работает?
curl localhost:8000/health - Файлы на месте?
ls -la /app - Env-переменные правильные?
env | grep DATABASE
Шаг 5: Healthcheck
Если в Dockerfile/compose есть HEALTHCHECK:
docker inspect app --format '{{.State.Health.Status}}'
# unhealthy
docker inspect app --format '{{json .State.Health.Log}}' | jq
# [{"Start": "...", "ExitCode": 1, "Output": "curl: (7) Failed to connect..."}]
Health.Log хранит последние ~5 проверок с выводом. Это часто даёт точную причину — например, healthcheck стучит в localhost:8000, а приложение слушает 0.0.0.0:8080.
Шаг 6: Ресурсы (docker stats)
Если контейнер «тормозит», но не падает:
docker stats --no-stream app
CPU% > 100% постоянно или MEM% > 80% — ресурсная проблема. CPU% низкий, MEM% низкий — проблема не в ресурсах, а в IO/network/коде.
Шаг 7: Network (docker network inspect)
Если контейнер не достучаться (DNS, connection refused):
docker network inspect <network-name>
docker exec app nslookup postgres
docker exec app curl -v postgres:5432
Стандартный case: app в одной сети, postgres в другой — connection refused. Чинится тем, что оба сервиса должны быть в одной user-defined bridge.
Exit codes — словарь
Знание типичных exit code’ов экономит время:
| Code | Что значит | Где искать причину |
|---|---|---|
| 0 | Нормальный выход. Процесс сам завершился успешно | Не ошибка, контейнер сделал свою работу и вышел |
| 1 | Generic application error | docker logs — traceback в приложении |
| 2 | Misuse of shell builtins (часто) | Bash-скрипт упал на синтаксисе |
| 125 | Docker daemon error: бракованные флаги | docker run --xyz=abc — Docker не понимает флаг |
| 126 | Контейнерная команда не исполняемая | chmod +x забыл, или скрипт от Windows с \r\n |
| 127 | Команда не найдена | CMD ссылается на бинарь, которого нет в PATH |
| 130 | Killed by SIGINT (Ctrl+C) | Пользователь нажал Ctrl+C |
| 137 | Killed by SIGKILL (128+9) | OOM Killer или docker kill |
| 139 | Segmentation fault (128+11) | Баг в C/C++ или Rust unsafe; native-lib проблема |
| 143 | Killed by SIGTERM (128+15) | docker stop (мягкое завершение) — норма |
Формула 128 + signal_number объясняет 130, 137, 139, 143:
- SIGINT = 2 -> exit 130
- SIGKILL = 9 -> exit 137
- SIGSEGV = 11 -> exit 139
- SIGTERM = 15 -> exit 143
Типичные сценарии
Сценарий 1: «Postgres не стартует, exit 1»
docker logs pg --tail 50
# Error: Database is uninitialized and superuser password is not specified.
Забыл POSTGRES_PASSWORD. Чинится через -e POSTGRES_PASSWORD=....
Сценарий 2: «App рестартится в цикле, exit 137»
docker inspect app --format '{{.State.OOMKilled}}'
# true
OOM. Чинится --memory лимитом + оптимизация кода (batching, lazy loading).
Сценарий 3: «App запущен, но HTTP не отвечает»
docker ps # Up 10 minutes
docker logs app --tail 20 # ничего криминального
docker exec app curl -v localhost:8000/health # connection refused
docker exec app ss -tlnp # ничего на :8000
Приложение упало без выхода (зависло), или слушает не тот порт. Смотри ENV (часто PORT env var), или код.
Сценарий 4: «App не видит Postgres»
docker logs app --tail 20
# OperationalError: could not translate host name "postgres" to address
DNS не резолвится. Проверяем сети:
docker inspect app --format '{{json .NetworkSettings.Networks}}'
# {"bridge": ...}
docker inspect pg --format '{{json .NetworkSettings.Networks}}'
# {"my-app_default": ...}
Разные сети — отсюда невидимость. Чинится compose-файлом (оба в одном файле автоматически в общей сети) или ручным docker network connect.
Сценарий 5: «Healthcheck unhealthy, но приложение работает»
docker inspect app --format '{{json .State.Health.Log}}' | jq '.[-1]'
# {"Start": "...", "ExitCode": 1, "Output": "curl: (7) Failed to connect to localhost port 8000"}
Healthcheck стучится на 8000, приложение на 8080. Чинится в Dockerfile (HEALTHCHECK правильным URL).
Anti-patterns
Что не делать при debug:
docker restartсразу, не посмотрев логи. Перезапуск часто маскирует проблему. Особенно вреденrestart: alwaysдля приложений с багами — оно крутится бесконечно, тратя ресурсы.docker logsбез--tail. На production-контейнере с гигабайтным логом ты получишь миллион строк в терминал — терминал зависнет, инцидент усугубится.- Удалить контейнер и заново запустить «авось заработает». Часто это убирает state, и ты теряешь возможность дебага. Сначала собери информацию (
logs,inspect), потом перезапускай. - Игнорировать exit code 0. Если init-контейнер вышел с 0 после миграций — это успех, а не проблема. Junior иногда видит «контейнер не Up» и пытается рестартить — а он не должен быть Up.
docker compose-versions команд
В compose-стенде те же команды доступны на уровне сервисов:
docker compose ps # = docker ps для сервисов
docker compose logs -f app # = docker logs -f
docker compose top # = docker top для всех сервисов
docker compose events # = docker events с фильтром
docker compose exec app sh # = docker exec -it
Удобнее, чем docker ... --filter, и работает с именами сервисов из compose-файла.
Попробуй сам
Создай 5 «сломанных» контейнеров и пройдись по чек-листу для каждого:
- Postgres без POSTGRES_PASSWORD:
docker run -d --name lab1 postgres:16. Что вdocker ps -a, что вlogs? - CMD не существует:
docker run -d --name lab2 alpine notacommand. Какой exit code? - OOM:
docker run -d --name lab3 --memory 64m python:3.13-slim python -c "x = ' ' * 200_000_000; print(len(x))". Что вinspect State.OOMKilled? - Контейнер запущен, но «зависший»:
docker run -d --name lab4 alpine sh -c "sleep 100". Сделайdocker ps(Up),docker top lab4(sleep),docker exec -it lab4 sh(зайди внутрь). - Сеть: создай две сети, запусти Postgres в одной, app-контейнер в другой, попробуй подключиться. Используй
network inspectиexec ... ping.
После каждого случая запиши себе:
- На каком шаге чек-листа ты нашёл причину?
- Какая команда дала максимум информации?