Классическая ситуация в продакшене. Сервер крутит десяток сервисов, всё работает месяцами, и в одно прекрасное утро мониторинг орёт: на корневом разделе кончилось место. Заходишь, делаешь
Код: Выделить всё
df -hВторая боль про docker производительность и надёжность: ты обновляешь демон или перезапускаешь systemd-юнит docker, и в этот момент падают ВСЕ контейнеры разом. Потому что по умолчанию демон - родитель процессов контейнеров, и его рестарт тащит их за собой.
Этот урок - про эксплуатацию Docker-хоста как инженерную дисциплину. Разберём, где именно живёт диск, как работает storage driver overlay2, какой реальный оверхед у контейнеров, как настраивается daemon.json (включая спасительный live-restore), как организовать ротацию, бэкап и мониторинг самого демона. Не игрушки, а то, что держит боевой хост в форме.

Где docker жрёт диск: анатомия var lib docker
Почти всё состояние Docker лежит в одном каталоге - по умолчанию /var/lib/docker (data-root). Запомни эту карту, она экономит часы паники:
- overlay2/ - слои образов и read-write слои контейнеров. Самый жирный потребитель. Тут лежат и базовые слои ubuntu/alpine, и всё, что контейнер записал поверх.
- image/ - метаданные образов, манифесты, дерево слоёв (что от чего наследуется).
- containers/ - по подкаталогу на контейнер. Внутри - конфиг и, главное, файл логов вида <id>-json.log. Вот где растут логи, если не настроена ротация.
- volumes/ - именованные тома. Данные БД, которые ты НЕ хочешь потерять. prune их тоже умеет сносить, будь осторожен.
- buildkit/ - кеш сборки BuildKit. Может разрастись до десятков гигабайт на CI-раннере.
Код: Выделить всё
docker system df
Код: Выделить всё
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 42 8 12.7GB 9.1GB (71%)
Containers 15 6 340MB 180MB (52%)
Local Volumes 23 9 6.2GB 3.8GB (61%)
Build Cache 310 0 18.4GB 18.4GB (100%)
Глубже - флаг -v показывает построчно по каждому образу, тому и слою кеша:
Код: Выделить всё
docker system df -v
Код: Выделить всё
du -h -d1 /var/lib/docker | sort -rh
sudo du -sh /var/lib/docker/overlay2/* | sort -rh | head -10
overlay2 - storage driver по умолчанию на современных ядрах. Под капотом это OverlayFS - union-файловая система прямо в ядре Linux, без патчей и пользовательских демонов. Идея: сложить несколько слоёв-каталогов в один видимый.
Терминология OverlayFS:
- lowerdir - нижние слои, read-only. Это слои образа. Их может быть много, они складываются стопкой.
- upperdir - верхний слой, read-write. Личный слой конкретного контейнера. Сюда уходят все его записи.
- merged - то, что контейнер видит как свою корневую ФС: объединение lower и upper.
- workdir - служебный каталог ядра для атомарных операций.
Теперь про оверхед честно. CPU и память - оверхед практически нулевой. Контейнер это обычный процесс хоста, обёрнутый в namespaces и cgroups; нет гипервизора, нет эмуляции. Диск - вот тут CoW аукается на write-heavy нагрузках: если БД активно пишет в свой upper-слой через overlay2, ты теряешь на CoW и накладных расходах union-FS. Правило: данные с интенсивной записью (Postgres, MySQL, очереди) выноси в volumes. Том это bind в чистую ФС хоста мимо overlay2 - запись идёт нативно. Сеть - дефолтный bridge добавляет NAT и iptables/nftables-правила, и на больших pps это заметно. Для предельной производительности есть --network host (контейнер в сетевом namespace хоста, без NAT, но и без изоляции портов).
daemon.json: центр управления демоном
Поведение демона задаётся файлом /etc/docker/daemon.json. Это JSON, его читает dockerd при старте. После правки нужен
Код: Выделить всё
systemctl reload dockerКод: Выделить всё
{
"data-root": "/data/docker",
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "20m",
"max-file": "5",
"compress": "true"
},
"default-ulimits": {
"nofile": { "Name": "nofile", "Soft": 65536, "Hard": 65536 }
},
"live-restore": true,
"registry-mirrors": ["https://mirror.example.ru"],
"default-address-pools": [
{ "base": "172.30.0.0/16", "size": 24 }
]
}
- data-root - переезд /var/lib/docker на отдельный диск/раздел. Это спасает корень от переполнения: даже если Docker распухнет, система останется жива. Переносить надо с остановленным демоном и rsync -aP старого каталога.
- log-opts - вот лекарство от бесконечно растущих логов. max-size 20m, max-file 5 = на контейнер максимум 5 файлов по 20 МБ = жёсткий потолок 100 МБ. compress дожимает повёрнутые файлы. Без этого один болтливый сервис за месяц напишет гигабайты в json.log.
- default-ulimits - дефолтные лимиты для всех контейнеров. nofile (число открытых дескрипторов) часто упирается в дефолтный 1024 на нагруженных воркерах - поднимаем заранее, чтобы не ловить "too many open files".
- registry-mirrors - RU-реалия. Docker Hub бывает недоступен или душит по rate-limit. Прописываешь зеркало (корпоративное, Yandex/VK Container Registry как pull-through или публичные зеркала) - и docker pull идёт через него прозрачно.
- default-address-pools - из каких подсетей нарезать bridge-сети compose-проектов. Без этого Docker может занять подсеть, конфликтующую с твоим VPN или офисной.
Код: Выделить всё
dockerd --validate --config-file /etc/docker/daemon.json
Опция
Код: Выделить всё
"live-restore": trueЧто это даёт на практике: обновляешь Docker Engine или перезапускаешь демон - сетевые сервисы продолжают отвечать, простой нулевой. Что НЕ покрывает: рестарт самого хоста (тут контейнеры всё равно стартуют заново по своей restart policy) и смену мажорной версии демона. Ещё нюанс: пока демон лежит, ты не можешь управлять контейнерами через docker (логи, exec, stop) - они работают, но "вслепую", до возврата демона. Включать live-restore стоит почти везде в проде; единственное ограничение - он несовместим со swarm-режимом.
Регулярный prune и автоочистка
Ручная зачистка мусора - точечно и предсказуемо:
Код: Выделить всё
docker image prune # только dangling-образы (<none>)
docker image prune -a # все образы, не привязанные к контейнерам
docker builder prune # кеш сборки BuildKit
docker container prune # остановленные контейнеры
docker system prune # всё вместе, КРОМЕ томов
Код: Выделить всё
docker system prune -a --volumesКод: Выделить всё
# чистим образы старше 7 дней и build cache, тома не трогаем
docker image prune -af --filter "until=168h"
docker builder prune -af --filter "unused-for=168h"
Бэкап Docker-хоста и мониторинг демона
Что бэкапить, важно понимать концептуально: образы воспроизводимы (это артефакты сборки, они в registry), конфигурация в коде (Dockerfile, compose.yaml - в git). А вот состояние - в томах. Бэкапить надо тома и daemon.json, а не весь /var/lib/docker целиком (копировать overlay2 на живом демоне бессмысленно и опасно). Снять том через временный контейнер:
Код: Выделить всё
docker run --rm \
-v pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .
Мониторинг самого демона - чтобы узнавать о проблемах не от пользователей. Минимум метрик: жив ли сокет, свободное место на data-root, число контейнеров, логи демона. Быстрая проверка здоровья:
Код: Выделить всё
docker info --format '{{.ServerVersion}} containers={{.Containers}} running={{.ContainersRunning}}'
journalctl -u docker --since "1 hour ago" -p err
Код: Выделить всё
"metrics-addr": "127.0.0.1:9323"Грабли и антипаттерны из практики
- Нет log-opts - дефолтный json-file пишет логи без лимита. Самая частая причина "внезапно кончился диск". Ставь max-size/max-file в daemon.json в первый же день.
- rm -rf /var/lib/docker/overlay2 руками - так не чистят. Ты порвёшь метаданные, и демон сойдёт с ума. Только docker prune.
- Запись БД в слой контейнера вместо тома - бьёт и по производительности (CoW), и по сохранности (данные умрут с контейнером).
- system prune --volumes в кроне - тихий убийца данных. В автоматизации никогда.
- Один большой раздел под корень и Docker вместе - переполнение Docker валит всю систему. Выноси data-root на отдельный диск.
- Кривой daemon.json без валидации - демон не стартует, прод лежит. Всегда dockerd --validate перед reload.
- Сделай , затем
Код: Выделить всё
docker system df. Найди самый жирный образ и самый большой слой build cache. Прикинь, сколько освободит prune.Код: Выделить всё
docker system df -v - Создай /etc/docker/daemon.json с log-opts (max-size 10m, max-file 3) и live-restore true. Прогони , затем
Код: Выделить всё
dockerd --validate --config-file /etc/docker/daemon.json.Код: Выделить всё
systemctl reload docker - Запусти контейнер, дай ему пописать в stdout (например, ping в цикле). Через минуту посмотри размер файла логов в /var/lib/docker/containers/<id>/ и убедись, что ротация держит потолок.
- Подними тестовый том, положи в него файл, сделай бэкап-командой выше, удали том, восстанови из tar.gz, проверь, что файл на месте.
- Безопасно почисти мусор: и
Код: Выделить всё
docker image prune -af --filter "until=24h". Сравни docker system df до и после.Код: Выделить всё
docker builder prune -af
- Какие подкаталоги /var/lib/docker и за что отвечают, и почему overlay2 обычно самый жирный?
- Что делает live-restore, какие сценарии простоя он закрывает, а какие нет?
- Почему запись из контейнера лучше уводить в volume, а не оставлять в слое overlay2 - при чём тут copy-on-write?
- Чем опасен флаг --volumes у docker system prune и как построить безопасную автоочистку через крон?
Docker-хост в проде требует не магии, а гигиены. Держи в голове карту /var/lib/docker и регулярно смотри docker system df. Настрой daemon.json: log-opts против раздувания логов, live-restore против простоя при рестарте демона, default-ulimits и registry-mirrors под свои реалии. Понимай overlay2 и copy-on-write - тогда понятно, почему БД живёт в томе, а не в слое. Автоматизируй prune с возрастным фильтром и без --volumes, бэкапь тома (а не overlay2), мониторь демон и свободное место. Сделай это один раз нормально - и утренняя паника "диск кончился, контейнеры легли" тебя больше не навестит.