Docker съел весь диск логами: разбор и лимиты
Мониторинг присылает алерт: диск на 100%, сервисы падают с «no space left on device», а вы точно помните, что вчера ничего крупного не заливали и базу не мигрировали. Первая реакция — паника и попытка понять, что вообще могло вырасти за ночь. В девяти случаях из десяти на сервере с Docker виновник один и тот же: логи контейнеров, которые никто не ограничил по размеру. Разберём это как реальный инцидент — от ложной версии до постоянного решения.
Содержание
- Первая версия: искали данные, а не логи
- Находка: `docker system df` и json-log
- Почему Docker вообще так делает
- Экстренная очистка: аккуратно, не трогая рабочие файлы неправильно
- Лимиты навсегда: daemon.json и per-service в compose
- Что если логи нужны надолго — сбор во внешнюю систему
- Профилактика: следите за `/var/lib/docker` отдельно от корня
Первая версия: искали данные, а не логи
Первое, что приходит в голову при внезапном заполнении диска — что-то разрослось само: база данных, каталог с аплоадами пользователей, кэш приложения. Логично начать именно отсюда:
df -h
du -sh /var/lib/postgresql/* 2>/dev/null
du -sh /var/www/uploads 2>/dev/null
du -sh /opt/app/storage 2>/dev/null
И вот тут начинается разочарование: база весит примерно столько же, сколько неделю назад, аплоады не изменились, бэкапы ротируются штатно. apt не тянул обновления, journalctl в разумных пределах. Диск при этом реально забит — df -h показывает 100%, а куда делось место, из очевидных мест не видно.
Это стандартная ложная версия при инцидентах с диском: искать растущие *данные*, потому что интуитивно кажется, что диск заполняют именно они. На практике же самый агрессивный и незаметный потребитель места на хосте с Docker — это не тома с данными приложения, а служебные логи самого движка контейнеризации, которые живут отдельно и по умолчанию ничем не ограничены.
Если это первое расследование заполненного диска на сервере — полезно заодно свериться с общим чек-листом, что вообще может отвалиться первым при 100% диска: там шире список подозреваемых, а не только Docker.
Находка: `docker system df` и json-log
Когда обход очевидных каталогов с данными ничего не дал, следующий шаг — посмотреть, что вообще занимает место с точки зрения самого Docker:
docker system df -v
Команда покажет разбивку по образам, контейнерам, локальным томам и кэшу сборки. Часто в ней уже видно неладное — например, контейнеры значатся с солидным размером, хотя это должен быть тонкий слой поверх образа. Но конкретно логи docker system df показывает не всегда наглядно, поэтому дальше идём напрямую в файловую систему:
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail -20
Это и есть момент находки. У Docker есть штатный лог-драйвер json-file — он же используется по умолчанию, если явно не выбран другой. Каждый контейнер пишет свой stdout/stderr в отдельный файл <container-id>-json.log внутри /var/lib/docker/containers/<container-id>/. Если приложение внутри контейнера болтливое — печатает в лог каждый HTTP-запрос, каждую ошибку соединения, каждый retry — этот файл растёт без остановки. Особенно жёстко это бьёт, когда контейнер попал в цикл рестартов или начал шумно логировать какую-то ошибку раз в секунду: за несколько часов такой файл может вырасти до размеров, сравнимых с половиной диска небольшого VPS.
Дополнительно стоит проверить не только текущие контейнеры, но и то, что происходит с уже удалёнными:
docker ps -a --filter status=exited
du -sh /var/lib/docker/overlay2 2>/dev/null
Иногда к моменту, когда вы разбираетесь, контейнер уже перезапустили вручную или он сам перезапустился — это не отменяет того, что накопленный лог-файл остался на диске до следующей ротации или очистки. Если ситуация похожая, но со слегка другим фокусом — не логи, а образы/тома/сборочный кэш — есть отдельный разбор что вообще забивает диск в Docker и как это почистить.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПочему Docker вообще так делает
Ключевой факт, который стоит держать в голове: Docker по умолчанию не ограничивает размер лог-файла контейнера, если вы явно не задали лимиты. Драйвер json-file пишет в файл, пока в системе есть место — физического потолка на объём нет. Это осознанное поведение по умолчанию (совместимость и предсказуемость поведения docker logs), а не баг, но на практике оно означает, что *любой* контейнер, который начинает шуметь сильнее обычного, способен в одиночку положить хост по диску.
Проверить, какой драйвер и лимиты сейчас применяются к конкретному контейнеру:
docker inspect --format '{{.HostConfig.LogConfig.Type}} {{.HostConfig.LogConfig.Config}}' <container_name>
Если во втором поле пусто (map[]) — лимитов нет вообще, и файл будет расти неограниченно. Это нормальная ситуация «из коробки» для установки без ручной настройки — то есть для подавляющего большинства серверов, которые никто специально не тюнил под логирование.
Стоит отдельно отметить: проблема усугубляется, если приложение внутри контейнера настроено на подробный (debug/verbose) уровень логирования «на всякий случай» и это забыли выключить после отладки, либо если контейнер попал в состояние, когда одна и та же ошибка (обрыв соединения с БД, неверный конфиг, таймаут внешнего API) повторяется в цикле — тогда рост из «медленного и незаметного» превращается в «за час съел всё».
Экстренная очистка: аккуратно, не трогая рабочие файлы неправильно
Когда причина понятна, хочется немедленно освободить место, но здесь легко сделать хуже. Главная ошибка — удалить лог-файл через rm у контейнера, который продолжает работать:
# Так делать не стоит на живом контейнере:
rm /var/lib/docker/containers/<id>/<id>-json.log
Проблема в том, что процесс контейнера продолжает писать в файловый дескриптор, который уже открыт на этот (теперь удалённый из каталога) inode. Файл технически исчезает из листинга, но место на диске не освобождается, пока дескриптор открыт — а df и du в этот момент начинают расходиться, потому что du считает по каталогу, а df — по реально занятым блокам. Если хотите разобраться в этом эффекте детальнее, есть отдельный материал про то, почему df и du иногда показывают разное — тот же принцип «удалённый, но открытый файл» всплывает не только с логами Docker.
Правильный способ экстренно почистить лог-файл живого контейнера — обнулить его содержимое, не трогая сам файл (inode) и дескриптор:
truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log
Это безопасно: файл остаётся тем же самым файлом, процесс продолжает писать в него дальше уже с нуля, docker logs не ломается. Если контейнеров-виновников несколько, можно обнулить всё разом (осторожно — это стирает историю логов, если она вам нужна для расследования, лучше сначала сохранить копию через docker logs <name> > /tmp/backup.log):
for f in /var/lib/docker/containers/*/*-json.log; do truncate -s 0 "$f"; done
Альтернативный и часто более простой вариант — просто перезапустить проблемный контейнер (docker restart <name>): Docker создаст новый лог-файл с нуля, а старый останется на диске как обычный (уже не открытый) файл, который можно спокойно удалить rm. Дополнительно стоит прогнать штатную уборку неиспользуемых объектов:
docker system prune -f
docker volume prune -f
Это не тронет логи активных контейнеров, но подчистит остановленные контейнеры, неиспользуемые образы и висячие тома — тоже часто вносящие вклад в общую картину.
Лимиты навсегда: daemon.json и per-service в compose
Экстренная очистка решает проблему сегодня, но без лимитов она повторится. Есть два уровня, на которых задаются ограничения.
Глобально — для всех новых контейнеров хоста, через /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
После правки — перечитать конфигурацию демона:
sudo systemctl restart docker
Важный нюанс: эти настройки применяются только к контейнерам, созданным после изменения — на уже существующие контейнеры они не распространяются задним числом. Чтобы применить лимит к контейнеру, который уже работает, его нужно пересоздать (docker compose up -d --force-recreate или docker run заново), а не просто перезапустить (restart).
Точечно — для конкретного сервиса, если в проекте разные контейнеры логируют с разной интенсивностью и общий глобальный лимит не подходит всем одинаково, в docker-compose.yml:
services:
app:
image: myapp:latest
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Настройка max-size: "10m" + max-file: "3" означает: как только текущий файл достигает 10 МБ, Docker ротирует его в <id>-json.log.1, а всего хранится не больше 3 таких файлов (текущий + 2 архивных) — то есть верхний предел на контейнер составит примерно max-size × max-file. Конкретные значения размера и числа файлов подбирайте под свой случай: для шумного веб-сервера с высоким трафиком может быть уместнее лимит побольше (скажем, 50m × 5), для тихого фонового воркера хватит и 5m × 2. Ориентируйтесь на то, сколько истории логов реально нужно для расследования инцидентов, а не берите значения наугад — единого «правильного» числа для всех проектов не существует.
Если в проекте много сервисов и хочется единообразия без копирования блока logging в каждый из них, вынесите настройку через x-logging в верхней части compose-файла как YAML-якорь и подключите через <<: *default-logging к каждому сервису — это тот же результат, но без дублирования.
Про сам процесс перехода со «старого» подхода к логированию на управляемый есть отдельный разбор лучших практик логирования в Docker — там шире раскрыты альтернативные драйверы и структурированные логи, а не только лимиты json-file.
Что если логи нужны надолго — сбор во внешнюю систему
Урезание max-size/max-file решает проблему диска, но одновременно означает, что старые логи физически исчезают — если инцидент случился неделю назад, а лимит хранит только последние 30 МБ, расследовать его по логам хоста уже не получится. Если логи важны для аудита или разбора инцидентов задним числом, разумнее не просто ограничивать локальный файл, а параллельно отправлять поток логов во внешнюю систему сбора: Loki, Graylog, ELK или просто syslog-сервер. Тогда лимит на json-file защищает диск хоста от переполнения, а полная история логов живёт отдельно, со своей ротацией и retention-политикой, которая уже не зависит от свободного места на боевом сервере.
Технически это делается либо через смену лог-драйвера у самого Docker (gelf, syslog, fluentd — задаются тем же полем log-driver в daemon.json или per-service), либо через отдельный агент (Promtail, Filebeat, Fluent Bit), который читает те же json-log файлы и пересылает их дальше, не мешая при этом ротации самого Docker. Второй вариант обычно проще внедрить постепенно, не трогая уже работающую конфигурацию контейнеров.
Профилактика: следите за `/var/lib/docker` отдельно от корня
Стандартный мониторинг диска обычно проверяет корневой раздел / целиком и шлёт алерт при достижении, скажем, 85-90%. Проблема в том, что к этому моменту место уже фактически кончилось, и разбираться приходится в аварийном режиме, как в начале этой статьи. Разумнее держать отдельный, более чувствительный порог именно на каталог Docker, потому что именно там прячется самый резкий и неожиданный рост:
#!/bin/bash
# /usr/local/bin/check-docker-disk.sh — запускать по cron раз в 5-10 минут
THRESHOLD_MB=2048
SIZE_MB=$(du -sm /var/lib/docker/containers 2>/dev/null | cut -f1)
if [ "$SIZE_MB" -gt "$THRESHOLD_MB" ]; then
echo "Docker containers dir выросла до ${SIZE_MB}MB" | logger -t docker-disk-check
# здесь же отправка алерта в Telegram/Slack/на почту
fi
Если на хосте уже стоит Prometheus + node_exporter, проще подключить textfile collector, который считает размер /var/lib/docker/containers и отдаёт метрику, на которую вешается алерт в Grafana/Alertmanager — так проблема будет видна на графике задолго до того, как диск дойдёт до 100%, а не постфактум по алерту «disk full».
Отдельно стоит завести алерт именно на *скорость роста*, а не только на абсолютный размер: резкий скачок за 10-15 минут почти всегда означает контейнер, зациклившийся на ошибке, и в этом случае важнее среагировать быстро, пока файл не съел всё, а не ждать, пока сработает порог по абсолютному объёму.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему docker system df не показал реальный размер логов?
Эта команда в первую очередь ориентирована на образы, контейнеры (их writable-слой) и тома — размер *-json.log файлов туда не всегда попадает наглядно. Прямой du по /var/lib/docker/containers/*/*-json.log надёжнее для конкретно этой задачи.
Поможет ли просто docker system prune -a?
Частично — очистит неиспользуемые образы, остановленные контейнеры и висячие тома, но не тронет json-log файлы активных контейнеров, потому что prune не работает с логами работающих процессов. Для логов нужна отдельная очистка (truncate/restart) и отдельные лимиты в конфиге.
Можно ли поставить лимит логов только для одного уже запущенного контейнера без пересоздания?
Нет, log-opts применяются только на создание контейнера. Единственный обходной путь для «уже сейчас» — вручную обнулять файл через truncate -s 0, пока не найдётся окно на пересоздание контейнера с правильным конфигом.
Что делать, если после truncate -s 0 место всё равно не освободилось?
Проверьте df -h и du -sh /var/lib/docker/containers отдельно — если расхождение сохраняется, скорее всего есть ещё один процесс (не обязательно тот же контейнер), который держит открытым дескриптор на удалённый файл; найти такие можно через lsof +L1 или lsof | grep deleted.
Стоит ли вообще отключать логирование контейнера полностью?
Как правило нет — log-driver: none лишает вас docker logs, что сильно усложняет будущую диагностику. Лучше ограничить размер (max-size/max-file), а не отключать логи целиком.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →