Централизованный сбор логов Docker в 2026 строится по одной схеме: контейнер пишет поток через драйвер логирования (json-file или local), агент Vector читает его через source docker_logs, добавляет метку сервиса из метаданных Compose и отправляет через sink loki в Grafana Loki, где записи ищутся в Grafana запросами LogQL. Имя сервиса берётся из метки com.docker.compose.service, поэтому оно не теряется при пересоздании контейнера.
Дальше по шагам: выбор драйвера (json-file, local, fluentd, GELF, journald, syslog), настройка блока logging в Docker Compose с ротацией, полный файл vector.toml от source до sink, хранение и запросы в Loki, ограничение дискового пространства и разбор ошибок с метками. Конфигурации рассчитаны на Docker Engine 27+, Compose v2, Vector 0.40+ и Loki 3.x.
Зачем централизовать логи Docker и что изменилось к 2026 году
Стандартный docker logs показывает записи живого контейнера и только на том хосте, где он запущен. Драйвер json-file по умолчанию складывает их в файл /var/lib/docker/containers/<id>/<id>-json.log, а параметры max-size и max-file в свежей установке Docker не заданы. Сервис с активным логированием переполняет раздел /var/lib/docker за считанные дни, а после пересоздания контейнера история исчезает.
Централизованный сбор закрывает три задачи: логи остаются доступными после удаления контейнера, поиск идёт по всем сервисам сразу, а на всплеск ошибок ставится алерт. Рабочая схема 2026 года: драйвер пишет журнал на хост, Vector читает его через Docker API, добавляет имя сервиса и отправляет в Grafana Loki, откуда записи попадают в Grafana.
Что теряется без централизованного сбора
Сценарий из практики: сервис shop-api отвечает 500-ми, дежурный перезапускает контейнер командой docker compose restart api, и причину сбоя смотреть уже негде. Драйвер json-file привязан к каталогу контейнера, поэтому пересоздание стирает историю. Типовые потери выглядят так:
- логи разбросаны по хостам: в кластере из трёх нод нужно заходить на каждую;
- в записях нет имени сервиса, только container_id;
- нет общей точки поиска: запрос «все 500-е по проекту» выполнить нечем;
- нет алертов и статистики: о проблеме узнаёте от пользователей.
Архитектура решения: от контейнера до Loki
Конвейер выглядит так:
Docker-контейнер -> драйвер логирования (json-file или local) -> Vector: source docker_logs -> transform remap -> sink loki -> Grafana Loki -> Grafana: Explore и алерты
Vector подключается к Docker двумя способами. Первый: source docker_logs забирает поток через Docker API, и compose-файл менять не нужно. Второй: драйвер fluentd отправляет логи по forward-протоколу в source fluent самого Vector, тогда на диск они не пишутся вообще.
Схема одинаково работает на одиночном хосте и в кластере: на каждой ноде поднимается свой Vector, а метка host различает источники. Общая архитектура конвейеров и сравнение агентов разобраны в материале про централизованный сбор и анализ логов в 2026.
Драйверы логирования Docker: сравнение и выбор
В контейнерных окружениях 2026 года реально используются шесть драйверов. Таблица показывает разницу, а детали ротации собраны в статье про драйверы и централизованный сбор логов Docker.
| Драйвер | Куда пишет | Ротация | Когда использовать |
|---|---|---|---|
| json-file | JSON-файл на хосте, одна строка на запись | max-size, max-file | дефолт, отладка, агенты, читающие файлы |
| local | бинарный файл на хосте | max-size, max-file | продакшен на одном хосте, экономия диска |
| fluentd | forward-протокол в Fluentd, Fluent Bit или Vector | буфер приёмника, fluentd-async | централизованный приёмник, много хостов |
| gelf | UDP или TCP в Graylog | на стороне Graylog | инфраструктура уже на Graylog |
| journald | journald хоста | настройки journald.conf | Linux со systemd, единый journal |
| syslog | syslog-сервер по UDP или TCP | на стороне сервера | legacy-инфраструктура и SIEM |
json-file и local: что выбрать для продакшена
json-file хранит одну JSON-запись на строку: {"log":"...","stream":"stdout","time":"2026-09-11T10:00:00.000000000Z"}. Формат читаемый, работает с docker logs, файл разбирается любым парсером. Ротацию включают опции max-size и max-file; без них файл растёт бесконечно.
Драйвер local появился в Docker 18.09 и пишет журнал в бинарном формате. Накладные расходы и объём данных ниже, чем у json-file, поэтому на одиночном хосте local выгоднее. Плата за экономию: файл на диске не читается как JSON, и агенты, которые разбирают каталог /var/lib/docker/containers напрямую, с ним не работают. Команда docker logs записи local показывает через Docker API, а вот сторонние парсеры файлов бессильны.
Правило выбора: json-file, если нужны прямой доступ к файлам и отладка; local, если важнее место на диске и логи забираются через API.
fluentd и GELF: когда нужен внешний приёмник
Драйвер fluentd отправляет записи по forward-протоколу. Настройка в Compose выглядит так:
services:
api:
image: shop-api:2026.09
logging:
driver: fluentd
options:
fluentd-address: "127.0.0.1:24224"
tag: "docker.{{.Name}}"
fluentd-async: "true"
Опция tag задаёт имя потока: шаблон {{.Name}} подставляет имя контейнера, например shop-api-1. Без fluentd-async: "true" драйвер блокирует запуск контейнера, если приёмник недоступен. С async записи буферизуются в памяти, по умолчанию около 1 МБ, и теряются при переполнении буфера.
Драйвер GELF отправляет сообщения в Graylog:
logging:
driver: gelf
options:
gelf-address: "udp://graylog.example.com:12201"
tag: "shop-api"
UDP не гарантирует доставку, а встроенного буфера у драйвера нет: при недоступном Graylog записи пропадают. Для биллинга и платежей берите TCP либо отправляйте логи через Vector, где настраивается disk-буфер.
Общее ограничение драйверов fluentd, GELF и syslog: команда docker logs с ними не работает, логи уходят по сети и на хосте не остаются. Дежурным придётся смотреть их в центральном хранилище.
Настройка логирования в Docker Compose
Драйвер задаётся для каждого сервиса через блок logging: ключ logging.driver выбирает драйвер, logging.options передаёт параметры. Дефолт для всех новых контейнеров хоста прописывается в /etc/docker/daemon.json.
Пример compose.yaml с ротацией и метками
services:
api:
image: registry.example.com/shop-api:2026.09
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
labels:
env: "prod"
log_collect: "true"
worker:
image: registry.example.com/shop-worker:2026.09
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
labels:
env: "prod"
log_collect: "true"
Разбор строк:
driver: json-fileоставляет логи на хосте в читаемом виде, их заберёт Vector;max-size: "10m"ограничивает один файл десятью мегабайтами,max-file: "3"хранит три файла, то есть до 30 МБ на контейнер;- блок
labelsпопадает в метаданные контейнера: по ним Vector фильтрует поток и строит метки Loki.
Метки контейнера не появляются в тексте лога сами по себе, их читает агент через Docker API. Изменения в блоке logging применяются только к новым контейнерам, поэтому после правки выполните docker compose up -d --force-recreate.
Как сохранить имя сервиса в логах
Compose помечает контейнеры набором меток, которые видно через docker inspect:
com.docker.compose.project, имя проекта;com.docker.compose.service, имя сервиса из compose-файла;com.docker.compose.container-number, номер реплики;com.docker.compose.config-hash, хеш конфигурации.
Проверить метку конкретного контейнера можно так:
docker inspect -f '{{ index .Config.Labels "com.docker.compose.service" }}' shop-api-1
Если контейнеры работают через драйвер fluentd, имя сервиса передаётся в теге: tag: "docker.{{.Name}}" даёт поток вида docker.shop-api-1, из которого приёмник извлекает сервис. В связке с Loki надёжнее другой путь: Vector берёт метку com.docker.compose.service и превращает её в метку потока Loki.
Сбор логов через Vector: конфигурация и фильтрация
Vector ставится из пакета дистрибутива или установочным скриптом и работает как один статический бинарник. Ему нужен доступ к /var/run/docker.sock. Сокет открывает полный доступ к Docker API, включая управление контейнерами, поэтому подключайте его только к доверенному агенту.
Рабочий конфиг целиком: источник читает логи контейнеров, transform добавляет сервис и разбирает JSON, sink отправляет всё в Loki.
# /etc/vector/vector.toml
[sources.docker]
type = "docker_logs"
docker_host = "unix:///var/run/docker.sock"
include_containers = ["shop-"]
exclude_containers = ["vector", "grafana"]
include_labels = { log_collect = "true" }
[transforms.enrich]
type = "remap"
inputs = ["docker"]
source = '''
.service = .label."com.docker.compose.service" ?? .labels."com.docker.compose.service" ?? .container_name
.env = .label."env" ?? .labels."env" ?? "prod"
.host = get_hostname!()
if contains(string!(.message), "GET /health") {
abort
}
if starts_with(string!(.message), "{") {
parsed, err = parse_json(.message)
if err == null {
if is_object(parsed) {
.app = parsed
}
}
}
'''
[sinks.loki]
type = "loki"
inputs = ["enrich"]
endpoint = "http://127.0.0.1:3100"
encoding.codec = "json"
remove_label_fields = true
labels.service = "{{ service }}"
labels.env = "{{ env }}"
labels.host = "{{ host }}"
[sinks.loki.buffer]
type = "disk"
max_size = 268435456
when_full = "block"
Проверьте конфиг командой vector validate /etc/vector/vector.toml и запустите сервис. Живой поток событий показывает vector top.
Source docker_logs: чтение логов без драйвера
Source docker_logs забирает stdout и stderr контейнеров через Docker API, поэтому менять compose-файл не нужно. Фильтры:
include_containersиexclude_containersпринимают префиксы имён, например shop- или vector;include_labelsиexclude_labelsфильтруют по меткам контейнера, сюда подходит меткаlog_collect: "true"из Compose;docker_hostуказывает сокет; если Vector работает в контейнере, сокет монтируется внутрь.
Ограничение: драйверы fluentd, GELF и syslog логи на хосте не хранят, и Docker API их не отдаёт. Для таких контейнеров источником становится сам приёмник, например source fluent в Vector.
Transform: добавление service_name и парсинг
VRL-скрипт из конфига делает три вещи. Достаёт имя сервиса из метки com.docker.compose.service, а если контейнер запущен без Compose, подставляет имя контейнера. Отбрасывает шум health-check через abort. Разбирает JSON-сообщения в поле app.
Не объединяйте распарсенный JSON с событием через merge: поля приложения затрут служебные service и host, и записи потеряют метку сервиса. Держите структуру в отдельном поле.
Если приложение пишет JSON, Vector обходится без регулярок и разбирает поля напрямую. Готовые конфиги логгеров для Python собраны в статье про структурированное логирование в Python для ELK Stack и Grafana Loki.
Sink Loki: отправка и метки
Sink loki отправляет события по HTTP в endpoint Loki. Шаблоны {{ service }} подставляют значения из полей события, из них строятся метки потока. Опция remove_label_fields = true удаляет служебные поля из тела записи, чтобы данные не дублировались.
Disk-буфер в конфиге, 256 МБ с политикой when_full = "block", переживает перезапуск Vector и недоступность Loki. При заполнении буфера block останавливает чтение, а drop_newest теряет новые записи. Для продакшена выбирайте block и следите за свободным местом.
Grafana Loki: хранение и запросы
Loki индексирует только метки, а тела записей сжимает в чанки. Подход даёт дешёвое хранение: поиск по тексту идёт сканированием чанков, а по метке нужный поток находится мгновенно. Примеры запросов LogQL:
{service="shop-api"} |= "error"
{service="shop-api"} | json | level = "error" | status >= 500
count_over_time({service="shop-api"} |= "error" [5m])
Срок хранения задаётся в конфиге Loki:
limits_config:
retention_period: 744h
compactor:
retention_enabled: true
delete_request_store: s3
744 часа, это 31 день. Параметр delete_request_store нужен, если чанки лежат в объектном хранилище: без него compactor не удалит старые данные.
Метки Loki: что можно и что нельзя
Каждая уникальная комбинация меток создаёт отдельный поток (stream). Хорошие метки: service, env, host, cluster, уровень логирования. Плохие: request_id, user_id, trace_id, IP-адрес, точный timestamp. Запрос с меткой request_id создаёт поток на каждый запрос, и инжестеры Loki падают от нехватки памяти. Правило простое: в метку идёт то, по чему вы фильтруете постоянно, всё уникальное остаётся в теле записи.
В Loki 3.x есть structured metadata: высококардинальные поля вроде trace_id можно приложить к записи, не создавая новый поток. В индекс они не попадают, но участвуют в фильтрах.
Сравнение Loki, ELK и Graylog по стоимости и сложности собрано в статье ELK vs Loki vs Graylog в 2026.
Ротация логов и ограничение дискового пространства
Ротация на уровне драйвера работает по двум параметрам: max-size задаёт предел одного файла, max-file задаёт количество файлов. При достижении предела Docker удаляет самый старый файл. Всё, что сервис написал раньше окна ротации, остаётся только в центральном хранилище, поэтому Vector должен успевать забрать поток до перезаписи.
Бюджет места считайте заранее. Двадцать контейнеров с max-size: "10m" и max-file: "3" держат на диске 20 × 30 МБ = 600 МБ. Те же контейнеры с размером 100 МБ на файл и пятью файлами займут уже 10 ГБ.
Настройка ротации на уровне демона
Общий дефолт для всех новых контейнеров задаётся в /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
После правки перезапустите демон: systemctl restart docker. Настройка действует только на контейнеры, созданные после перезапуска; работающие контейнеры сохраняют прежние параметры, пока их не пересоздать. Блок logging в Compose перекрывает дефолт демона.
Для драйвера local параметры те же: max-size и max-file работают и с ним, а место на диске расходуется экономнее.
Контроль: df -h /var/lib/docker показывает свободное место, du -sh /var/lib/docker/containers показывает занятое логами. Алерт на 80% заполнения раздела стоит дешевле, чем ночь разбора упавшего хоста.
Типовые ошибки с метками и атрибутами
- Высококардинальные метки в Loki. request_id или user_id в labels создают поток на каждое значение. Инжестеры упираются в лимит потоков, запросы падают. Решение: убрать поле из меток, оставить его в теле записи или в structured metadata.
- Потеря service_name при парсинге. Склейка события с распарсенным JSON через
mergeзатирает поле service. Держите структуру в отдельном поле, например app. - Дублирование логов. Драйвер fluentd отправляет логи в приёмник, а Vector в это же время читает тот же контейнер через API. Источник должен быть один: или драйвер, или docker_logs.
- Потеря записей при недоступном хранилище. Memory-буфер sink переполняется, и политика when_full решает судьбу событий. Для Loki, который перезапускается при обновлении, ставьте disk-буфер.
- docker logs перестал показывать логи. После переключения на драйвер fluentd, GELF или syslog команда не работает: записи уходят по сети. Предупредите дежурную смену и дайте ссылку на Grafana.
Чек-лист перед продакшеном
- Ротация включена: max-size и max-file заданы в daemon.json или в Compose.
- Запрос
{service="shop-api"}в Grafana Explore возвращает записи. - В метках Loki нет request_id, user_id и других уникальных значений.
- У sink Loki включён disk-буфер, проверен перезапуском Loki.
- Retention в Loki настроен: retention_period и compactor.
- Алерты стоят на заполнение раздела /var/lib/docker и на пропадание потока логов от сервиса.
FAQ: частые вопросы по сбору логов Docker
Можно ли использовать несколько драйверов одновременно?
Один драйвер на контейнер. Разные сервисы в одном compose-файле могут применять разные драйверы, а дефолт для новых контейнеров задаётся в daemon.json. Два источника на одном контейнере дадут дубликаты.
Как читать логи драйвера local через docker logs?
Команда работает через API Docker и показывает записи, несмотря на бинарный формат файлов. Прямой разбор файлов на диске при этом недоступен, для такого сценария используйте json-file.
Что делать, если Loki недоступен?
Vector с disk-буфером накапливает события на диске и повторяет отправку после восстановления. Проверьте, что в конфиге есть блок [sinks.loki.buffer] с типом disk и политикой when_full, а размер буфера покрывает время простоя Loki.
Как масштабировать схему на Kubernetes?
Vector запускается как DaemonSet, читает файлы из /var/log/pods, а имя сервиса берёт из меток pod, namespace и container. Структура конвейера та же: source, transform с метками, sink в Loki. Отличие одно: контейнеры пишут в отдельные файлы на каждой ноде, поэтому источник файловый, а не docker_logs.
Итог
Пять шагов, которые дают рабочую систему сбора логов:
- Выберите драйвер: json-file для отладки и чтения файлов, local для экономии диска, fluentd для отправки по сети.
- Включите ротацию: max-size 10m и max-file 3 для всех контейнеров.
- Пропишите logging и labels в compose-файле, пересоздайте контейнеры.
- Поднимите Vector с source docker_logs, transform с меткой сервиса и sink loki с disk-буфером.
- Проверьте запросы в Grafana, настройте retention и алерты.
Логи отвечают на вопрос «что случилось», метрики показывают «как давно и насколько плохо». Связка Prometheus и Grafana разобрана в руководстве про мониторинг Docker-контейнеров с Prometheus, Grafana и Loki. Если стенда ещё нет, разместите Loki и Grafana на VPS: например, Timeweb Cloud предоставляет облачные серверы и хранилище с гибким изменением ресурсов.