Пятница, вечер, сборка падает с ошибкой no space left on device. Мониторинг молчит, приложения работают, а диск кончился. Знакомая сцена для любого, кто держит сервер с контейнерами дольше полугода: Docker копит мусор тихо, вежливо, без единого предупреждения, и однажды выставляет счёт. Откуда столько, если приложений всего три? Всё просто: каждая пересборка оставляет слои, каждое обновление образа оставляет старую версию, каждая неудачная команда запуска оставляет остановленный контейнер, и всё это годами лежит в /var/lib/docker. Ниже полная ревизия: откуда берётся мусор, чем он отличается от нужного, какие команды убирают без риска и как поставить уборку на расписание так, чтобы никогда не вспоминать о ней вручную.
Куда Docker прячет гигабайты и диагностика system df
Мусор в Docker разнообразен, и у каждого вида своя причина появления. Образы копятся слоями: у каждого тега своя история слоёв, и после обновления со старого тега на новый прошлая версия остаётся лежать на диске, пока её явно не спросят. Пересборка проекта оставляет висячие слои без тегов, если собирать часто и под разными именами. Остановленные контейнеры держат свои записи в слое записи, даже когда мертвы годами. Анонимные тома создаются при каждом запуске с монтированием без имени и переживают свои контейнеры. Кэш сборщика копится от каждого docker build и умеет занимать больше места, чем сами образы. И отдельная статья расходов, о которой часто забывают: журналы контейнеров, по умолчанию пишущиеся без ограничений.
Полезно знать географию: слои образов живут в подкаталоге overlay2, тома в каталоге volumes, записи контейнеров с журналами в containers, где у каждого контейнера свой файл с суффиксом json.log. Глазами по этим каталогам ходить не нужно, но понимание, что journal контейнера это обычный файл на хосте, снимает половину мистики.
Первый шаг диагностики всегда одинаков и безвреден:
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 24 3 5.6GB 4.9GB (87%)
Containers 8 3 120MB 90MB (75%)
Local Volumes 12 4 2.1GB 800MB (38%)
Build Cache 31 0 1.2GB 1.2GB
Столбец RECLAIMABLE честно показывает, сколько можно вернуть, и уже одного взгляда хватает, чтобы понять масштаб. Детализация по объектам включается флагом подробного отчёта: там и имена кандидатов, и их размеры, и время создания. Для журналов есть короткая проверка средствами системы: du -sh /var/lib/docker/containers показывает, сколько съели логи, и чаще всего именно эта цифра объясняет загадочно исчезнувшие десятки гигабайт. Размер записи конкретного контейнера выводится флагом s у команды ps, и пара запущенных проектов с активным выводом в консоль способна объяснить остальное. Порядок цифр для ориентира: у активно разрабатываемого проекта за месяц без уборки сводка легко удваивается, и это норма, а не сигнал аварии.
Разница между висячими слоями и просто старыми образами
У чистки есть своя азбука, и путаница в ней стоит удалённых нужных вещей. Висячий образ это слой без тега, потерявший имя после пересборки: в списках он выглядит как <none> с пустым тегом, и его нельзя запустить по имени, потому что имени нет. Уникальность при этом никуда не делась, у слоя есть длинный идентификатор sha256, просто ссылаться на него удобно только кувалдой. Эти слои бесполезны почти всегда, и их можно убирать смело. Неиспользуемый образ это полноценный образ с тегом, к которому не подключено ни одного контейнера: старая версия приложения, снятая с боевого после обновления, запасная сборка на случай отката.
Разница принципиальная для стратегии. Висячие слои чистятся автоматически и бездумно, хоть каждую ночь: от них нет пользы по определению. Неиспользуемые образы трогать вслепую опасно: старая версия образа это ваш откат, и чистка с флагом all за минуты до инцидента отберёт путь назад. Практика выработала правило: свежесть удаляемого ограничивается фильтром по времени, например, не трогать ничего моложе недели, чтобы откатные версии жили достаточно долго для спокойствия.
Посмотреть кандидатов на выход перед уборкой можно глазами:
docker images -f dangling=true
docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}"
docker ps -a --filter status=exited
docker volume ls -f dangling=true
Первый список показывает висячие слои, второй печатает компактную таблицу всех образов с размерами для ревизии, третий и четвёртый дают картину по остановленным контейнерам и осиротевшим томам. Самое разумное первые месяцы удалять руками, сверяясь с этими списками, пока рука не набита.
Точечные команды prune для каждого типа мусора
Семейство команд prune у Docker устроено логично: каждая команда убирает свой тип мусора и ничего больше. Контейнеры, образы, сети, тома, кэш сборщика, у каждой свой инструмент:
docker container prune --filter "until=24h"
docker image prune
docker image prune -a --filter "until=168h"
docker builder prune --keep-storage 5GB
docker network prune
Без флагов команда чистки образа убирает только висячие слои, самый безопасный вариант. Флаг all расширяет охват на все образы без контейнеров, и вот тут включается фильтр времени, чтобы старые добрые версии образов не уезжали раньше недели. Чистка контейнеров с фильтром моложе суток оставляет недавно остановленные, которые ещё могут понадобиться при разборе поломки. Кэш сборщика умеет хранить лимит: заданный объём останется неприкосновенным запасом для быстрых пересборок, а лишнее сверху уедет. Результат каждой команды печатается тут же, строкой Total reclaimed space, так что эффект уборки виден без повторного запроса сводки.
Сети заслуживают отдельного объяснения, потому что копятся незаметнее всего. Каждый проект на compose создаёт свою сеть с именем по шаблону имя_проекта_default, эксперименты одной командой оставляют случайные имена из цифр, и за полгода таких сетей набирается на десятки. Неиспользуемая сеть не содержит ничего важного по определению, поэтому чистка безопасна: всё, что нужно живым контейнерам, не тронется.
Полную очистку одной командой выполняет system prune, но это инструмент кувалды:
docker system prune -a --volumes
Флаг all уберёт все образы без контейнеров, флаг томов заберёт и неиспользуемые тома, и в этой комбинации команда за один вечер способна лишить сервер и откатных версий, и заготовленных образов. Запускать её вслепую на боевом сервере можно только имея свежую копию всего важного и чёткое понимание, что именно должно уцелеть.
Фильтры until и label для безопасной уборки по расписанию
Фильтры превращают уборку из кувалды в скальпель. Фильтр времени until задаёт нижнюю границу возраста: обрабатываются только объекты старше заданного промежутка, а всё свежее остаётся нетронутым. Часы до суток для контейнеров, неделя для образов, неделя для кэша сборщика, и на расписании такие настройки дают серверу окно успокоения после каждого изменения. Фильтры в одной команде складываются: время и метка работают вместе, как логическое И, и сужают охват ещё точнее.
Фильтр по меткам защищает именованные объекты точечно. Критичные тома помечаются при создании:
docker volume create --label keep=true dbdata
docker volume prune --filter "label!=keep=true"
docker image prune -a --filter "until=168h" --filter "label!=keep=true"
Метка keep в фильтре с отрицанием говорит команде: трогай всё, кроме помеченного. Так именованные тома с базами переживают любую автоматическую уборку, потому что защита описана в самом объекте, а не в памяти администратора. Той же меткой защищают и образы внутреннего назначения, и остановленные контейнеры, которые нужны для отладки. Дисциплина простая: создаёшь что-то ценное, вешаешь метку, и расписание никогда не спросит об этом снова. Метки вешаются и на образы при сборке: флаг label в команде build помечает внутренние инструменты, и фильтр с отрицанием обходит их так же, как тома.
Особая осторожность с томами и остановленными контейнерами
Тома стоят особняком во всей этой экономике, потому что в них живёт содержимое, а не код. Образ можно перекачать из реестра за минуты, контейнер пересоздаётся командой, а том с базой, потерянный уборкой, восстанавливается только из копии, если копия есть. Правило выжжено инцидентами: чистка томов по расписанию не запускается никогда, ревизия томов проводится руками и с открытым списком.
Ревизия укладывается в три команды и полчаса внимания:
docker volume ls
docker volume inspect dbdata
docker volume ls -f dangling=true
Список всех томов, затем подробности по каждому подозрительному: точка монтирования, метки, время создания. Анонимные тома, оставшиеся от удалённых контейнеров, убираются штатной чисткой, а именованные остаются до решения человека. У проектов на compose есть своя тонкость: команда остановки с флагом томов сносит и анонимные, и объявленные в файле тома проекта за один мах, поэтому разница между down и down с флагом томов это разница между уборкой кухни и сносом дома.
То же касается контейнеров в остановленном состоянии: перед генеральной уборкой список docker ps -a просматривается глазами, и всё, что похоже на забытый инструмент отладки, либо запускается заново, либо удаляется осознанно.
Отдельная строка заботы: журналы контейнеров, которые очистка не трогает вообще. Ограничение задаётся в конфигурации службы:
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
После перезапуска службы каждый контейнер получит потолок журнала в три файла по десять мегабайт, и пункт из списка пропавших гигабайт исчезнет сам собой. Настройка применяется к новым контейнерам, поэтому старые долгожители после правки стоит пересоздать, чтобы потолок достался и им.
Скрипт для cron с журналом и разбором результатов
Автоматизация собирается из уже знакомых команд в один сценарий с журналом. Порядок еженедельной уборки без риска для содержимого выглядит так:
- Снимок system df до уборки пишется в журнал для сравнения;
- Контейнеры старше двух суток уходят через container prune с фильтром;
- Висячие образы и образы старше недели убираются с фильтром времени;
- Кэш сборщика подстригается до лимита keep-storage;
- Тома остаются нетронутыми, их ревизия проводится руками раз в месяц.
Сам скрипт живёт по пути /usr/local/sbin/docker-prune.sh:
#!/bin/bash
echo "=== $(date) ==="
docker system df
docker container prune --filter "until=48h" --force
docker image prune -a --filter "until=168h" --force
docker builder prune --keep-storage 5GB --force
docker network prune --force
docker system df
Права на исполнение и расписание:
sudo chmod +x /usr/local/sbin/docker-prune.sh
echo "0 4 * * 0 root /usr/local/sbin/docker-prune.sh >> /var/log/docker-prune.log 2>&1" | sudo tee /etc/cron.d/docker-prune
Каждое воскресенье в четыре утра сервер сам снимает пробу до и после, и журнал показывает динамику одной строкой: сколько было, сколько стало, что именно уехало. Полезная привычка: раз в месяц пробегать журнал глазами, сверяя цифры до и после, и любые аномалии, вроде внезапно выросшего съеденного кэша, обсуждать до того, как они станут пятничной аварией. Для нетерпеливых есть и путь автоматических оповещений: скрипт сравнивает цифры до и после и присылает письмо при скачке, но первым месяцам достаточно и глаз. Чтобы журнал сам не превратился в помойку, рядом кладётся правило ротации в /etc/logrotate.d с недельным циклом и лимитом в несколько сжатых копий, и цикл замыкается: уборщик следит за диском, ротация следит за уборщиком.
Финальная мысль простая: чистый сервер это не разовая акция, а привычка расписания. Метки защищают ценное, фильтры ограничивают аппетиты автоматической уборки, журнал превращает невидимую работу в видимую, а понедельничный взгляд на цифры подтверждает, что пятничная драма больше не повторится. Docker копит мусор без устали, но и убирается без каприза, если приказы отданы заранее и в правильном порядке. Гигиена тегов закрывает вопрос на корню: вместо latest осмысленные версии, и мусор перестаёт рождаться.