.git у mailcow вырос до 13 ГБ: как найти объекты
АйТи Фреш
Linux, Docker и DevOps

Почему служебный .git у mailcow разросся до 13 ГБ и как найти в нём крупные объекты

Автор: , директор ООО «АйТи-Фреш» · · ~13 мин чтения
Каталог .git установки mailcow занимает 13 ГБ на диске из-за накопленной истории обновлений
Служебная папка .git незаметно съела диск, пока почта и база работали как обычно.

Если du -sh на mailcow-dockerized упирается в разгадку — не почта и не логи, а служебный .git весом 13 ГБ при рабочей копии меньше 100 МБ — вы не одиноки: это известная особенность многолетних установок. Разбираю, откуда берётся такой объём и как найти и вычистить крупные объекты, не сломав обновления.

Симптом: диск забит, а виноват не vmail и не MySQL

Кинотеатр «Большой экран» — 50 рабочих мест, касса, бухгалтерия и рассылки афиши идут через собственный mailcow, который мы держим на аутсорсе корпоративной почты с момента установки три года назад. В сентябре сработал алерт на нехватку места на системном диске VPS: свободно оставалось меньше 2 ГБ, а vmail (сами письма) и база MySQL по объёму совпадали с тем, что было месяц назад — рост шёл где-то в другом месте.

du -h --max-depth=1 /opt/mailcow-dockerized | sort -h быстро показал виновника (именно так, без звёздочки: шаблон * в bash не захватывает скрытые каталоги вроде .git): каталог .git внутри установки весил 13,4 ГБ, при том что вся рабочая копия репозитория (сами скрипты, шаблоны конфигов, docker-compose.yml) занимала меньше 100 МБ. Для сравнения: свежий git clone того же репозитория mailcow-dockerized занимает около 50–55 МБ на .git. То есть служебная папка распухла почти в 250 раз относительно нормы, при этом сама почтовая система работала штатно — проблема была чисто дисковая.

Первая мысль администратора кинотеатра была — снести всё и переустановить mailcow с нуля с восстановлением из бэкапа. Отговорил: во-первых, это часы простоя почты кассы и бухгалтерии ради проблемы, которая решается без переустановки за полчаса; во-вторых, переустановка никак не гарантирует, что через год-два .git снова не раздуется точно так же — причина в том, как установка живёт с git при обновлениях, а не в конкретных файлах.

Сравнение размера каталога .git свежей установки mailcow и установки после многолетних обновлений
Одна и та же рабочая копия файлов — и разница в размере .git почти в четверть тысячи раз.

Откуда берётся такой объём — это не баг, а следствие апдейтов через git pull

Mailcow обновляется скриптом update.sh, и git для него — рабочий механизм, а не просто история. Скрипт делает git fetch origin, перед обновлением фиксирует локальное состояние коммитом «Before update on <дата>», затем выполняет git merge -Xtheirs -Xpatience с сообщением «After update on <дата>». То есть в локальном .git копятся и апстрим-история, и ваши собственные коммиты с локальными правками. Самообслуживание git держится на git gc --auto: по Pro Git он срабатывает, когда рыхлых объектов около 7000 или pack-файлов больше 50. Если же git gc, repack или fetch прерываются (кончилось место, оборвалась сеть, сервер перезагрузили посреди обновления), в .git/objects/pack/ остаются временные файлы tmp_pack_* — и их никто за вас не удалит.

В issue #6393 репозитория mailcow-dockerized автор описывает практически идентичную картину: du -ms показал 13 502 МБ в .git при каталоге установки меньше 100 МБ, а свежий клон — 52 МБ. git status и git fsck никаких повреждений не находили, а git repack завершился ошибкой из-за нехватки места — полная переупаковка требует места для двух копий данных: старых pack-файлов и новых, которые собираются взамен. Показательный комментарий в том же тикете: у другого пользователя в списке крупнейших файлов первой строкой стоял .git/objects/pack/tmp_pack_… на 21 ГБ, и он продолжал расти.

Отдельно уточню: это не значит, что update.sh сам по себе работает неправильно. У мейнтейнера в том же обсуждении установка, файловая система которой создана в 2017 году, держит .git на 73 МБ, а на других долгоживущих установках git gc ужимал каталог с 87–113 МБ до 39–40 МБ. Разработчики пометили тикет как not-a-bug, а в июле 2025 года он закрылся ботом как устаревший. Моё прочтение: гигабайты вырастают не от «нормальной» истории, а от прерванных операций git на сервере, где место и так на исходе, — и тогда рост сам себя подпитывает.

Почему git repack сам себя запирает в тупик

