1. Docker logs без настройки
Если вы раньше запускали Spring Boot локально, вы привыкли к простой картине: приложение пишет в консоль IDE, вы туда и смотрите. В контейнере всё похоже, но есть важный поворот сюжета: контейнер — это не «мини-сервер», а упакованный процесс. И наблюдать его проще всего так же, как любой процесс в системе — через стандартные потоки вывода.
В Docker у контейнера есть главный процесс (обычно это ваш java -jar ...). Пока этот процесс жив — контейнер «жив». Как только процесс завершается — контейнер считается остановленным. Логично, что Docker как среда запуска в первую очередь умеет «подслушивать» то, что этот процесс выводит наружу: обычный вывод и вывод ошибок. То есть stdout и stderr. В типичном Docker Engine / Docker Desktop это подключено «из коробки», и команда docker logs просто показывает вам то, что процесс уже отправил в эти потоки.
Можно представить это как очень простой «проводной» канал между приложением и миром:
flowchart TD
%% Идея: Docker просто прокидывает наружу stdout/stderr процесса (PID 1)
A["Java процесс (PID 1 в контейнере)"] --> B["stdout"]
A --> C["stderr"]
B --> D["Docker runtime собирает вывод"]
C --> D
D --> E["docker logs / docker compose logs"]
E --> F["вы читаете и понимаете, что происходит"]
Это и есть причина, почему для контейнеров консольные логи — не странная привычка «из мира Linux», а самый естественный и дешевый канал наблюдения. Вам не нужно залезать внутрь контейнера, не нужно искать файлы, не нужно думать о том, в какой папке «лежит лог». Ваш процесс уже сказал всё вслух — остаётся только слушать.
2. stdout и stderr в контейнере
Слова stdout и stderr часто звучат так, будто это что-то «для админов», а разработчику можно прожить без этого знания. На практике, если вы работаете с Docker, это становится базовой грамотностью — примерно как понимать разницу между localhost на хосте и localhost внутри контейнера. Ничего сверхъестественного: это просто два разных «канала», куда процесс может писать текст.
stdout (standard output) — это «обычный» вывод программы. stderr (standard error) — вывод ошибок. Важно понимать, что это не магическое разделение. Это конвенция: принято писать нормальные сообщения в stdout, а сообщения об ошибках — в stderr. Но никто физически не запрещает вам написать “я сломался” в stdout, а “всё отлично” в stderr. Просто потом вы сами будете грустить, читая логи, и винить Docker, хотя виноват будет человек с клавиатурой (то есть мы).
В Java эти потоки видны буквально руками:
public class StreamsDemo {
public static void main(String[] args) {
// stdout — «обычные» сообщения приложения
System.out.println("Hello from stdout");
// stderr — ошибки/диагностика (по конвенции)
System.err.println("Hello from stderr");
}
}
Если такой процесс запущен как основной процесс контейнера, Docker «соберёт» оба потока. В большинстве случаев вы увидите их вместе в docker logs. Но на уровне идеи полезно держать их раздельно: это помогает не смешивать нормальные события и ошибки в один бесформенный текстовый суп.
Чтобы зафиксировать различие без философии, вот маленькая таблица, которую полезно помнить именно в контексте контейнеров:
| Поток | Какой смысл | Что туда обычно пишут | Почему Docker это любит |
|---|---|---|---|
| stdout | «Нормальная жизнь процесса» | старт, режим работы, важные события | легко собирать и показывать через docker logs |
| stderr | «Сигнал: что-то пошло не так» | ошибки, исключения, диагностические сообщения | можно отдельно анализировать, фильтровать, отправлять в отдельные каналы |
Самое важное: Docker не “делает логи” вместо вашего приложения. Docker только честно переносит наружу то, что приложение уже пишет. Если приложение молчит — Docker тоже будет молчать, и вы будете смотреть на пустой docker logs как на пустой холодильник ночью: вроде открываешь, надеешься, а там только свет.
3. docker logs: чтение stdout/stderr
Команда docker logs иногда воспринимается как отдельная магия: будто Docker «где-то внутри» ведёт специальные журналы. На самом деле, в типичном окружении всё проще: Docker просто хранит вывод stdout/stderr и даёт вам его прочитать. То есть вы читаете не «файл приложения», а историю того, что процесс говорил в консоль.
В нашем курсе это важная привычка: когда контейнер «не работает», первая реакция — не “паника и перезапуск”, а спокойное: “Окей, что он написал?”. И это буквально одна команда:
# Показать накопленные логи контейнера (stdout/stderr)
docker logs catalog-service
Чуть более удобный режим — «следить вживую». Это особенно полезно на старте приложения, когда вы ожидаете “Tomcat started”, “Started CatalogApplication”, и хотите увидеть это сразу:
# Следить за логами в реальном времени (tail -f для контейнера)
docker logs -f catalog-service
Иногда логов много, и вам не нужен весь роман «Война и мир», вы хотите последнюю страницу. Тогда полезен --tail:
# Показать только последние 50 строк (быстро посмотреть «конец истории»)
docker logs --tail 50 catalog-service
И ещё один человеческий сценарий: «покажи логи только за последние N минут». Для этого есть --since:
# Показать логи, которые появились за последние 5 минут
docker logs --since 5m catalog-service
Заметьте, что во всех этих командах мы ничего не настраиваем в приложении. Мы только выбираем удобный способ чтения вывода процесса. И это как раз контейнерное мышление: среда запуска отвечает за “как читать”, приложение отвечает за “что писать”.
4. Логи в файле: плохой default
Очень частая первая мысль: «Давайте писать логи в файл, например /app/logs/app.log, а потом будем его смотреть». Звучит логично, пока вы мысленно представляете контейнер как мини-сервер. Но контейнер — не мини-сервер. Он ближе к одноразовой капсуле с процессом. И у контейнера есть два неприятных свойства: его легко пересоздать, и его файловая система (writable layer) не предназначена быть вашим “долгоживущим диском”.
Если вы пишете лог в файл внутри контейнера, вы привязываете свою наблюдаемость к файловой системе контейнера. А это значит, что при пересоздании контейнера (что в Docker-мире происходит постоянно, и это нормально) вы легко теряете историю или получаете куски логов, разбросанные по разным контейнерам. Это превращает диагностику в квест: “какой из контейнеров был запущен вчера, а какой сегодня?”.
Ещё один момент: чтобы посмотреть файл логов, вам часто придётся идти внутрь контейнера через docker exec. Это уже не “быстро посмотреть симптомы”, а “зайти внутрь, найти путь, проверить права, убедиться, что файл вообще пишется”. Для учебного и повседневного developer-workflow это лишняя сложность. Файлы внутри контейнера и без того не самая надежная опора по умолчанию, и логи здесь не исключение.
Наконец, файл логов внутри контейнера рано или поздно поднимает скучные, но неизбежные вопросы: ротация, размер, права на запись, разные окружения, тома, bind mounts… Всё это существует в жизни, но начинать контейнерную наблюдаемость лучше с того, что работает сразу: stdout/stderr.
5. Пример: Container-Ready Catalog Service
В сквозном проекте курса нам повезло: Spring Boot по умолчанию уже «контейнеро-дружелюбный» в плане логов. Он пишет в консоль, а значит Docker может это собирать. Поэтому наша цель сегодня — не “изобрести логирование”, а научиться правильно читать его в контейнерном сценарии и перестать ожидать, что где-то существует “секретная папка с логами”.
Представим, что образ сервиса уже собран и называется так:
# Собираем Docker-образ из текущей директории (Dockerfile должен быть рядом)
docker build -t docker-java-catalog-service .
Теперь запускаем контейнер:
# Запускаем контейнер:
# --name нужен, чтобы удобно ссылаться на контейнер в docker logs
# -p 8080:8080 пробрасывает порт на хост
docker run --name catalog-service -p 8080:8080 docker-java-catalog-service
После запуска первый признак «оно живо» — это не “контейнер появился в списке”, а то, что в логах есть понятная стартовая картина. И вот здесь включается базовый ритуал взрослого разработчика (не путать с шаманом): открыть логи и посмотреть, что там.
# Быстро посмотреть последние строки старта (самое полезное в первые секунды)
docker logs --tail 30 catalog-service
Типичная картина для Boot-сервиса — несколько строк про старт, порт, контекст приложения. Детальный текст может отличаться, но смысл один: вы хотите увидеть, что приложение не упало сразу, что оно слушает порт, и что оно не ругается на конфигурацию. Это “первый сигнал жизни”, который дешевле любого HTTP-запроса, потому что не требует даже curl.
И очень важно: на этом этапе мы не обязаны лезть в контейнер, не обязаны смотреть ps внутри него, не обязаны гадать. Достаточно прочитать то, что процесс сказал в консоль. Если он сказал “I’m alive” — отлично. Если он сказал “Exception: …” — тоже отлично, потому что теперь у нас есть конкретный симптом, а не мистическое “не работает”.
6. System.out.println vs Logger
Как учебная демка System.out.println полезен: он мгновенно показывает сам факт, что процесс пишет в stdout, а Docker это собирает. Для понимания того, как контейнер вообще забирает вывод процесса, этого более чем достаточно.
Но в реальном Boot-сервисе на println далеко не уедешь. У него нет уровней, сообщения быстро превращаются в шум, а серьёзная диагностика начинает выглядеть как археология по случайным строкам. Поэтому нормальный baseline здесь — Logger (обычно через SLF4J).
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExportService {
private static final Logger log = LoggerFactory.getLogger(ExportService.class);
public void export(String fileName) {
log.info("Export started: file={}", fileName);
}
}
Для Docker канал при этом не меняется. Хоть System.out.println, хоть log.info(...), хоть log.error(...) — если приложение пишет в консоль, это попадёт в те же контейнерные логи и будет читаться через docker logs. Контейнеру всё равно, кто именно произнёс строку; для него это один и тот же поток.
7. Типичные ошибки при работе с docker logs
Когда вы только начинаете контейнеризовать сервис, очень легко продолжать мыслить как «локальный разработчик», который всегда может открыть IDE, найти файл логов и “как-нибудь разберусь”. В контейнерном мире у нас другая цель: быстро увидеть симптомы через docker logs, не устраивая археологическую экспедицию внутрь контейнера.
Ошибка №1: считать файл логов внутри контейнера «главным источником правды».
Проблема в том, что файл внутри контейнера живёт на writable layer и привязан к жизненному циклу конкретного контейнера. Как только контейнер пересоздали — вы либо потеряли историю, либо получили разрозненные куски. В учебном и базовом production-мышлении лучше держать baseline на stdout/stderr, а файл рассматривать как отдельный, осознанный сценарий.
Ошибка №2: писать всё через System.out.println, потому что “так быстрее”.
На первых шагах это нормально как демонстрация потоков. Но в сервисе это быстро превращается в шум: у сообщений нет уровней, единого формата и нормальной управляемости. Если логирование нужно для диагностики, baseline — нормальный Logger, а не россыпь случайных строк.
Ошибка №3: путать “контейнер запустился” с “сервис готов”.
docker run может успешно создать контейнер, но приложение внутри может тут же упасть, зациклиться на старте или печатать ошибку конфигурации. Поэтому чтение docker logs — это часть нормального запуска, а не “что-то, что делаем только при аварии”.
Ошибка №4: делать сообщения многословными и многострочными без необходимости.
В контейнере логи почти всегда читаются построчно и часто с конца через --tail. Если вы превращаете каждое событие в пять строк пояснений, нужный сигнал теряется. Лучше коротко: что случилось, с чем и чем закончилось. Остальное — либо отдельные строки, либо stack trace, когда реально есть ошибка.
Ошибка №5: ожидать, что Docker “сам поймёт”, что важно, а что нет.
Docker честно показывает то, что выводит процесс. Если процесс молчит — Docker тоже молчит. Если процесс пишет хаос — Docker покажет хаос. Поэтому ответственность за качество логов всегда начинается в приложении, даже если читаем мы их через docker logs.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