Избранное
flotsam
Чистка Docker-диска, которая знает, от какого проекта остался каждый объект — индекс «проект → каталог», честная арифметика слоёв и dry run по умолчанию.
- Go 1.25
- Docker SDK v28
- SQLite (modernc.org)
- Cobra
- Vue 3 + Vite
- GitHub Actions
- ghcr.io multi-arch
Проблема
docker system df с готовностью сообщит, что ушло 84 ГБ. Но он не отвечает на единственный вопрос, который важен перед удалением:
Вот этот том — от какого он проекта, и можно ли его снести, не потеряв данные?
Как только контейнеры проекта удалены, его тома становятся сиротами. На них всё ещё висит лейбл com.docker.compose.project, но нигде не записано, где проект лежал на диске — и Docker эту связь уже не восстановит. Выбор вырождается в docker system prune -a (снести всё и надеяться) либо «пусть лежит ещё пару лет».
Flotsam — это то, что всплывает после того, как корабль пошёл ко дну. Ровно это тулза и собирает.
Что она знает такого, чего не знает Docker
flotsam ведёт собственный индекс: проект → каталог, в SQLite (чистый Go, без CGO), наполняемый пассивно на каждом запуске из лейбла com.docker.compose.project.working_dir — то есть пока контейнеры ещё существуют. Спустя месяцы, когда контейнеров давно нет, индекс всё ещё отвечает:
🟢 legacy-crm 12.4 GB ⚠ каталог отсутствует
/home/v/work/legacy-crm (последний раз 2025-11-03, источник: index)
├─ volume legacy-crm_pgdata 8.1 GB postgresql 🟡 содержит данные БД
├─ volume legacy-crm_uploads 3.9 GB unknown-data 🟢 каталог проекта удалён
└─ image legacy-crm-app:latest 340 MB 🟢 каталог проекта удалён
Разрешение имени — цепочка с явным «неизвестно» в конце: живой лейбл → индекс → скан файловой системы → unknown. А read-only проба монтирует том и говорит, что внутри на самом деле: postgres, mysql, mongo, redis или неопознанные данные. Потому что «8 ГБ чего-то» — не основание для решения.
Честная арифметика освобождения
Удаление пяти образов не освобождает сумму их размеров: они делят слои. flotsam строит граф слоёв и считает слой освобождаемым, только если все ссылки на него лежат внутри удаляемого набора, а образы, запиненные запущенными контейнерами, исключаются.
Самое интересное было в сопоставлении записей ImageHistory со слоями RootFS.Layers. Очевидное правило — брать записи с Size > 0 — дало на живой машине всего 55 % совпадений. Причина: WORKDIR создаёт настоящий слой нулевого размера. Классификация записи по CreatedBy (RUN/COPY/ADD/WORKDIR дают слой; ENV/CMD/ENTRYPOINT — нет; #(nop) — нет) плюс relaxed-проход для образов от bazel/ko подняли цифру до 99,4 % — 333 образа из 335 совпали точно.
Модель риска
Классификация — 18 упорядоченных правил, побеждает первое сработавшее, а всё, что не совпало ни с чем, по умолчанию получает risky, а не «наверное, можно»:
| Уровень | Что значит | |
|---|---|---|
| 🟢 | safe | Каталог проекта удалён, dangling-образ, пустой том, давно остановленный контейнер |
| 🟡 | probably-safe | Проект спит > 30 дней, свежий неиспользуемый образ, свежий build cache |
| 🔴 | risky | В томе данные БД, проект активен, происхождение неизвестно, путь неоднозначен |
| ⛔ | never | Защищён конфигом, том подключён к контейнеру, образ запущенного контейнера |
risky и never никогда не попадают в план, какие бы флаги вы ни передали. Красный объект удаляется только по одному, по полному имени, с --force-risky.
Гарантии безопасности
applyпо умолчанию — dry run; реальное удаление требует--no-dry-run.- Том с
RefCount > 0не удаляется никогда — даже с--force-risky. - Инвентарь пересобирается прямо перед удалением; всё, что изменилось за это время, пропускается с предупреждением.
- Никакого
docker system prune -a— только точечные удаления по ID. - Ничего вне Docker не трогается: ни каталоги проектов, ни образ диска ВМ.
flotsam hostпечатает команды для сжатияDocker.raw/ext4.vhdx; запускаете их вы. - Каждая операция дописывается в
~/.local/share/flotsam/history.jsonl. - Каталог считается «отсутствующим», только если виден его родитель — отмонтированное поддерево не будет принято за удалённый проект.
Последнее правило — не теоретическое. На macOS и Windows -v /:/host:ro монтирует корень виртуальной машины Docker Desktop, а не ваш диск: /host/Users существует и пуст. Без правила «виден ли родитель» контейнерная сборка объявила сиротами 8 из 12 живых проектов. Теперь flotsam распознаёт ситуацию, помечает хостовую ФС как непроверяемую, отказывается называть что-либо сиротой и прямо об этом пишет — вместо того чтобы приглашать удалить живые данные.
Поставка
По одному статическому бинарю на платформу, всего шесть (linux/darwin/windows × amd64/arm64), собираются GitHub Actions на теге v*, с checksums.txt в релизе; мультиархитектурный образ 22 МБ на ghcr.io; и Vue 3 SPA, вшитая в бинарь, поэтому go build не требует Node — CI падает, если закоммиченный бандл разошёлся с исходниками. Всё, что умеет CLI, доступно в браузере на 127.0.0.1:7333: по токену, с валидацией Host/Origin и без кук.
Выводы
- Ценные данные никогда не лежали в Docker: это связь «проект → каталог», и снять её нужно было до того, как она понадобится. Пассивный индекс не стоит ничего на запуске — и в нём вся фича.
- Бенчмарк из ТЗ — это гипотеза. Описанная эвристика сопоставления слоёв на реальных образах работала на 55 %; честной цифру «сколько на самом деле освободится» сделала только классификация по
CreatedBy. - Когда инструмент чего-то проверить не может — например, не видит хостовую ФС — правильный вывод это отказ, а не догадка. Инструментам удаления оптимизм не положен.