Логирование контейнеров: драйверы, ротация, сбор

Рейтинг: 74.1% · 11 голосов
Практический курс по Docker: образы, контейнеры, тома, сети, Compose и продакшен. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
Marina_DevOps
Сообщения: 54
Зарегистрирован: 11 май 2026, 05:31

Логирование контейнеров: драйверы, ротация, сбор

Сообщение Marina_DevOps »

Оглавление курса (46)
  1. Что такое Docker и какие задачи он решает
  2. Установка Docker и запуск первого контейнера
  3. Образы: слои, теги и реестр Docker Hub
  4. Пишем свой Dockerfile
  5. Тома и хранение данных: volumes и bind mounts
  6. Сети в Docker: связываем контейнеры между собой
  7. Переменные окружения и конфигурация контейнеров
  8. Docker Compose: поднимаем многоконтейнерное приложение
  9. Оптимизация образов: multi-stage сборка, размер и кэш слоёв
  10. Логи, отладка и мониторинг контейнеров
  11. Базовая безопасность контейнеров
  12. Подготовка к продакшену: что важно учесть
  13. Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root
  14. Реестры образов: приватные registry, push и pull, теги и digest, imagePullSecrets
  15. BuildKit и buildx: multi-arch сборки, секреты сборки, экспорт кэша
  16. Docker в CI/CD: автосборка, сканирование образов (Trivy, Docker Scout), публикация
  17. Итоговый проект и куда расти: от Dockerfile до прода, обзор оркестрации (Kubernetes, Podman, OCI)
  18. Архитектура Docker: dockerd, containerd, runc и OCI
  19. Как работает изоляция: namespaces и cgroups
  20. Образы изнутри: слои, OverlayFS и storage driver
  21. Жизненный цикл контейнера и политики перезапуска
  22. Отладка контейнеров: exec, attach, nsenter, debug
  23. Сети Docker глубже: bridge, overlay, macvlan и iptables
  24. Docker Compose глубже: profiles, healthcheck-зависимости, override
  25. Данные и тома глубже: драйверы, бэкап, права
  26. Секреты и конфигурация: как не светить пароли
  27. Ограничение ресурсов: CPU, память, OOM и cgroups
  28. Здоровье и автозапуск: HEALTHCHECK, init и сигналы
  29. Логирование контейнеров: драйверы, ротация, сбор (вы здесь)
  30. Мониторинг контейнеров: stats, cAdvisor, Prometheus
  31. Безопасность глубже: rootless, user namespaces, seccomp, capabilities
  32. Supply chain: сканирование, SBOM и подпись образов
  33. Глубокая оптимизация образов: distroless, scratch, кэш
  34. Multi-arch и сборка под ARM: buildx и эмуляция
  35. Реестры в продакшене: Harbor и облачные registry
  36. Docker без Docker: Podman, containerd, nerdctl, Buildah
  37. От Compose к оркестрации: когда контейнеров становится много
  38. Docker Swarm: встроенная оркестрация
  39. Сборка образов в CI без демона: kaniko, BuildKit, Buildah
  40. Траблшутинг Docker: типичные ошибки и как чинить
  41. Производительность и эксплуатация Docker-хоста
  42. Docker Desktop, WSL2 и альтернативы на Windows и Mac
  43. Контейнеризация приложений: бэкенд, фронтенд, БД
  44. Docker и Kubernetes: containerd, миграция, kompose
  45. Лучшие практики и антипаттерны Docker
  46. Сквозной проект: production-ready стек и путь дальше
Логи, которые тихо съедают диск

Классическая история. Сервис в проде живёт полгода, всё хорошо, мониторинг зелёный. И вдруг ночью алерт: на ноде кончилось место. Заходишь, а в /var/lib/docker/containers лежит файл на 80 гигабайт - это лог одного болтливого контейнера, который никто не ротировал. Дефолтный драйвер json-file пишет всё подряд и НЕ ограничивает размер, пока ты явно не скажешь. Это грабли номер один в логировании Docker, и на них наступали все.

Логирование - это не "посмотреть docker logs, когда что-то сломалось". В масштабе это контракт между приложением и платформой, политика хранения, ротация и централизованный сбор. В этом уроке разберём docker логирование вглубь: как работают драйверы под капотом, почему json-file без max-size опасен, когда брать local, journald, syslog или fluentd, и как организовать сбор логов docker через Fluent Bit или Vector в Loki/ELK. Поехали.

