Удалил в mailcow ящики на десятки гигабайт — место не освободилось: сколько времени есть на отмену
Удалили в mailcow ящики на пару десятков гигабайт, а `df -h` показывает те же цифры, что и вчера — это не баг и не утечка места, а штатное поведение: удалённые ящики переносятся в каталог _garbage на том же томе и по умолчанию пять суток лежат там зашифрованными, прежде чем сборщик мусора их окончательно сотрёт. Разбираю, где физически лежат эти данные, как поменять срок хранения и какая команда снесёт корзину вместе со всей остальной почтой, если запустить её не подумав.
Кейс: художественная школа почистила базу выпускников и испугалась счётчика диска
Художественная школа «Юный художник» — небольшая, 10 рабочих мест, но переписки много: родители присылают сканы работ на конкурсы, фотографии готовых картин по 15-20 МБ за письмо, переписку с музеями и галереями по выставкам. Раз в год перед началом учебного года администратор школы чистит почтовые ящики выпускников, которые больше не нужны — обычно это два-три ящика, но в этот раз накопилось пять за два года, и суммарный объём вложений в них перевалил за 30 ГБ.
Мы ведём для школы корпоративную почту на mailcow, и администратор школы сама удалила эти пять ящиков через веб-интерфейс — права администратора домена это позволяли. Через десять минут она написала нам: место на диске сервера не изменилось ни на гигабайт, хотя ящики из интерфейса пропали полностью, будто их никогда и не было.
Первая реакция администратора была вполне логичной для человека без опыта работы с почтовыми серверами — она решила, что удаление «зависло» или прошло не до конца, и уже собиралась повторить операцию по тем же ящикам ещё раз через другой раздел интерфейса. Хорошо, что она сначала написала нам, а не стала экспериментировать: повторное удаление уже отсутствующих в интерфейсе ящиков ничего бы не ускорило, а вот причину нехватки видимого результата стоило разобрать по существу.
Почему место не освобождается сразу: garbage collector и MAILDIR_GC_TIME
В mailcow удалённые домены и ящики не стираются с диска немедленно. Вместо этого письма физически переносятся во внутреннюю корзину, а окончательное удаление выполняет отдельный процесс уборки мусора по расписанию — стандартное и задокументированное поведение, описанное в разделе документации mailcow про восстановление случайно удалённых данных.
Срок, на который данные задерживаются в корзине перед окончательным стиранием, задаётся переменной MAILDIR_GC_TIME в mailcow.conf. Значение указывается в минутах, а не в днях или часах — это первое, на чём спотыкаются администраторы, которые пытаются поменять срок хранения и подставляют туда, например, «5» вместо пяти дней в минутах.
По умолчанию MAILDIR_GC_TIME=7200 — это ровно 5 суток (7200 минут = 120 часов). Комментарий в шаблоне mailcow.conf (скрипт generate_config.sh) поясняет прямым текстом: удалённые домены и ящики переносятся в /var/vmail/_garbage/<метка времени>_<имя>, срок их хранения задаётся этим параметром в минутах, а проверка выполняется периодически. В комментарии написано «раз в час», но в актуальном docker-compose.yml задание dovecot_maildir_gc в планировщике ofelia запускается каждые 30 минут. Сам скрипт maildir_gc.sh внутри dovecot-mailcow делает простую вещь: find /var/vmail/_garbage/ -mindepth 1 -maxdepth 1 -type d -cmin +${MAILDIR_GC_TIME} -exec rm -r {} \; — стирает каталоги старше заданного числа минут.
Отсюда и ответ на «почему место не освободилось»: удаление ящика в mailcow — это перенос каталога внутри того же тома vmail-vol-1, а перенос внутри одной файловой системы не освобождает ни байта. Место вернётся только тогда, когда очередной проход сборщика увидит, что каталог старше MAILDIR_GC_TIME, и удалит его.
Где физически лежат «удалённые» данные и почему они зашифрованы
Данные из удалённого ящика не исчезают и не переезжают в какое-то абстрактное «облако корзины» — они остаются на том же диске, в томе Docker, который использует Dovecot для всей почты. Документация mailcow прямо указывает путь: /var/lib/docker/volumes/mailcowdockerized_vmail-vol-1/_data/_garbage. Внутри — папки, названные по шаблону с временной меткой и санированным именем домена и пользователя, то есть по имени сразу видно, что и когда было удалено.
Важный нюанс: данные внутри _garbage остаются зашифрованными. mailcow шифрует письма на диске плагином Dovecot mail_crypt, а ключи лежат в отдельном томе crypt-vol-1. Документация отдельно предупреждает: восстанавливать нужно в тот же самый mailcow, из которого удаляли, либо с теми же ключами из crypt-vol-1. Просто скопировать папку из _garbage на другой сервер и рассчитывать прочитать письма не получится — без правильных ключей это нечитаемый набор байтов.
Именно поэтому «место не освободилось» — это буквально правда в двух смыслах сразу: данные физически лежат на диске, и они всё ещё зашифрованы теми же ключами, что и живая почта, а не превратились в обычный текстовый архив, который можно было бы просто перенести или заархивировать вручную для экономии места.
Сколько есть времени на отмену и как изменить срок хранения
По умолчанию у администратора есть 5 дней на то, чтобы передумать и восстановить случайно удалённый ящик — это тот самый интервал, который задаёт MAILDIR_GC_TIME=7200. Для школы это было и хорошо, и не очень: хорошо, потому что решение об удалении выпускников было обдуманным и повторно проверенным, а не очень — потому что 30 ГБ лежали в корзине лишние пять дней, пока администратор ждала, что место освободится само.
Если вам нужно сократить этот срок — например, диск сервера близок к заполнению и вы не собираетесь ничего восстанавливать — значение можно уменьшить прямо в mailcow.conf:
# mailcow.conf, например 24 часа вместо 5 суток
MAILDIR_GC_TIME=1440
docker compose up -dОбратная сторона: в GitHub-трекере mailcow есть issue #5274 (июнь 2023), где пользователь предлагал снизить срок хранения по умолчанию вплоть до нуля — у него корзина заняла 3 ГБ из 20 ГБ тома и забила диск. Issue закрыли со статусом not planned, дефолт так и остался 7200 минут. И я с этим согласен: 5 суток — разумный компромисс между защитой от ошибки администратора и расходом места, а понижать значение стоит осознанно, а не потому что «жалко места прямо сейчас».
- MAILDIR_GC_TIME задаётся в минутах, а не в днях
- значение по умолчанию — 7200 минут, то есть 5 суток
- сборщик мусора запускается периодически (в актуальном docker-compose.yml — каждые 30 минут), а не сразу после удаления
- уменьшение значения сокращает и место, занятое корзиной, и время на отмену ошибки
Как восстановить письма из корзины, если поторопились удалить
Если решение удалить ящик оказалось ошибкой и вы укладываетесь в срок MAILDIR_GC_TIME, восстановление возможно, но не одной кнопкой — это ручная процедура. Первый шаг документация формулирует однозначно: нужно заново создать в mailcow тот же самый ящик (тот же адрес), прежде чем переносить данные обратно, потому что переносить письма буквально некуда без существующей учётной записи.
Дальше папки из _garbage/[метка_времени]_[домен][пользователь] копируются обратно в обычное расположение ящика в томе vmail-vol-1 — вручную, с сохранением структуры каталогов Maildir. После копирования файлов Dovecot ещё не знает о них — нужно заставить его переиндексировать содержимое ящика и пересчитать квоту:
docker compose exec dovecot-mailcow doveadm force-resync -u user@example.com '*'
docker compose exec dovecot-mailcow doveadm quota recalc -u user@example.comДля школы этот сценарий, к счастью, не понадобился — удаление выпускников было решением, а не ошибкой, поэтому мы просто дождались истечения MAILDIR_GC_TIME и убедились по df -h и du каталога _garbage, что место освободилось штатно. Но я держу эту процедуру под рукой на каждый проект с mailcow именно на случай, если кто-то из клиентов удалит не тот ящик — пять дней страховки того стоят.
После force-resync письма в клиенте могут появиться не мгновенно, а с небольшой задержкой — это нормально, Dovecot заново строит внутренние индексы папки. Если после восстановления ящик долго показывает пустые папки или старое содержимое, прежде чем подозревать саму процедуру восстановления, стоит свериться с тем, почему Dovecot иногда не показывает письма, которые уже есть на диске — там разобраны похожие симптомы, но по другой причине.
Отдельно напомню: восстановление из _garbage работает только на том же сервере mailcow и с теми же ключами шифрования. Если вы планируете держать архив старой переписки отдельно — не в корзине, а как полноценную резервную копию за пределами живого сервера — это уже другая задача, и я разбирал её отдельно применительно к тому, почему простой копии vmail недостаточно для чтения архива: без ключей crypt-vol-1 скопированные файлы так же нечитаемы, как и содержимое корзины на чужом сервере.
Самая опасная ошибка на этом пути: docker compose down -v
Когда администратор в панике из-за «не освобождающегося места» ищет быстрое решение, в интернете легко наткнуться на совет «снести volumes и поднять заново» — это docker compose down -v (или docker-compose down -v для отдельно стоящей версии). Документация mailcow по удалению постоянных данных описывает это предельно прямо: команда уничтожает все тома mailcow целиком — mysql-vol-1 со всей базой данных, redis-vol-1, весь vmail-vol-1 (то есть вообще все письма, не только удалённые), rspamd-vol-1 и crypt-vol-1.
Отдельное и самое серьёзное предупреждение — про crypt-vol-1: удаление этого тома делает нечитаемыми абсолютно все письма на сервере, потому что это том с ключами шифрования, а не сами письма. Даже если у вас есть резервная копия vmail-vol-1 в виде файлов, без соответствующих ключей crypt-vol-1 эта резервная копия превращается в бесполезный набор зашифрованных данных.
Для школы с десятью рабочими местами и архивом переписки с музеями за несколько лет это было бы катастрофой несопоставимого масштаба по сравнению с исходной проблемой «жалко пять дней ждать освобождения места». Я всегда объясняю клиентам разницу между docker compose down -v (уничтожает вообще всё, включая живую почту) и штатным истечением MAILDIR_GC_TIME (стирает только то, что уже удалено и просрочено) — это две вещи, которые случайно спутать стоит слишком дорого.
Что делать вместо паники: практический план на случай нехватки места
Если диск сервера действительно заполняется и пять дней ожидания критичны, правильный порядок такой: сначала проверить, сколько места реально занимает корзина — sudo du -sh /var/lib/docker/volumes/mailcowdockerized_vmail-vol-1/_data/_garbage/* покажет размер каждого удалённого ящика с меткой времени в имени. Часто оказывается, что причина нехватки места вообще не в корзине, а в чём-то другом (логах, резервных копиях, спам-карантине).
Если корзина действительно основной потребитель места и ждать нельзя, есть два пути. Первый — временно понизить MAILDIR_GC_TIME, применить через docker compose up -d (пересоздастся dovecot-mailcow, получивший новое значение в окружении), дождаться очередного прохода сборщика и вернуть значение обратно. Второй, точечный — удалить руками только конкретный каталог в _garbage, в имени которого точно тот ящик, что вы готовы потерять навсегда: сборщик делает ровно то же самое, просто по таймеру. Я выбираю второй, когда речь об одном-двух ящиках: не надо перезапускать dovecot, и защита для остальных удалённых ящиков сохраняется. Но перед rm -r я дважды читаю путь — ошибка на уровень выше снесёт живую почту.
Если же место продолжает заканчиваться регулярно, а не разово после чистки старых ящиков, разбираться нужно уже не с корзиной mailcow, а с диском сервера в целом — причиной может быть что угодно, от логов до резервных копий других сервисов; общий подход к поиску процесса, который держит место, я описывал в статье про то, как найти процесс, держащий удалённый файл, хотя механика там другая, сам принцип диагностики «сначала понять, кто занимает место, потом действовать» универсален.
Для художественной школы мы в итоге ничего экстренно не меняли — 30 ГБ не были критичны для их диска, и администратор согласилась просто подождать штатные пять дней, а на будущее мы договорились, что удаление ящиков выпускников будет плановой ежегодной процедурой в начале августа, а не разовым решением в последний момент перед учебным годом, когда каждый лишний гигабайт на счету.
Частые вопросы
Почему после удаления ящика в mailcow место на диске не освобождается сразу?
Удаление ящика в mailcow — это перенос его каталога в _garbage на том же томе vmail-vol-1, где он лежит зашифрованным до истечения MAILDIR_GC_TIME (по умолчанию 7200 минут, то есть 5 суток). Сборщик мусора проверяет корзину периодически и стирает только каталоги старше этого срока.
Сколько времени есть на восстановление случайно удалённого ящика?
По умолчанию 5 дней — ровно то время, что данные лежат в каталоге _garbage тома vmail-vol-1, прежде чем сборщик мусора удалит их окончательно.
Как восстановить письма из корзины mailcow?
Нужно заново создать тот же ящик в mailcow, скопировать его папки из _garbage/[метка][домен][пользователь] обратно в обычное расположение ящика, а затем выполнить docker compose exec dovecot-mailcow doveadm force-resync -u user@example.com '*' и doveadm quota recalc -u user@example.com, чтобы Dovecot переиндексировал содержимое.
Можно ли ускорить освобождение места, изменив MAILDIR_GC_TIME?
Да, значение задаётся в минутах и меняется в mailcow.conf с последующим docker compose up -d. Но это сокращает и время на отмену случайного удаления. Предложение снизить дефолт до нуля (issue #5274) закрыто как not planned, по умолчанию по-прежнему 7200 минут.
Можно ли удалить конкретный ящик из _garbage вручную?
Да, сборщик мусора делает то же самое — rm -r каталога старше MAILDIR_GC_TIME. Удаляйте только каталог с нужной меткой времени и именем внутри _garbage и проверьте путь перед командой: после этого восстановить ящик будет нельзя.
Что будет, если запустить docker compose down -v, чтобы освободить место?
Эта команда уничтожает вообще все тома mailcow — базу данных, всю живую почту в vmail-vol-1 и, что особенно критично, ключи шифрования в crypt-vol-1, без которых все письма на сервере становятся нечитаемыми. Для освобождения места, занятого только корзиной удалённых писем, эта команда не подходит и не нужна.
Источники
- mailcow docs: Recover accidentally deleted data — Путь к каталогу корзины /var/lib/docker/volumes/mailcowdockerized_vmail-vol-1/_data/_garbage, требование восстанавливать в тот же mailcow или с теми же ключами crypt-vol-1, точные команды doveadm force-resync и doveadm quota recalc внутри dovecot-mailcow. https://docs.mailcow.email/backup_restore/b_n_r-accidental_deletion/
- mailcow docs: Remove Persistent Data — Полный список томов, уничтожаемых командой docker compose down -v (mysql-vol-1, redis-vol-1, vmail-vol-1, rspamd-vol-1, crypt-vol-1) и явное предупреждение, что удаление crypt-vol-1 делает всю почту нечитаемой. https://docs.mailcow.email/troubleshooting/debug-rm_volumes/
- GitHub mailcow-dockerized generate_config.sh — MAILDIR_GC_TIME=7200 по умолчанию, значение в минутах, каталог /var/vmail/_garbage/timestamp_sanitizedstring; в docker-compose.yml задание ofelia dovecot_maildir_gc запускает maildir_gc.sh каждые 30 минут. https://github.com/mailcow/mailcow-dockerized/blob/master/generate_config.sh
- GitHub mailcow-dockerized issue #5274 — Reduce default MAILDIR_GC_TIME (to zero) — Июнь 2023: предложение снизить дефолт MAILDIR_GC_TIME до нуля (корзина заняла 3 ГБ из 20 ГБ); закрыто как not planned. https://github.com/mailcow/mailcow-dockerized/issues/5274
- mailcow-dockerized: maildir_gc.sh — Скрипт сборщика: find /var/vmail/_garbage/ -mindepth 1 -maxdepth 1 -type d -cmin +${MAILDIR_GC_TIME} -exec rm -r. https://github.com/mailcow/mailcow-dockerized/blob/master/data/Dockerfiles/dovecot/maildir_gc.sh



