Короткий ответ

Драйвер json-file по умолчанию не ротирует. Каждый контейнер пишет /var/lib/docker/containers/<id>/<id>-json.log без потолка. На host.example включите max-size и max-file глобально в daemon.json и/или в compose сервиса web. Существующие гигабайтные файлы сами не усохнут — после политики нужен recreate или аккуратный truncate. Не docker system prune -a (это образы). Не удаляйте app_data.

Карта «кто съел диск», если не уверены: диск из‑за Docker.

Симптомы и как отличить

  • sudo du -sh /var/lib/docker/containers сравнимо с df;
  • docker logs app-web-1 минутами открывается;
  • docker system df показывает маленький Images, большой — не volumes, а «прочее» на ФС;
  • DEBUG-уровень приложения в проде.
ИсточникПризнакНе путать
json.logфайлы *-json.logoverlay
journald драйвердиск в /var/log/journalэтот рецепт другой
приложение пишет в volumeрост app_dataне logging driver

Возможные причины

  1. Нет log-opts в daemon.json.
  2. Compose без секции logging:, контейнер создан до глобальной политики (opts не ретроактивны).
  3. Сервис в цикле печатает stacktrace (restart loop).
  4. access-лог nginx в stdout на высокой RPS без лимита.
  5. Кто-то включил docker logs -f в systemd навсегда — не размер файла, но нагрузка.
  6. Редко: драйвер local vs json-file путают в документации.

Диагностика

1. Размер логов

sudo du -sh /var/lib/docker/containers
sudo find /var/lib/docker/containers -name '*-json.log' -printf '%s %p\n' | sort -n | tail

Сопоставьте id с именем:

docker ps -aq | while read id; do
  n=$(docker inspect -f '{{.Name}} {{.HostConfig.LogConfig.Type}} {{.HostConfig.LogConfig.Config}}' "$id")
  s=$(sudo stat -c%s /var/lib/docker/containers/$id/$id-json.log 2>/dev/null || echo 0)
  echo "$s $n"
done | sort -n

2. Политика

python3 -m json.tool /etc/docker/daemon.json
cd /opt/app && docker compose config | grep -A10 logging || echo 'no logging in yaml'

3. Темп роста

sudo ls -l /var/lib/docker/containers/$(docker inspect -f '{{.Id}}' app-web-1)/$(docker inspect -f '{{.Id}}' app-web-1)-json.log
sleep 10
sudo ls -l /var/lib/docker/containers/$(docker inspect -f '{{.Id}}' app-web-1)/$(docker inspect -f '{{.Id}}' app-web-1)-json.log

Рост МБ/10с — чините уровень логов приложения, не только ротацию.

Решение

Сценарий A. Глобальная ротация Engine 27.x

sudo cp -a /etc/docker/daemon.json /etc/docker/daemon.json.bak.$(date +%F)

Добавьте (смержите с существующим JSON, не затрите storage-driver):

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
sudo python3 -m json.tool /etc/docker/daemon.json >/dev/null
sudo systemctl reload docker || sudo systemctl restart docker

Уже созданные контейнеры не подхватят opts. Recreate стека:

cd /opt/app
docker compose up -d --force-recreate
docker inspect app-web-1 --format '{{json .HostConfig.LogConfig}}'

app_data сохранится. Не down -v.

Сценарий B. Только сервис web в compose

services:
  web:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Полезно, если daemon.json общий и спорный.

Сценарий C. Уже 40 ГБ лога, диск 100%

  1. Включить политику как выше.
  2. Остановить только жирный контейнер.
  3. Truncate после stop или recreate:
id=$(docker inspect -f '{{.Id}}' app-web-1)
sudo systemctl reload docker 2>/dev/null || true
# после compose stop web:
sudo truncate -s 0 /var/lib/docker/containers/$id/$id-json.log
docker compose -f /opt/app/compose.yaml start web

Если stop нельзя и inode держит место — recreate быстрее.

Не find ... -delete по всем json.log на хосте с чужими стеками без списка.

Сценарий D. Приложение-болтун

Ротация — предохранитель, не лечение. Срежьте DEBUG. Иначе 3×10 MiB будут крутиться как вентилятор, CPU на json тоже есть.

Политика в daemon.json действует на новые контейнеры после reload/restart docker. live-restore позволяет reload без убийства процессов, но LogConfig у уже созданных не меняется. Поэтому обязателен compose up -d --force-recreate без -v. Проверка: inspect max-size. Если забыли recreate, диск снова упрётся в тот же json.log.

Драйвер local в 27.x ротирует по умолчанию (около 100m×5, уточняйте docker info и документацию текущей патч-версии, не держите цифру как вечную). Миграция json-file → local в инциденте — лишний риск: сначала opts на json-file, место вернуть, драйвер менять отдельно с тестом docker logs.

Не направляйте docker logs в cron каждую минуту без --since: вы читаете весь файл. Для сбора в syslog используйте драйвер или vector/promtail, читающий тот же json с ротацией, либо journald. Stdout приложения на DEBUG с телами запросов за час заполняет 10m×3 и вы теряете след инцидента — режьте уровень, не max-size до гигабайт «на всякий случай».

Если контейнер в restart loop, ротация не спасёт CPU: json-file синхронно пишет каждый crash. Чините exit, параллельно включите max-size.

Как проверить, что проблема устранена

docker inspect app-web-1 --format '{{json .HostConfig.LogConfig}}'
sudo find /var/lib/docker/containers -name '*-json.log' -printf '%s\n' | awk '{s+=$1} END {print s}'
df -h /var/lib/docker

В LogConfig видны max-size/max-file. Сумма логов ограничена порядком N контейнеров × 30 MiB (при 10m×3), не десятки гигабайт. Стек жив.

Если не помогло

  • После daemon.json inspect без opts — не было recreate.
  • Драйвер journald: ротация systemd-journald, не json-file. Смотрите journalctl --disk-usage.
  • local driver имеет свою ротацию; не смешивайте советы.
  • Место не вернулось: deleted-open, lsof +L1, restart контейнера.
  • Заполняет не log, а overlay/volume — вернитесь к карте диска.

Профилактика

  • log-opts в эталоне daemon.json всех docker-хостов.
  • Шаблон compose с logging.
  • Алерт на размер *-json.log > 200 MiB.
  • Не логировать тела запросов с PII в stdout.
  • CI grep: отсутствие max-size на новых стеках.

FAQ

json-file vs journald vs local

json-file — простой, нуждается в max-size. journald — в journald лимиты хоста. local — бинарный, с дефолтной ротацией в современных Engine; если уже json-file в проде, проще включить opts, чем менять драйвер в инциденте.

max-file 3 мало для разбора?

Для инцидента увеличьте до 10×20m на одном сервисе, не снимайте потолок.

truncate на живом файле?

Риск. Предпочтительнее stop/recreate. Если truncate на живом — сразу проверьте docker logs --tail 5.

prune чистит json.log?

Нет. prune про неиспользуемые объекты. Жирный лог живого контейнера prune не трогает.

syslog драйвер?

Тогда ротация на приёмнике. Не оставляйте json-file без opts «пока не настроим syslog».