Изображение

Контракт stdout/stderr: откуда вообще берутся логи

Правило, которое надо вбить в голову раз и навсегда: контейнер пишет логи в stdout и stderr, и больше никуда. Не в файл /app/log/app.log внутри контейнера, не в свой собственный logrotate - а в стандартные потоки. Это часть методологии двенадцати факторов (12factor, фактор "logs") и это контракт с платформой.

Почему так. Когда процесс PID 1 в контейнере что-то печатает в stdout/stderr, эти потоки перехватывает не сам контейнер, а демон dockerd (точнее, через containerd-shim). Демон берёт каждую строку, навешивает метаданные - время, поток (stdout или stderr) - и отдаёт настроенному логирующему драйверу. Драйвер решает, что с этой строкой делать дальше: записать в JSON-файл, отправить в journald, послать по сети в syslog или fluentd.

Код: Выделить всё

   контейнер (PID 1)
        | пишет в stdout / stderr
        v
   containerd-shim  ->  dockerd
        | строка + метаданные (time, stream)
        v
   логирующий драйвер (json-file / local / journald / syslog / fluentd / gelf / awslogs)
        |
        v
   файл на диске   ИЛИ   сеть/коллектор
Отсюда сразу следствие: если приложение внутри само пишет в файл, Docker этих логов НЕ видит, docker logs покажет пустоту, а файл будет расти внутри слоя контейнера, пока не лопнет. Поэтому правильно настроенный образ направляет логи в stdout. В nginx это делают симлинком access.log -> /dev/stdout, в приложениях - конфигом логгера на console-аппендер.

json-file: дефолт, который обязан иметь ротацию

По умолчанию Docker использует драйвер docker json-file. Каждая строка превращается в одну JSON-запись и дописывается в файл на хосте по пути вида:

Код: Выделить всё

/var/lib/docker/containers/<container-id>/<container-id>-json.log
Посмотрим, что внутри. Возьмём строку лога и разберём поля:

Код: Выделить всё

{"log":"GET /health 200 1.2ms\n","stream":"stdout","time":"2026-06-15T08:14:22.118431Z"}
Поля:
  • log - сама строка вывода приложения, вместе с символом перевода строки \n. Заметь: одна строка вывода = одна JSON-запись. Многострочный стектрейс развалится на несколько записей - это отдельная боль, к ней вернёмся.
  • stream - откуда пришло: stdout или stderr. Полезно отделять ошибки от обычного вывода.
  • time - метка времени в RFC3339 с наносекундами, в UTC.
Главная проблема: по умолчанию у json-file НЕТ лимита размера. Файл растёт, пока есть место на диске. Один цикл логов в горячем сервисе - и через неделю у тебя терабайт мусора. Лечится это опциями ротации, и это не "желательно", а ОБЯЗАТЕЛЬНО для любого прода.

Включаем ротацию глобально через /etc/docker/daemon.json:

Код: Выделить всё

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
После правки демона нужен рестарт: systemctl restart docker. Что это значит: каждый лог-файл режется по достижении 10 мегабайт, хранится максимум 3 файла (текущий плюс два архивных). Старое удаляется. Итого потолок на контейнер - около 30 мегабайт. Важно: настройка в daemon.json применяется только к НОВЫМ контейнерам. Уже запущенные продолжают жить со старыми опциями, пока их не пересоздашь.

Можно задать ротацию точечно на конкретный контейнер - это переопределяет глобальную:

Код: Выделить всё

docker run -d \
  --log-opt max-size=20m \
  --log-opt max-file=5 \
  nginx:1.27
В compose.yaml то же самое через секцию logging:

Код: Выделить всё

services:
  web:
    image: nginx:1.27
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
Проверить, что реально применилось к контейнеру:

Код: Выделить всё

docker inspect --format '{{.HostConfig.LogConfig}}' web
# {json-file map[max-file:3 max-size:10m]}
local лучше json-file для прода (если не нужен docker logs снаружи)

Есть менее известный, но более правильный для дефолта драйвер - local. Он тоже хранит логи на диске, но в бинарном protobuf-формате, сжимает архивные файлы и ВКЛЮЧАЕТ ротацию по умолчанию (100m на файл, 5 файлов). То есть он защищён от переполнения из коробки - именно поэтому документация Docker рекомендует его для большинства локальных сценариев.

