JavaRush /Курсы /Docker for Spring /Troubleshooting: симптом → проверка

Troubleshooting: симптом → проверка

Docker for Spring
22 уровень , 4 лекция
Открыта

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,
docker logs
,
docker inspect --format ...
Правило одного изменения Между проверками меняем ровно одну вещь только порт или только 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.

Симптом Первая точка отказа / класс Первая команда Где подтверждать дальше Одна типичная правка
docker build
падает на COPY или RUN
build вывод самого docker build путь к файлу, build context, .dockerignore, Dockerfile поправить путь/контекст, а не лезть в сеть и порты
Контейнер сразу Exited startup или config
docker logs <container-name>
docker inspect по State, Entrypoint, env vars исправить путь к jar, команду запуска или невалидное значение env var
app жив, но HTTP не открывается с host network / ports
docker compose ps
docker inspect ... Ports + логи со строкой про реальный порт Boot привести ports: и SERVER_PORT к одной реальности
app не достучался до PostgreSQL / Redis / RabbitMQ network, readiness или config
docker compose logs app postgres
docker compose config, env vars, health зависимостей исправить service name / профиль / readiness-модель
Приложение стартовало «не в том режиме» config
docker compose config
docker compose exec app printenv или docker inspect .Config.Env исправить имя или значение env var / профиля
Экспорт пишет с ошибкой или файл не появляется на host permission / mount / config
docker compose 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 ещё раз.

  1. Симптом: endpoint экспорта отработал, но файла на host нет.

  2. Сначала смотрим desired model теми же файлами, с которыми реально поднимаем стек:

    docker compose -f compose.yaml -f compose.dev.yaml config
  3. Если 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)
  4. Если inspect всё ещё показывает старый target path или старое значение env, проблема уже не в YAML. Контейнер просто не пересоздан.

  5. Делаем одно изменение — явно пересоздаём только сервис app:

    docker compose -f compose.yaml -f compose.dev.yaml up -d --force-recreate app
  6. Повторяем исходную проверку: снова запускаем экспорт и смотрим, появился ли файл в ./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 — не украшение, а системная привычка.

1
Задача
Docker for Spring, 22 уровень, 4 лекция
Недоступна
Bash-playbook для single-container сценария
Bash-playbook для single-container сценария
1
Задача
Docker for Spring, 22 уровень, 4 лекция
Недоступна
Bash-playbook для Compose-стека
Bash-playbook для Compose-стека
1
Опрос
Контейнерная Диагностика, 22 уровень, 4 лекция
Недоступен
Контейнерная Диагностика
Поиск проблем в контейнерах
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