Это ключевой момент, который стоит понимать перед тем, как лезть чистить .git руками: полный repack — операция, которая временно удваивает потребление места. Как объясняет инженерный блог GitHub про внутреннее устройство хранилища объектов, при полной пересборке (git repack -a -d -f) git создаёт новый packfile рядом со старыми, и только после успешного завершения удаляет исходные объекты — если на диске хватает места на обе копии одновременно, операция падает, а диск к тому моменту уже почти полностью забит тем самым разросшимся .git, который вы пытаетесь почистить.

Там же разобрана разница между полным и инкрементальным repack: задача incremental-repack в git maintenance собирает мелкие pack-файлы ниже фиксированного порога (по умолчанию два гигабайта) в общий, не трогая уже большие паки, — это требует заметно меньше временного места, но и не даёт максимального сжатия истории. Для ситуации с забитым диском я всегда начинаю не с агрессивных флагов, а с того, что можно удалить без переупаковки вовсе: брошенные tmp_pack_*. И только потом — git gc, когда под него появилось место.

Как найти, какие именно объекты занимают больше всего места

Прежде чем чистить наугад, смотрю общую статистику объектной базы:

cd /opt/mailcow-dockerized
git count-objects -v

Вывод показывает count и size (рыхлые объекты вне паков), in-pack и size-pack (объекты внутри packfile) — по этим цифрам сразу видно, раздута история именно в packfile или накопился мусор из рыхлых объектов, который вообще не был упакован ни разу. Дальше нахожу самые тяжёлые объекты внутри существующих pack-файлов:

git verify-pack -v .git/objects/pack/pack-*.idx | sort -k 3 -n | tail -15

Третья колонка в выводе verify-pack — это размер объекта в байтах, поэтому сортировка по ней и tail сразу дают список самых крупных blob-ов. Дальше по хэшу конкретного объекта ищу, какому файлу и в каком коммите он принадлежал:

git rev-list --objects --all | grep <хэш-объекта>