Код: Выделить всё

{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}
Минус local - его нельзя прочитать обычным текстовым инструментом, формат бинарный. Но команда docker logs с ним работает (об этом ниже). Если ты не парсишь json-файлы напрямую сторонними тулзами, бери local: он экономнее по диску и безопаснее по дефолту.

Где живёт docker logs и почему он работает не со всеми драйверами

Команда docker logs читает лог НЕ из приложения и не из сети - она читает локальный файл, который оставил драйвер. Поэтому критичное ограничение: docker logs работает только с драйверами json-file и local. Если ты переключил контейнер на syslog, fluentd, gelf или awslogs - логи уходят по сети наружу, локальной копии нет, и docker logs ответит ошибкой:

Код: Выделить всё

docker logs web
# Error response from daemon: configured logging driver does not support reading
Это частая ловушка: переключили на fluentd "чтобы централизованно", а потом дежурный не может быстро глянуть логи пода руками. Решение - dual logging или просто держать json-file/local плюс отдельный сборщик, который тейлит файлы (об этом в разделе про Fluent Bit).

Полезные флаги docker logs, которые реально применяют в эксплуатации:

Код: Выделить всё

docker logs -f web                  # follow, как tail -f
docker logs --tail 100 web          # последние 100 строк
docker logs --since 10m web         # за последние 10 минут
docker logs -t web                  # добавить таймстемпы
docker logs --since 2026-06-15T08:00:00 --until 2026-06-15T09:00:00 web
Под капотом -f держит открытый файловый дескриптор и досылает новые строки. И тут граблина: при ротации файла дескриптор может указывать на уже переименованный или удалённый файл. Драйверы local и json-file умеют это обрабатывать корректно, но если ты сам тейлишь -json.log внешним tail -F, помни про переоткрытие файла.

Когда какой драйвер: краткий разбор
  • json-file - дефолт, читается docker logs, удобно для дев и отладки. В прод - только с обязательными max-size/max-file.
  • local - лучший дефолт для прода на одной ноде: ротация и сжатие из коробки, экономит диск. docker logs работает.
  • journald - отдаёт логи в systemd-journal хоста. Плюс: единый journalctl на всё, фильтрация по CONTAINER_NAME, structured-метаданные. Удобно на хостах, где всё уже под systemd. Читается через journalctl CONTAINER_NAME=web, docker logs тоже работает.
  • syslog - шлёт в локальный или удалённый syslog (rsyslog/syslog-ng) по UDP/TCP/TLS. Классика для старых пайплайнов и сетевого оборудования. docker logs НЕ работает.
  • fluentd - отправляет в Fluentd/Fluent Bit по их forward-протоколу, типичный вход в ELK или другой бэкенд. docker logs НЕ работает. Грабли: если коллектор недоступен и стоит mode по умолчанию (blocking), драйвер может ЗАБЛОКИРОВАТЬ приложение. Лечится fluentd-async=true и буфером.
  • gelf - формат Graylog Extended Log Format, шлёт в Graylog или Logstash по UDP/TCP. docker logs НЕ работает.
  • awslogs - прямо в AWS CloudWatch Logs. Для облака AWS удобно, но привязка к вендору. В RU-реалиях вместо этого чаще терминируют в Loki/ELK на своих мощностях или в Yandex Cloud Logging.
Отдельно про режим доставки. У сетевых драйверов есть опция mode: blocking (по умолчанию) или non-blocking. В blocking, если буфер забит и коллектор лежит, запись лога БЛОКИРУЕТ процесс приложения - то есть твой сервис может встать из-за того, что упал логстек. В проде на сетевых драйверах почти всегда ставят non-blocking с max-buffer-size, осознанно жертвуя возможностью потерять часть логов ради того, чтобы приложение не висело:

Код: Выделить всё

docker run -d \
  --log-driver fluentd \
  --log-opt fluentd-address=10.0.0.5:24224 \
  --log-opt mode=non-blocking \
  --log-opt max-buffer-size=4m \
  myapp:latest
Структурные логи: пиши JSON, а не простыню текста

