1. Роль playbook и логов
Если бы логи всегда были короткие, ошибки — честные, а конфиги — не менялись, мы бы на этом курсе учили только команду docker logs и уходили пить чай. Но реальность другая: у вас есть несколько сервисов, несколько профилей Spring Boot, несколько Compose-файлов, и каждый кусочек может «переопределить» другой. В такой системе победить можно только порядком действий, а не героизмом.
Troubleshooting playbook — это не «бюрократия ради бюрократии», а способ сделать диагностику повторяемой. Причём не только для вас сегодняшнего, который ещё помнит, что трогал, но и для вас завтрашнего, который уже не помнит вообще ничего, потому что завтрашний вы — это немного другая личность. Playbook нужен, чтобы любой «контейнер не стартует» превращался в понятный маршрут: зафиксировали симптом → проверили одну гипотезу → внесли одно изменение → перепроверили исходную проверку.
Теперь у нас уже есть вся база для сборки маршрута: мы умеем называть симптом, находить первую точку отказа, выбирать дешёвую команду и не путать app-ошибку с Compose-ошибкой. Осталось собрать из этого короткий playbook, который одинаково полезен и для банального broken host, и для историй, где симптом уже связан с самим поведением процесса под нагрузкой.
2. Скелет playbook: симптом → класс → гипотеза → проверка
Сейчас мы сделаем главный финт дня: превратим разрозненные команды и наблюдения в алгоритм, который можно повторять почти без творчества. И это хорошо. Творчество оставим для написания кода и названий переменных. В диагностике творчество обычно заканчивается фразой «ну я уже всё попробовал».
Начнём с того, что у playbook есть «позвоночник» — шесть шагов. Он специально короткий: если алгоритм длиннее, чем рецепт борща, им никто не пользуется. Важная деталь: мы не обещаем, что этот playbook чинит всё. Он обещает другое — что вы быстро поймёте, куда смотреть дальше, и перестанете менять три вещи одновременно.
Вот его удобно представить как блок-схему:
flowchart TD
A["Симптом: что именно не работает?"] --> B["Первая точка отказа: build / start / init / call"]
B --> C["Класс проблемы: build / startup / config / network / permission"]
C --> D["Одна гипотеза: думаю, причина вот в этом"]
D --> E["Одна проверка: logs / inspect / config / exec"]
E --> F{"Гипотеза подтвердилась?"}
F -- да --> G["Одно изменение"]
G --> H["Перепроверка исходного failing-check"]
F -- нет --> I["Новая гипотеза без накопления случайных правок"]
Чтобы это не было абстракцией, давайте закрепим термины в маленькой таблице. Она заменит нам десять буллетов и позволит не путаться.
| Термин | Что это значит «по-человечески» | Пример из нашего Boot+Compose мира |
|---|---|---|
| Симптом | Первый наблюдаемый факт, без объяснений | curl http://localhost:8080/actuator/health не отвечает |
| Первая точка отказа | Где впервые стало плохо | «сборка упала на COPY», «контейнер стартанул и сразу умер», «приложение живое, но БД недоступна» |
| Класс проблемы | Тип поломки (помогает выбрать инструмент) | network vs config очень часто путают |
| Гипотеза | Одно конкретное предположение | «порт проброшен не туда» или «в SPRING_PROFILES_ACTIVE опечатка» |
| Подтверждающая проверка | Дешевая команда, которая подтверждает или снимает гипотезу | docker compose config, , |
| Правило одного изменения | Между проверками меняем ровно одну вещь | только порт или только host, но не «всё подряд» |
Заметьте, мы специально держимся близко к наблюдаемому поведению. Это важный навык для backend-разработчика: не придумывать объяснение раньше, чем есть факт. Иначе мозг начинает «достраивать реальность» — примерно как Spring Boot достраивает вам автоконфигурацию, только мозг делает это без документации.
3. Мини-playbook для одного контейнера: когда «оно просто не работает»
В одиночном контейнере всё проще, но именно поэтому там особенно легко впасть в самоуверенность. Кажется: «ну это же один контейнер, сейчас за 30 секунд починю». А потом проходит час, и вы уже читаете про шаманские танцы вокруг ENTRYPOINT. Поэтому даже для single-container запуска полезно иметь короткий маршрут, который вы делаете почти на автопилоте.
Начинать стоит с вопроса: контейнер вообще запускался и что с ним стало? Здесь идеально подходит «тройка» команд: увидеть список контейнеров, прочитать логи, подтвердить фактическую конфигурацию.
Ниже <container-name> — это условное имя контейнера. Если сервис поднят через Compose, контейнер сервиса app удобно получать через $(docker compose ps -q app) и вообще не зависеть от generated names.
docker ps -a # видим: контейнер вообще запускался? какой статус (Exited/Up)?
docker logs <container-name> # читаем: почему упал/что происходило при старте
docker inspect <container-name> # подтверждаем: как реально запущен (env, ports, mounts, entrypoint)
Смысл такой последовательности в том, что logs часто сразу показывает первопричину (например, приложение не нашло jar, не смогло распарсить параметр или упало из-за отсутствующего файла). inspect нужен, когда логов недостаточно и надо подтвердить, чем контейнер реально запущен: какой Entrypoint, какие env vars, какие порты опубликованы, какие mounts подключены.
Если вы чувствуете соблазн сразу сделать docker exec -it <container-name> sh, остановитесь на секунду. exec — это как вскрыть системный блок и начать пальцем щупать проводки. Иногда нужно, но сначала всё же логичнее посмотреть, что говорит система сама о себе. Как правило, exec нужен тогда, когда у вас уже есть конкретная гипотеза вида «переменная окружения не дошла» или «файл реально отсутствует».
Например, если гипотеза про env vars, проверка должна быть короткой и нацеленной:
docker exec <container-name> printenv | grep SPRING # проверяем, что нужные переменные реально внутри контейнера
# SPRING_PROFILES_ACTIVE=postgres,cache
А если гипотеза про файл (условно, jar не там), то проверка должна быть про файл, а не «давайте просто откроем шелл и посмотрим»:
docker exec <container-name> ls -la /app # проверяем, что нужный файл вообще существует в ожидаемом пути
# ... app.jar
Эта точечность экономит время: вы не превращаете диагностику в туризм по файловой системе контейнера. Потому что контейнер — не музей, там нечего смотреть, пока вы не знаете, что ищете.
4. Compose-playbook: config, actual state и логи нескольких сервисов
Compose — это место, где ошибка особенно любит маскироваться под «сломался Spring Boot». Но в multi-container среде у вас появляется ещё один слой: нужно раздельно увидеть resolved-модель запуска, actual state контейнеров и цепочку логов нескольких сервисов. Иначе очень легко чинить приложение там, где на самом деле сломан YAML, override или readiness зависимости.
Стартовый маршрут в Compose обычно выглядит так:
# Если стек поднимается несколькими файлами, здесь используйте тот же набор `-f`, что и в `up`
docker compose config
docker compose ps
docker compose logs app postgres
docker compose config показывает desired model: что Compose соберёт из файлов, override и .env. docker compose ps и docker inspect показывают actual state: какие контейнеры уже созданы, в каком они состоянии и что в них реально применено. Если вы поправили YAML, config уже красивый, а контейнер продолжает жить со старым env или mount, это не магия — сервис надо пересоздать и только потом повторить исходный failing-check.
# Если меняли конфигурацию app и хотите применить её к уже созданному контейнеру
docker compose up -d --force-recreate app
Логи нескольких сервисов нужны не ради количества текста. app обычно показывает симптом, а зависимость — причину, поэтому в Compose полезно сразу читать хотя бы пару «клиент + зависимость».
docker compose logs --tail=50 app postgres
docker compose logs -f app postgres
docker compose exec остаётся полезным, но только когда контейнер стабильно живёт и вы проверяете конкретную гипотезу.
# Для running-контейнера
docker compose exec app env | grep SPRING
# Для fast-fail сценария, когда `exec` уже не успевает
docker inspect --format '{{json .Config.Env}}' $(docker compose ps -q app)
5. Правило одного изменения
Сейчас будет самая «не техническая», но самая спасительная часть. Когда что-то не работает, руки начинают чесаться сделать сразу три правки: поменять порт, переписать SPRING_DATASOURCE_URL, добавить depends_on, и ещё на всякий случай docker compose down -v, чтобы «всё точно чисто». Знакомо? Поздравляю, вы человек.
Проблема в том, что после трёх изменений вы теряете причинно-следственную связь. Оно могло заработать из-за первого изменения, могло не заработать из-за второго, а третье вообще могло быть лишним и теперь будет аукаться через неделю. Поэтому playbook держится на дисциплине: между двумя проверками меняем одну вещь. В этом смысле диагностика похожа на unit-тест: вы меняете один вход и смотрите один результат.
Практически это выглядит так: у вас есть failing-check, например curl на /actuator/health не отвечает. Вы меняете только публикацию порта, и снова повторяете тот же curl. Если стало отвечать — вы знаете, что причина была в портах. Если нет — портовая гипотеза снята, и вы не «засорили» систему ещё двумя случайными изменениями.
6. Короткие кейсы: симптом → перепроверка
В реальной работе удобнее держать рядом не три длинные истории, а одну lookup-матрицу. Длинные разборы полезны в первый раз, а рядом с терминалом обычно нужна короткая версия, которая быстро возвращает в playbook.
| Симптом | Первая точка отказа / класс | Первая команда | Где подтверждать дальше | Одна типичная правка |
|---|---|---|---|---|
падает на COPY или RUN |
build | вывод самого docker build | путь к файлу, build context, .dockerignore, Dockerfile | поправить путь/контекст, а не лезть в сеть и порты |
| Контейнер сразу Exited | startup или config | |
docker inspect по State, Entrypoint, env vars | исправить путь к jar, команду запуска или невалидное значение env var |
| app жив, но HTTP не открывается с host | network / ports | |
docker inspect ... Ports + логи со строкой про реальный порт Boot | привести ports: и SERVER_PORT к одной реальности |
| app не достучался до PostgreSQL / Redis / RabbitMQ | network, readiness или config | |
docker compose config, env vars, health зависимостей | исправить service name / профиль / readiness-модель |
| Приложение стартовало «не в том режиме» | config | |
docker compose exec app printenv или docker inspect .Config.Env | исправить имя или значение env var / профиля |
| Экспорт пишет с ошибкой или файл не появляется на host | permission / mount / config | |
docker inspect .Mounts, APP_EXPORT_DIR, права на target dir | выровнять mount target и путь в приложении, при нужде пересоздать сервис |
6.1. Один полный прогон: YAML уже правильный, а контейнер всё ещё старый
Представьте: экспорт должен писать файлы в ./data/exports. Вы уже поправили APP_EXPORT_DIR в compose.dev.yaml, но на host всё равно пусто. Это тот самый момент, где надо развести desired model и actual state, а не переписывать YAML ещё раз.
-
Симптом: endpoint экспорта отработал, но файла на host нет.
-
Сначала смотрим desired model теми же файлами, с которыми реально поднимаем стек:
docker compose -f compose.yaml -f compose.dev.yaml config -
Если resolved-конфигурация уже показывает правильные APP_EXPORT_DIR и volumes, сравниваем её с actual state контейнера:
docker inspect --format '{{json .Mounts}}' $(docker compose -f compose.yaml -f compose.dev.yaml ps -q app) docker inspect --format '{{json .Config.Env}}' $(docker compose -f compose.yaml -f compose.dev.yaml ps -q app) -
Если inspect всё ещё показывает старый target path или старое значение env, проблема уже не в YAML. Контейнер просто не пересоздан.
-
Делаем одно изменение — явно пересоздаём только сервис app:
docker compose -f compose.yaml -f compose.dev.yaml up -d --force-recreate app -
Повторяем исходную проверку: снова запускаем экспорт и смотрим, появился ли файл в ./data/exports.
6.2. Оформление playbook в репозитории
Сейчас мы не будем превращать playbook в роман, потому что у нас уже есть один роман — compose.yaml после пары месяцев жизни. Playbook — это короткий документ, который помогает быстро воспроизвести диагностику и не забыть важные проверки. Идеально, если он живёт либо в README.md как раздел Troubleshooting, либо в отдельном файле вроде docs/troubleshooting-playbook.md и на него есть ссылка из README.
Полезный формат — не список команд, а шаблон «карточки инцидента». Он заставляет вас фиксировать симптом, гипотезу и перепроверку, то есть удерживает в рамках алгоритма. Пример шаблона — минимальный и читабельный:
### Симптом
`curl http://localhost:8080/actuator/health` не отвечает.
### Класс проблемы
Network / Ports.
### Гипотеза
В Compose опубликован не тот порт.
### Проверка
`docker compose config` → смотрю секцию `ports`.
### Исправление (одно)
Меняю `8080:8081` на `8080:8080`.
### Перепроверка
Повторяю `curl` на `/actuator/health`.
Обратите внимание, что здесь нет «магии» и нет лишних ветвлений. Это и есть «короткий, но рабочий» playbook. Он не заменяет мышление, но не даёт этому мышлению превратиться в хаотичное переключение между вкладками и командами.
Для Compose-спорных случаев удобно дописать ещё две короткие строки: Resolved config (docker compose config) и Actual container state (docker inspect / docker compose exec printenv). Это помогает сразу увидеть, где вы спорите с YAML, а где — со старым контейнером.
Если вы используете несколько Compose-файлов, отдельной строчкой стоит зафиксировать, что перед любой диагностикой вы смотрите merged-конфигурацию через docker compose config. Docker прямо рекомендует этот приём, чтобы видеть итоговую конфигурацию и избегать проблем с путями и overrides.
7. Типичные ошибки при работе по troubleshooting playbook
Ошибка №1: playbook превращают в энциклопедию «на всякий случай».
Самая частая ошибка — превращать playbook в энциклопедию команд «на всякий случай». В итоге документ становится слишком длинным, его никто не читает, а вы снова возвращаетесь к хаосу. Playbook должен оставаться коротким: симптом, гипотеза, одна проверка, одно изменение, перепроверка.
Ошибка №2: пропускают перепроверку.
Вторая типичная ошибка — пропускать перепроверку. Исправление без перепроверки — это как git commit без запуска тестов: вроде вы что-то сделали, но уверенности нет. Перепроверка должна повторять именно исходный failing-check, иначе вы легко «почините не то» и получите фальшивое чувство победы.
Ошибка №3: делают несколько изменений подряд.
Третья ошибка — делать несколько изменений подряд. Это ломает диагностику даже тогда, когда вы очень опытны, а для новичка превращает процесс в лотерею. Если очень хочется поменять три вещи, это хороший сигнал, что вы не сформулировали гипотезу достаточно конкретно.
Ошибка №4: доверяют фрагменту YAML вместо resolved модели.
Четвёртая ошибка — доверять фрагменту YAML вместо resolved модели. В Compose-мире особенно при нескольких файлах правду показывает не то, что вы «помните», и не то, что «написано в одном файле», а то, что Compose реально собрал и применил. Поэтому docker compose config — не украшение, а системная привычка.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