Но сначала — просто ls -lh .git/objects/pack/. Если рядом с парами pack-*.pack/pack-*.idx лежат файлы tmp_pack_* на гигабайты, дальше можно не копать: это недособранные паки от прерванных операций, в историю они не входят. А вот verify-pack на установках mailcow обычно показывает скромную картину: самые крупные объекты в актуальном дереве — единицы мегабайт (в обсуждении #6393 это, например, swagger-ui-bundle.js.map около 2 МБ). Сотни таких версий за годы дают десятки мегабайт, но не гигабайты.

У «Большого экрана» так и вышло. git count-objects -v показал size-pack около 180 МБ — для установки трёхлетней давности многовато, но не 13 ГБ. Остальное лежало в .git/objects/pack/ в виде четырёх файлов tmp_pack_* общим объёмом 13,2 ГБ с датами прошлых плановых обновлений: в двух случаях update.sh запускался, когда диск уже был заполнен больше чем на 90 %, и фоновая упаковка не доходила до конца. Ни один из этих файлов не нужен для работы установки — это мёртвый груз, который git fsck не считает ошибкой.

Пошаговая схема поиска крупных объектов в раздутом каталоге .git через git count-objects и git verify-pack
Четыре команды подряд — и вместо гадания видно, какой именно объект занимает гигабайты.

Что можно и что нельзя чистить в .git своей установки mailcow

Важное предостережение: .git в директории mailcow — не мусор, а репозиторий, через который update.sh понимает, какая ветка у вас стоит, сравнивает её с апстримом и сливает обновления поверх ваших локальных правок. Полное удаление .git без замены сломает штатное обновление. А замена на свежий клон, хоть и рабочая, выбрасывает локальные коммиты «Before update on …» — историю того, что вы меняли в файлах установки. Если правок не было — не страшно; если были, их надо сначала сохранить.

Отдельно предупреждаю клиентов: не путайте чистку .git (истории установочного репозитория) с резервным копированием самой почты через backup_and_restore.sh — это два независимых механизма про совершенно разные данные. Проблемы с нулевым размером архива бэкапа, которые я разбирал в статье про mailcow-бэкап .tar.zst нулевого размера, никак не связаны с раздутым .git — не нужно связывать эти две диагностики между собой, если на сервере одновременно обнаружились обе проблемы, что бывает, когда за диском давно не следили.

Автор issue #6393 в итоге поступил радикально: сделал git clone в подкаталог, удалил старый .git, переместил новый на его место и проверил git status — и сам назвал это «очень плохим решением», хотя оно сработало. Мейнтейнер в ответ предложил штатный путь: git reflog expire --expire=now --all и git gc --prune=now --aggressive. На нескольких установках это ужало .git до 39–40 МБ — но gc тоже нужно место, поэтому на забитом диске сначала освобождаю его. Порядок у меня такой: удалить брошенные tmp_pack_*, затем gc, и только если это не помогло — замена на свежий клон с сохранённым патчем локальных правок.

Дерево решений для очистки раздутого каталога .git в установке mailcow в зависимости от свободного места на диске
Сначала убираем брошенные временные паки и gc, свежий клон — только крайняя мера.

Как я делаю это на проде клиента без риска сломать обновления

У «Большого экрана» свободных оставалось меньше 2 ГБ, поэтому начал с самого дешёвого шага — убрал временные паки. Делаю это только при остановленном стеке и убедившись, что никакой процесс git не запущен (pgrep -a git пусто), иначе можно удалить файл, который прямо сейчас пишется:

cd /opt/mailcow-dockerized
docker compose down
pgrep -a git || echo 'git не запущен'
ls -lh .git/objects/pack/tmp_pack_*
rm -f .git/objects/pack/tmp_pack_*
git fsck --no-dangling

После этого освободилось больше 13 ГБ, и под штатную сборку мусора места стало с запасом. Дальше — то, что советовал мейнтейнер в #6393, плюс контрольные проверки. Если бы gc не помог, я бы сохранил локальные правки через git diff origin/master > /root/mailcow-local.patch и только потом менял .git на свежий клон:

git reflog expire --expire=now --all
git gc --prune=now --aggressive
git count-objects -vH
git status
docker compose up -d

После чистки .git весил 61 МБ вместо 13,4 ГБ, git status показал чистое дерево без расхождений с рабочей копией, локальные коммиты предыдущих обновлений остались на месте, а следующий плановый update.sh неделю спустя отработал штатно. Весь простой почты кассы и бухгалтерии — около двадцати минут, из которых большая часть ушла на gc --aggressive.

Что я теперь мониторю, чтобы не доводить до критичного объёма

Раздутый .git — штука тихая: он не ломает почту, не появляется в логах ошибок, не триггерит стандартные проверки здоровья mailcow, поэтому легко не заметить его годами, пока диск не начнёт заканчиваться в неподходящий момент — например, прямо во время планового обновления, которому самому нужно немного свободного места для временных файлов.

С похожей логикой я слежу и за другими каталогами, которые растут незаметно годами: например, разбирал отдельно, куда уходят гигабайты в Zimbra перед переездом — там тоже цифры на диске не совпадают с интуитивным ожиданием, пока не посчитаешь по каждому подкаталогу отдельно, а не полагаешься на общий процент занятости раздела.

На новых установках, которые разворачиваю с нуля, сразу добавляю в регламент клиента пункт про размер .git наравне с проверкой свободного места под vmail и MySQL — это занимает одну строку в ежеквартальном отчёте, но избавляет от ситуации, когда диск заканчивается посреди рабочего дня без предупреждения. Дешевле потратить пять минут раз в три месяца, чем разбираться с авральной чисткой на проде, где почта уже недоступна из-за забитого раздела.

Частые вопросы

Это баг mailcow, который когда-нибудь починят?

Скорее нет. Issue #6393 с этим симптомом помечен мейнтейнерами как not-a-bug и в июле 2025 года закрыт ботом как устаревший. Гигабайты обычно дают прерванные операции git (брошенные tmp_pack_*), а не сам механизм обновления.

Можно просто удалить .git и продолжать обновляться?

Нельзя без замены. update.sh опирается на git: сравнивает ветку с апстримом и сливает обновления поверх локальных правок. Сначала попробуйте удалить брошенные tmp_pack_* и выполнить git gc; замена .git на свежий клон — крайняя мера, и перед ней сохраните git diff origin/master.

Почему git repack падает с ошибкой нехватки места, если места на диске вроде есть?

Полная переупаковка (git repack -a -d -f) на время операции требует места для двух копий объектной базы: старых pack-файлов и новых, которые собираются им на замену. Если .git уже занимает большую часть диска, свободного места на вторую копию физически не остаётся.

Как понять, что именно раздувает .git — не потеряю ли я какие-то нужные данные при замене?

Почта, ящики и база в .git не хранятся — там история файлов установки (скрипты, шаблоны, docker-compose.yml) и ваши локальные коммиты, которые update.sh делает перед каждым обновлением. ls -lh .git/objects/pack/, git count-objects -v и git verify-pack покажут, что занимает место; удаление tmp_pack_* и git gc историю не теряют, а замена на свежий клон теряет локальные коммиты.

Нужно ли останавливать mailcow перед заменой .git?

Да, я останавливаю стек (docker compose down) на время операции — она короткая (пара минут), но лучше исключить любую вероятность, что update.sh или другой процесс параллельно обращается к репозиторию, пока вы меняете .git местами.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи