mailcow: удалил ящики, место не освободилось — почему
АйТи Фреш
Linux, Docker и DevOps

Удалил в mailcow ящики на десятки гигабайт — место не освободилось: сколько времени есть на отмену

Автор: , директор ООО «АйТи-Фреш» · · ~14 мин чтения
Удалённые ящики mailcow хранятся зашифрованными в корзине на диске до истечения MAILDIR_GC_TIME, поэтому место не освобождается сразу
Письма не исчезли и не переехали — они лежат в зашифрованной корзине ровно там же, на том же диске.

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

Кейс: художественная школа почистила базу выпускников и испугалась счётчика диска

Художественная школа «Юный художник» — небольшая, 10 рабочих мест, но переписки много: родители присылают сканы работ на конкурсы, фотографии готовых картин по 15-20 МБ за письмо, переписку с музеями и галереями по выставкам. Раз в год перед началом учебного года администратор школы чистит почтовые ящики выпускников, которые больше не нужны — обычно это два-три ящика, но в этот раз накопилось пять за два года, и суммарный объём вложений в них перевалил за 30 ГБ.

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

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

Прежде чем паниковать из-за не освободившегося места — не удаляйте ничего ещё раз «на всякий случай» и не запускайте команды с флагом -v. В большинстве случаев данные никуда не делись, они просто ещё не прошли срок хранения в корзине.
Схема mailcow: удалённый ящик переносится в _garbage и стирается сборщиком мусора через MAILDIR_GC_TIME
Место освобождается не в момент удаления, а спустя настроенный срок хранения — это осознанная страховка, а не задержка или баг.

Почему место не освобождается сразу: 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 в mailcow: 7200 минут по умолчанию равно 5 суткам, проверка раз в час
Параметр задаётся в минутах — самая частая ошибка при попытке вручную сократить срок хранения корзины.

Как восстановить письма из корзины, если поторопились удалить

Если решение удалить ящик оказалось ошибкой и вы укладываетесь в срок 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 (стирает только то, что уже удалено и просрочено) — это две вещи, которые случайно спутать стоит слишком дорого.

Никогда не запускайте docker compose down -v на продакшен-сервере mailcow, чтобы «освободить место» — эта команда не трогает избирательно только корзину, она уничтожает все тома целиком, включая базу данных и ключи шифрования всей почты.
Сравнение безопасного понижения MAILDIR_GC_TIME и опасной команды docker compose down -v в mailcow
Освободить место, занятое корзиной, можно без риска — но не командой, которая уничтожает все тома целиком.

Что делать вместо паники: практический план на случай нехватки места

Если диск сервера действительно заполняется и пять дней ожидания критичны, правильный порядок такой: сначала проверить, сколько места реально занимает корзина — 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, без которых все письма на сервере становятся нечитаемыми. Для освобождения места, занятого только корзиной удалённых писем, эта команда не подходит и не нужна.

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

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

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

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

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

Источники

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