В масштабе grep по тексту не работает. Когда у тебя сотни контейнеров и миллионы строк, нужны структурные (JSON) логи: приложение печатает в stdout не "user 42 failed login from 1.2.3.4", а готовый JSON-объект:

Код: Выделить всё

{"ts":"2026-06-15T08:14:22Z","level":"error","msg":"failed login","user_id":42,"ip":"1.2.3.4","trace_id":"abc123"}
Тогда коллектор парсит это в поля, и ты в Loki/Kibana делаешь точные запросы: level=error and user_id=42, строишь дашборды, цепляешь trace_id к трейсингу. Это и есть разница между "логи как помойка текста" и "логи как данные". Почти все современные логгеры (zap, zerolog, logrus, monolog с JSON-форматтером, structlog) умеют JSON-вывод - включи его в проде.

Тонкость: при драйвере json-file твой JSON попадёт ВНУТРЬ поля log как экранированная строка - получится JSON в JSON. Это нормально, коллектор распарсит двойную вложенность. Но именно поэтому многие сразу тейлят логи коллектором и не полагаются на формат драйвера.

Централизованный сбор: Fluent Bit / Vector -> Loki / ELK

Локальные файлы хороши для одной ноды. Но когда нод десятки, нужен централизованный сбор логов docker: агент на каждой ноде читает логи всех контейнеров и шлёт в общее хранилище, где их можно искать единым интерфейсом.

Два главных хранилища-стека:
  • Loki + Grafana - индексирует не контент, а только метки (labels), поэтому дёшев по ресурсам и хранилищу. Логи смотришь в Grafana через LogQL. Популярен под Kubernetes и Docker.
  • ELK / OpenSearch - Elasticsearch/OpenSearch + Kibana. Полнотекстовый поиск, мощно, но прожорливо по RAM и диску.
Агенты-сборщики на ноде:
  • Fluent Bit - лёгкий (на C), быстрый, вендоронейтральный, куча выходов. Стандарт де-факто для сбора логов в контейнерных средах. Тейлит /var/lib/docker/containers/*/*-json.log плагином tail, парсит JSON и шлёт в Loki/Elasticsearch/Kafka.
  • Vector - на Rust, очень производительный, с мощным языком трансформаций VRL (фильтрация, обогащение, маскирование PII прямо в пайплайне). Отлично, когда нужно много преобразований на лету.
  • Grafana Alloy - актуальная замена Promtail. Важно для 2026: Promtail достиг end-of-life в марте 2026, новых фич не получает, мигрировать надо на Alloy (это дистрибутив OpenTelemetry Collector от Grafana). Если поднимаешь стек сегодня - бери Alloy или Fluent Bit, а не Promtail.
Минимальный пример Fluent Bit, который тейлит docker-логи и шлёт в Loki, в compose.yaml:

Код: Выделить всё

services:
  fluent-bit:
    image: fluent/fluent-bit:3.1
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
    ports:
      - "2020:2020"
И сам fluent-bit.conf:

Код: Выделить всё

[INPUT]
    Name        tail
    Path        /var/lib/docker/containers/*/*-json.log
    Parser      docker
    Tag         docker.*

[OUTPUT]
    Name        loki
    Match       *
    Host        loki
    Port        3100
    Labels      job=docker
Архитектурно это всегда одна схема: источник (stdout контейнера) -> драйвер кладёт на диск -> агент тейлит файл -> отправляет в хранилище -> ты смотришь в UI. При таком подходе на самих контейнерах оставляют local или json-file с ротацией (как буфер на ноде), а тяжёлую работу по агрегации делает агент. Так ты и docker logs руками не теряешь, и централизацию получаешь.

Типичные грабли и антипаттерны
  • json-file без ротации - переполнение диска, нода падает. Всегда max-size/max-file (или драйвер local).
  • Ротация = потеря логов. Ротация удаляет старые файлы. Если агент-сборщик тейлит медленно или лежал, а файл уже ротировался, эти строки потеряны навсегда. Вывод: max-size должен быть достаточным буфером, чтобы агент успевал вычитывать даже после простоя.
  • Приложение пишет в файл внутри контейнера - Docker логи не видит, docker logs пуст, слой пухнет. Перенаправляй в stdout/stderr.
  • Сетевой драйвер в blocking-режиме - падение коллектора вешает приложение. В проде - non-blocking с буфером.
  • Переключил на fluentd/syslog и удивляешься, что docker logs не работает - это by design, читается только json-file/local.
  • Многострочные стектрейсы - json-file режет на строки, в хранилище прилетает по записи на строку. Склеивать надо парсером multiline на стороне агента (Fluent Bit multiline filter) или, лучше, писать ошибки в JSON одной строкой.
  • Логирование чувствительных данных - пароли, токены, PII в логах = инцидент безопасности. Маскируй на стороне приложения или в пайплайне (Vector VRL, Fluent Bit modify/grep).
