Почему служебный .git у mailcow разросся до 13 ГБ и как найти в нём крупные объекты
Если 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 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 своей установки 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, и только если это не помогло — замена на свежий клон с сохранённым патчем локальных правок.
Как я делаю это на проде клиента без риска сломать обновления
У «Большого экрана» свободных оставалось меньше 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 — это занимает одну строку в ежеквартальном отчёте, но избавляет от ситуации, когда диск заканчивается посреди рабочего дня без предупреждения. Дешевле потратить пять минут раз в три месяца, чем разбираться с авральной чисткой на проде, где почта уже недоступна из-за забитого раздела.
- Раз в квартал — du -sh /opt/mailcow-dockerized/.git отдельной строкой в отчёте по серверу
- Алерт на заполнение диска не по проценту, а по абсолютным свободным гигабайтам (2 ГБ свободных — это мало для repack, даже если это 5 % диска)
- После каждого крупного обновления mailcow — git count-objects -v, если size-pack заметно растёт от месяца к месяцу
- Держать актуальный бэкап через backup_and_restore.sh отдельно от .git — это разные вещи, второе не заменяет резервную копию почты
- На новых установках сразу закладывать отдельный том под /opt, чтобы разрастание .git не конкурировало за место с почтовыми ящиками на одном разделе
- Проверка ls -lh .git/objects/pack/ на брошенные tmp_pack_* — особенно после обновления, которое прервалось или шло на почти полном диске
Частые вопросы
Это баг 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 местами.
Источники
- GitHub issue #6393 — Huge git repository — Проверил цифры (13 502 МБ .git против 52 МБ у свежего клона), git status/fsck без ошибок, падение repack из-за места, совет мейнтейнера (reflog expire + gc --prune=now --aggressive), комментарий про tmp_pack_ на 21 ГБ, метки not-a-bug/stale и закрытие 27.07.2025. https://github.com/mailcow/mailcow-dockerized/issues/6393
- Pro Git — Git Internals: Maintenance and Data Recovery — Проверил синтаксис и назначение git count-objects -v (поля count/size/in-pack/size-pack), условия автозапуска git gc --auto (~7000 объектов, 50+ паков), команды git verify-pack -v и git rev-list --objects для поиска крупных объектов по хэшу. https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery
- GitHub Engineering Blog — Git's database internals I: packed object store — Проверил: полная переупаковка требует места под две копии объектной базы; incremental-repack в git maintenance собирает паки ниже порога 2 ГБ. https://github.blog/open-source/git/gits-database-internals-i-packed-object-store/
- mailcow-dockerized update.sh (master, GitHub) — Сверил git-операции update.sh: git fetch origin, коммит «Before update on DATE», git merge -Xtheirs -Xpatience «After update on DATE». https://github.com/mailcow/mailcow-dockerized/blob/master/update.sh



