stdout/ stderr и логи контейнера

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

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.

1
Задача
Docker for Spring, 14 уровень, 0 лекция
Недоступна
Консольный лог через `Logger`
Консольный лог через `Logger`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