Мини-лаба: настрой ротацию и поймай переполнение руками
  • Запусти болтливый контейнер БЕЗ ротации и посмотри, как растёт файл:

Код: Выделить всё

docker run -d --name noisy alpine sh -c 'while true; do echo "spam $(date)"; done'
docker inspect --format '{{.LogPath}}' noisy
# глянь размер файла через пару минут: ls -lh <путь из вывода>
  • Останови и удали, теперь с ротацией:

Код: Выделить всё

docker rm -f noisy
docker run -d --name noisy \
  --log-opt max-size=1m --log-opt max-file=2 \
  alpine sh -c 'while true; do echo "spam $(date)"; done'
  • Проверь, что файлов не больше двух и каждый около мегабайта:

Код: Выделить всё

ls -lh "$(dirname "$(docker inspect --format '{{.LogPath}}' noisy)")"
  • Прочитай логи штатно и за последнюю минуту:

Код: Выделить всё

docker logs --tail 5 noisy
docker logs --since 1m noisy | wc -l
  • Переключи глобальный дефолт на local в /etc/docker/daemon.json, сделай systemctl restart docker, пересоздай контейнер и убедись, что docker logs всё ещё работает, а формат на диске стал бинарным. Не забудь docker rm -f noisy в конце.
Контрольные вопросы
  • Почему json-file по умолчанию опасен в проде и какими двумя опциями это лечится?
  • С какими двумя драйверами работает docker logs и почему он не работает с syslog или fluentd?
  • Чем драйвер local отличается от json-file и почему его рекомендуют как дефолт?
  • Что произойдёт с приложением, если сетевой логирующий драйвер в blocking-режиме потеряет связь с коллектором, и как от этого защититься?
Итог

Логи в контейнерах - это контракт stdout/stderr плюс политика хранения. Дефолтный json-file без max-size/max-file тихо съест диск, поэтому ротация обязательна, а ещё лучше - драйвер local с ротацией из коробки. docker logs читает только локальные json-file/local, сетевые драйверы (syslog, fluentd, gelf, awslogs) шлют наружу и этой команде недоступны. В масштабе пиши структурные JSON-логи и собирай их агентом (Fluent Bit или Vector) в Loki или ELK, помня про non-blocking, буферы и риск потери строк при ротации. Настроишь это один раз правильно - и ночных алертов про забитый диск больше не будет.
👍2 ❤️5 🔥2 😄 🤔
Аватара пользователя
kotlin_veteran
Сообщения: 1
Зарегистрирован: 24 май 2026, 23:36

Re: Логирование контейнеров: драйверы, ротация, сбор

Сообщение kotlin_veteran »

А у меня как раз на проде json-file без ротации забил 60 гигов за неделю, теперь понятно почему. Перешёл на local, спасибо
👍1 ❤️1 🔥 😄 🤔2
Аватара пользователя
grafananinja
Сообщения: 1
Зарегистрирован: 14 май 2026, 13:28

Re: Логирование контейнеров: драйверы, ротация, сбор

Сообщение grafananinja »

Вопрос: если писать сразу в fluentd, а docker logs не работает, как дежурному быстро глянуть логи руками? Или обязательно держать json-file параллельно?
👍1 ❤️ 🔥1 😄 🤔
Ответить
← Предыдущая глава
Здоровье и автозапуск: HEALTHCHECK, init и сигналы
Следующая глава →
Мониторинг контейнеров: stats, cAdvisor, Prometheus

Все главы курса «Docker: контейнеризация от основ до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: docker stats мониторинг контейнеров как смотретьМониторинг и метрики nginxdocker network как связать контейнеры между собой по имениЛоги nginx: access_log и JSON

Вернуться в «Docker с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость