1. «Контейнер запущен» ≠ «сервис работает»
Контейнер в docker ps — ещё не доказательство того, что сервис действительно работает. Контейнер — это всего лишь «коробка» вокруг процесса. Он может быть запущен, а приложение внутри ещё стартует, упало при инициализации контекста, слушает не тот порт или вообще зависло на старте. Поэтому вводим простое правило: успех — это не docker run, успех — это HTTP-ответ + внятные стартовые логи.
Давайте зафиксируем минимальную модель проверки, чтобы она стала не разовой магией, а рефлексом.
На уровне первого запуска сервис считается «живым», когда выполняются два условия. Во-первых, служебный endpoint отвечает — это быстрый индикатор, что приложение поднялось и хотя бы базовая инфраструктура Spring Boot работает. Во-вторых, прикладной endpoint тоже отвечает — это подтверждение, что поднялся не только HTTP-слой, но и реально работает ваш контроллер и ваш код, а не просто «заглушка здоровья». И параллельно мы читаем логи, чтобы понимать, что происходило при старте, а не гадать по симптомам.
Вот как это выглядит в виде маленькой схемы:
flowchart TD
%% Сначала проверяем факт HTTP-ответа, а потом уже углубляемся в детали
A[Контейнер запущен] --> B{"HTTP отвечает?"}
B -->|Нет| C[Смотрим docker ps + порты + docker logs]
B -->|Да| D{"Health OK? /actuator/health"}
D -->|Нет| E[Читаем логи: что не поднялось]
D -->|Да| F{"Бизнес endpoint OK? /api/catalog/items"}
F -->|Нет| G[Логи + проверка правильного URL]
%% Если дошли сюда — для текущего шага считаем сервис живым
F -->|Да| H[Сервис считается живым для текущего шага]
Обратите внимание на важную психологическую штуку: эта схема специально устроена так, чтобы вы не «тыкались» случайными командами. Сначала наблюдаем, потом проверяем, потом читаем симптомы.
2. Smoke-check №1: проверяем /actuator/health
Первый smoke-check должен быть быстрым, предсказуемым и не зависеть от демо-данных или сложной бизнес-логики. Поэтому начинаем с Actuator endpoint /actuator/health. В нашем учебном сервисе он уже есть как часть операционного baseline, и его задача проста: сказать «приложение живо» в понятном JSON-формате. Это не про красоту, а про диагностику.
Сначала убедимся, что контейнер вообще запущен и у него то же имя, с которым мы его поднимали: catalog-service.
# Проверяем, что контейнер действительно запущен и порт проброшен наружу
docker ps
# CONTAINER ID NAMES STATUS PORTS
# 4c2a... catalog-service Up 15 seconds 0.0.0.0:8080->8080/tcp
Теперь делаем запрос на health. Если у вас есть curl, используем его:
# Smoke-check: проверяем, что Actuator отвечает и приложение вообще поднялось
curl http://localhost:8080/actuator/health
# {"status":"UP"}
Если вы на Windows и curl ведёт себя неожиданно, потому что PowerShell иногда подсовывает свой алиас, можно использовать Invoke-RestMethod:
# Для PowerShell это удобный «родной» способ сделать HTTP-запрос
Invoke-RestMethod http://localhost:8080/actuator/health
# status
# ------
# UP
Смысл ответа на сегодня максимально конкретный. Нас не интересует «всё ли идеально» — это будет позже. Нас интересует: есть ли ответ вообще и есть ли в нём UP.
Чуть расширим проверку: иногда полезно сразу видеть HTTP-статус. Для этого можно добавить флаг -i, который покажет заголовки:
# -i показывает статус и заголовки — удобно сразу увидеть 200/401/503 и т.д.
curl -i http://localhost:8080/actuator/health
# HTTP/1.1 200
# Content-Type: application/vnd.spring-boot.actuator.v3+json
#
# {"status":"UP"}
Если вы получили 200 и UP, это означает, что приложение уже подняло веб-слой и базовая инфраструктура Spring Boot внутри контейнера жива. Если вы получили Connection refused, это не «Actuator сломан», а сигнал, что ваш компьютер не смог подключиться к порту: либо контейнер не запущен, либо порт не опубликован, либо приложение ещё не поднялось, либо вы проверяете не тот порт.
И да, на старте бывает совершенно нормальная ситуация: контейнер уже запущен, а приложение ещё грузится. Тогда curl может не успеть с первого раза. Это не повод переустанавливать Docker или уходить в философию. Просто подождите несколько секунд и повторите запрос. Spring Boot — штука бодрая, но не телепатическая: ему нужно время, чтобы развернуть контекст.
3. Smoke-check №2: проверяем /api/catalog/items
После health делаем вторую проверку — прикладной endpoint. В нашем проекте это GET /api/catalog/items. Он хорош тем, что сразу проверяет несколько вещей: работает маршрутизация, поднялся контроллер, сервисный слой не падает на старте и ответ сериализуется в JSON. Для первого запуска этого более чем достаточно: мы не строим тестовую пирамиду, а оказываем первую помощь на месте.
Выполним запрос:
# Прикладной smoke-check: проверяем, что работает ваш контроллер и сериализация ответа
curl http://localhost:8080/api/catalog/items
# [{"id":1,"sku":"SKU-001","title":"Demo item","price":10.00,"status":"ACTIVE"}]
Точный JSON и количество элементов могут отличаться в зависимости от того, как подготовлен starter repo, но логика та же. Вам важно увидеть две вещи: ответ приходит и он похож на JSON — не на HTML, не на «whitelabel error page» и не на тишину.
Если хотите проверять аккуратнее и приучать себя думать статус-кодами, можно сделать так:
curl -i http://localhost:8080/api/catalog/items
# HTTP/1.1 200
# Content-Type: application/json
#
# [...]
Теперь давайте разберём типовые варианты «что пошло не так».
Если вместо JSON вы видите 404, это часто означает банальную вещь: вы ошиблись в URL или приложение стартовало, но endpoint не зарегистрирован, например если вы случайно дёргаете /api/catalog/item вместо /api/catalog/items. И здесь важно не превращаться в «мага, который читает судьбу по 404». Мы не гадаем — мы открываем логи и смотрим, поднялся ли вообще контроллер и дошло ли приложение до конца старта.
Если вы видите 500, контейнер и приложение, скорее всего, живы, раз уж ответ вообще пришёл. Но внутри произошла ошибка при обработке запроса. На этом дне мы не уходим глубоко в исправление бизнес-логики, но обязаны уметь увидеть эту ошибку в логах, иначе Docker быстро превратится для вас в «чёрный ящик».
На этом шаге уже должно появиться полезное ощущение: health — это «пульс есть», а /api/catalog/items — это «пациент уже может произнести своё имя и дату рождения». Не медицинская аналогия века, но для первого запуска работает.
4. Читаем стартовые логи
Логи — это ваша главная «камера наблюдения» за тем, что реально происходит внутри контейнера. Начинающие часто относятся к docker logs как к инструменту «только когда всё сломалось». Но инженерный подход другой: логи читаются каждый раз после старта, даже если всё вроде бы нормально. Это как посмотреть в зеркало перед выходом: можно и не смотреть, но потом будет обидно, когда выяснится, что на голове у вас «проект собрался, но не стартовал».
Самая простая команда, и самая полезная на старте, выглядит так:
docker logs catalog-service
Но на практике вы быстро заметите: логов может быть много, и чаще всего нужен именно «хвост» старта. Поэтому удобно ограничивать вывод:
# Берём «хвост» логов, чтобы быстро увидеть старт, ошибку или строку Started ...
docker logs --tail 80 catalog-service
# ... Starting CatalogApplication ...
# ... Tomcat started on port 8080 (http) ...
# ... Started CatalogApplication in 2.4 seconds ...
Что именно мы ищем в стартовых логах? Не нужно читать каждую строчку как художественный роман — Spring Boot не всегда пишет сюжетно. Нам нужны несколько сигнальных моментов.
Во-первых, в логах должно быть видно, что приложение действительно стартует, а не мгновенно завершается. Обычно это строка вида Starting ....
Во-вторых, должен появиться сигнал, что веб-сервер поднялся и слушает порт. В классическом случае это что-то вроде Tomcat started on port 8080 (http).
В-третьих, мы ищем финальную строку, или близкую к ней по смыслу, о том, что приложение закончило старт. Как правило, это Started ... in ... seconds. Это одна из самых ценных строчек на этом этапе: она превращает старт из «кажется, работает» в «приложение дошло до конца инициализации».
Если вам нужно наблюдать старт в реальном времени, можно открыть второй терминал и включить «подписку» на логи:
# -f = follow, логи будут идти потоково (остановка вывода — Ctrl+C)
docker logs -f catalog-service
Теперь важный момент: логи и HTTP-проверка должны жить вместе в вашей голове. Например, ситуация: curl /actuator/health даёт Connection refused. Что вы делаете? Не переписываете Dockerfile в панике. Вы открываете docker logs и смотрите: приложение вообще стартует? Может, оно упало на старте. И если да, то в логах будет конкретная причина, а не ощущение, что «вселенная против вас».
Чтобы сделать это ещё более механическим и меньше зависеть от настроения, можно держать у себя такой мини-набор команд, буквально как чек:
docker ps
curl -i http://localhost:8080/actuator/health
curl -i http://localhost:8080/api/catalog/items
docker logs --tail 80 catalog-service
Это не «скрипт», а привычка. Когда такая привычка появляется, Docker перестаёт быть страшным: он становится предсказуемым.
5. Склеиваем картину: HTTP-симптомы + логи = быстрый диагноз
Самая частая проблема новичка в Docker звучит так: «У меня не работает». Это плохая формулировка не потому, что вы плохой человек, а потому, что она не помогает мозгу выбрать следующий шаг. Нам нужно научиться превращать «не работает» в наблюдаемую картину: что показывает curl, что показывает docker ps, что показывают логи.
У этого первого контейнера есть полезный ритуал, который лучше прогонять целиком, а не вспоминать кусками:
- ./gradlew bootJar — собираем артефакт.
- java -jar build/libs/docker-java-catalog-service-0.0.1-SNAPSHOT.jar — один раз убеждаемся, что сам jar жив без Docker.
- docker build -t docker-java-catalog-service:day3 . — собираем image из корня проекта.
- docker run --name catalog-service -p 8080:8080 docker-java-catalog-service:day3 — запускаем контейнер.
- curl http://localhost:8080/actuator/health — проверяем базовый признак жизни.
- curl http://localhost:8080/api/catalog/items — проверяем, что жив и прикладной путь.
- docker logs --tail 80 catalog-service — читаем стартовую картину, а не гадаем по одному симптому.
Если после docker build и docker run картина остаётся мутной, не лечите всё как «чисто Docker-проблему». Вернитесь к локальному java -jar: этот шаг быстро отделяет сломанный артефакт от сломанной контейнеризации.
Когда такой минимум повторяется без сюрпризов, Dockerfile перестаёт быть магическим файлом и превращается в обычный способ управлять запуском.
Ниже небольшая таблица-шпаргалка, которая помогает не метаться. Это не энциклопедия, а минимальный «переводчик симптомов» для первого запуска.
| Что вы видите в curl | Что это чаще всего означает | Что проверить в первую очередь |
|---|---|---|
| Failed to connect / Connection refused | До порта никто не слушает: контейнер не запущен, порт не опубликован, приложение не подняло web-сервер | docker ps, секция PORTS, затем docker logs --tail 80 |
| HTTP 200 на /actuator/health, но 404 на /api/catalog/items | Приложение поднялось, но вы дергаете не тот URL или endpoint реально отсутствует | Сверить путь, затем docker logs на наличие ошибок маппинга/старта |
| HTTP 500 на /api/catalog/items | Endpoint найден, но обработка запроса падает внутри приложения | docker logs (там будет stack trace / причина) |
| Долго висит без ответа (timeout) | Контейнер жив, но приложение может зависнуть на старте или «подвиснуть» на обработке | docker logs -f, посмотреть, дошёл ли старт до Started ... |
Обратите внимание: почти в каждой строке таблицы всплывают логи. Это нормально. В контейнерном мире логи — это «чёрный ящик», который вдруг становится прозрачным, потому что вы умеете пользоваться docker logs.
Ещё один практический приём: когда вы видите проблему, попробуйте сформулировать её через один из двух вопросов. Либо «процесс жив?» — это видно по docker ps и логам, — либо «HTTP отвечает?» — это видно по curl. Когда вы научитесь честно отвечать на эти вопросы, половина типовых проблем перестанет быть загадкой.
Когда image уже собран и контейнер запущен, помогает ещё более короткий runtime-чек в виде текста — его даже можно сохранить себе в заметки:
1) Есть контейнер в docker ps?
2) Есть ли опубликованный порт 8080?
3) Отвечает ли /actuator/health?
4) Отвечает ли /api/catalog/items?
5) Если что-то нет — открыть docker logs и искать момент, где всё пошло не так
Да, это почти инструкция по сборке шкафа. Но шкаф хотя бы можно собирать молча. А Spring Boot, если его не слушать, начинает «мстить» stack trace’ами.
6. Типичные ошибки при smoke-check и логах
Ошибка №1: остановиться на docker ps и объявить победу.
Очень хочется: контейнер в списке — значит всё ок. Но контейнер может быть “Up”, а приложение внутри всё ещё стартует или уже упало и перезапустилось — в более сложных сценариях. Даже на этом раннем шаге дисциплина простая: после запуска контейнера вы делаете хотя бы один HTTP-запрос и смотрите стартовые логи. Это не паранойя, а гигиена.
Ошибка №2: проверять только /api/catalog/items и игнорировать /actuator/health.
Если прикладной endpoint не отвечает, вы сразу проваливаетесь в мир «слишком много причин». Health endpoint нужен как быстрый фильтр: приложение вообще поднялось или вы стучитесь в пустоту? Это экономит минуты, а иногда и часы, и помогает не ругаться на контроллеры, когда проблема на самом деле в старте приложения.
Ошибка №3: путать Connection refused и 404, называя это одинаково «не работает».
Connection refused означает, что соединение не установилось: на этом порту никто не слушает или вы подключаетесь не туда. 404 означает, что вы подключились к приложению, но оно не нашло маршрут. Это две разные вселенные. В первой вы смотрите на контейнер, порты и старт. Во второй — на правильность URL, маппинги и, конечно, логи.
Ошибка №4: читать одну последнюю строчку логов и пытаться по ней понять всё.
Spring Boot умеет красиво падать, но причина почти всегда лежит выше по логу. Если приложение не стартовало, в логах обычно есть момент «вот здесь всё пошло не туда»: исключение, сообщение о порте, отсутствие конфигурации. Поэтому на старте лучше смотреть хотя бы последние 50–100 строк. --tail 80 — хороший рабочий минимум. Ищите связный кусок: от Starting до Started или до явной ошибки.
Ошибка №5: искать логи по случайному container ID, потому что контейнер запускали без имени.
Технически так жить можно, но это боль. Когда у контейнера есть имя catalog-service, вы просто пишете docker logs catalog-service и не тратите ресурсы мозга на копипаст ID. В реальной разработке это мелочь, которая экономит десятки микрострессов в день. А микрострессы, как известно, отлично складываются в состояние «почему я устал, я же ничего не делал».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