АйТи Фреш
Главная / Статьи / Серверы и хостинг
Серверы и хостинг

Почему после mailcow 2025-12 бэкап создаёт .tar.zst размером 0 байт

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~16 мин чтения
Почему после mailcow 2025-12 бэкап создаёт .tar.zst размером 0 байт
Иллюстрация к статье «Почему после mailcow 2025-12 бэкап создаёт .tar.zst размером 0 байт».

После обновления mailcow каталог резервной копии выглядит убедительно: дата свежая, `mailcow.conf` на месте, имена архивов правильные. Только `backup_vmail.tar.zst` и соседние файлы весят 0 байт. Я, Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш», разбирал такой отказ руками. Ниже покажу, почему установка `zstd` на сервер ничего не меняет, как починить бэкап без бессмысленной остановки почты и чем обязательно проверить результат.

Нулевой .tar.zst — это не резервная копия

Первое и главное: файл с правильным расширением ещё не является архивом. Если backup_vmail.tar.zst занимает 0 байт, переписки в нём нет. Восстанавливать нечего. Не успокаивайте себя тем, что каталог создался по расписанию и рядом лежит mailcow.conf. Скрипт успел открыть выходной файл, но конвейер сжатия оборвался раньше, чем в него попали данные.

В релизе mailcow 2025-12 от 9 декабря 2025 года разработчики заменили pigz на zstd. Вместо прежних .tar.gz скрипт стал формировать .tar.zst командой GNU tar с внешней программой сжатия. Одновременно служебный backup-образ перевели на Debian Trixie. Сам переход разумный: zstd обычно даёт хороший баланс скорости и степени сжатия. Проблема была не в формате и не в производительности, а в рассинхронизации скрипта и локального Docker-образа.

Характерный журнал выглядит однозначно: сначала перечисляются каталоги /vmail, /crypt или /redis, затем появляются /bin/sh: 1: zstd: not found, Cannot write: Broken pipe, Child returned status 127. После этого скрипт может перейти к следующему компоненту, а пустой файл останется на диске. Результат backup_mariadb.tar.zst при этом может отличаться: SQL-ветка запускается не в backup-образе, а в образе MariaDB из docker-compose.yml — там сначала работает mariabackup, и только потом tar с zstd --rsyncable. Поэтому ненулевой архив базы — не повод считать набор исправным. Один удачный компонент не делает набор пригодным для полного восстановления.

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

Почему zstd на хосте не помогает

Скрипт не сжимает почту средствами основной операционной системы. Для каждого компонента он запускает временный контейнер --name mailcow-backup --rm из ghcr.io/mailcow/backup:latest, подключает к нему Docker volume только для чтения (:ro,z) и каталог назначения как /backup, выполняет /bin/tar --use-compress-program="zstd --rsyncable -T${THREADS}", а затем контейнер удаляется. Поэтому отсутствие постоянно работающего контейнера mailcow-backup — нормальная картина. Он существует только во время задания.

Файловая система контейнера изолирована. Установка пакета командой apt install zstd на Debian-хосте добавляет /usr/bin/zstd хосту, но не меняет слои уже скачанного образа. Ровно поэтому администратор видит успешный which zstd на сервере и одновременно получает zstd: not found из /bin/sh контейнера. Это не ошибка PATH хоста, не права на каталог и не проблема расширения .zst.

Причина конкретного дефекта 2025-12 — закешированный локально ghcr.io/mailcow/backup:latest. У тега latest уже появилась новая сборка с zstd, но ранняя версия скрипта не подтягивала её принудительно. Обычный docker run находил локальный тег и запускал старое содержимое. В 2025-12a от 12 декабря разработчики добавили предварительную проверку и загрузку актуального backup-образа. На актуальных ветках 2026 года эта логика присутствует, поэтому правильное долгосрочное решение — обновить mailcow, а не модифицировать контейнер вручную.

Пакет, установленный внутри контейнера, запущенного вручную без создания нового образа, тоже исчезнет вместе с этим контейнером. Такой эксперимент годится для диагностики, но не для исправления.
Почему после mailcow 2025-12 бэкап создаёт .tar.zst размером 0 байт — схема
Схема к статье. Открыть схему в полном размере

Как я подтверждаю диагноз за пять минут

Сначала фиксирую версию репозитория и сведения о локальном образе. Затем запускаю одноразовый контейнер из того же тега и спрашиваю, видит ли он zstd. Почтовые контейнеры для этой проверки останавливать не нужно.

cd /opt/mailcow-dockerized
git describe --tags --always
docker image inspect ghcr.io/mailcow/backup:latest \
  --format '{{.Id}} {{json .RepoDigests}}'
docker run --rm --entrypoint /bin/sh \
  ghcr.io/mailcow/backup:latest \
  -c 'command -v zstd && zstd --version'

Если последняя команда не выводит путь к zstd и завершается ошибкой, диагноз подтверждён: проблема внутри используемого образа. Если zstd уже есть, не надо продолжать лечить декабрьский дефект по памяти. Ищите в полном журнале No space left on device, ошибки монтирования, недоступность registry, права на каталог, сбой MariaDB или оборванное сетевое хранилище. Одинаковое расширение файла не означает одинаковую причину отказа.

После этого смотрю последний набор целиком, а не только самый большой файл. Команда ниже находит свежий каталог и выводит размеры всех архивов. Путь указан для типового размещения; у вас он должен совпадать с MAILCOW_BACKUP_LOCATION из cron.

backup_root=/srv/mailcow-backup
latest_dir=$(find "$backup_root" -mindepth 1 -maxdepth 1 -type d \
  -name 'mailcow-*' -printf '%T@ %p\n' | sort -nr | head -n1 | cut -d' ' -f2-)
printf 'Последний набор: %s\n' "$latest_dir"
find "$latest_dir" -maxdepth 1 -type f -name '*.tar.zst' \
  -printf '%f %s bytes\n' | sort
find "$latest_dir" -maxdepth 1 -type f -name '*.tar.zst' \
  -size 0 -print
Не ограничивайтесь кодом возврата cron. Скрипт не проверяет свободное место перед запуском, а обработка кодов tar обсуждается до сих пор: в issue #7035 (январь 2026) пользователь указывал, что безобидный код 1 («file changed as we read it») считается сбоем, и тикет закрыт без изменений. Я проверяю и журнал, и результат на диске.
Порядок действий: Как я подтверждаю диагноз за пять минут — схема
Порядок действий: Как я подтверждаю диагноз за пять минут. Открыть схему в полном размере

Исправление: обновить mailcow и принудительно получить образ

Мой основной вариант — перейти как минимум на 2025-12a, а в 2026 году — на поддерживаемый стабильный релиз после изучения промежуточных release notes. Так исправляется не только текущий кеш, но и сама причина его повторного появления: скрипт начинает проверять backup-образ перед работой. Обновление всей установки выполняю в согласованное окно, потому что update.sh может пересоздавать рабочие контейнеры.

cd /opt/mailcow-dockerized
git status --short
./update.sh

Если полноценное обновление сейчас запрещено регламентом, для конкретного сбоя 2025-12 достаточно явно скачать актуальный образ. Работающие SMTP, IMAP и веб-интерфейс ради этого не останавливаю: backup-контейнер временный и обычно не используется между заданиями. После загрузки сразу проверяю программу внутри него.

docker pull ghcr.io/mailcow/backup:latest
docker run --rm --entrypoint /bin/sh \
  ghcr.io/mailcow/backup:latest \
  -c 'command -v zstd && zstd --version'

Затем запускаю новый полный бэкап из штатного расположения скрипта. Документация отдельно предупреждает не копировать backup_and_restore.sh в другой каталог: иначе администратор легко продолжает запускать старую копию после обновления репозитория. Число потоков выбираю по правилу из документации — «ядра минус два», чтобы почте хватило процессора. На виртуальной машине с четырьмя vCPU это THREADS=2; скрипт принимает значения от 1 до 99, по умолчанию 1.

cd /opt/mailcow-dockerized
MAILCOW_BACKUP_LOCATION=/srv/mailcow-backup THREADS=2 \
  ./helper-scripts/backup_and_restore.sh backup all
Не запускайте ради одного образа `docker system prune -a --volumes`. Это несоразмерно и потенциально опасно: ключ `--volumes` расширяет область очистки до неиспользуемых томов. Здесь достаточно адресного `docker pull`; удалять весь Docker-кеш и останавливать mailcow не требуется.

Хостел «Ночлег на Таганке»: 8 рабочих мест и две пустые ночи

Покажу на условном, но технически конкретном примере. Семейный хостел «Ночлег на Таганке» — 8 рабочих мест: администраторы ресепшена в две смены, бухгалтер, управляющая и владельцы. Ящиков 13: восемь личных и пять общих — booking@, info@, reception@, buh@ и служебный для уведомлений от систем бронирования. mailcow работает в виртуальной машине Debian 13: 4 vCPU, 8 ГБ RAM, SSD 120 ГБ под систему и Docker volumes. Объём vmail перед обновлением — 41 ГиБ, львиная доля в booking@ с подтверждениями, сканами паспортов и счетами. Копии складываются в /srv/mailcow-backup на отдельном виртуальном диске 500 ГБ, cron стартует в 03:10 с THREADS=2 и --delete-days 7.

После перехода на 2025-12 cron продолжал завершаться без писем об ошибках. Два ночных каталога появились вовремя, в каждом лежали mailcow.conf и backup_mariadb.tar.zst размером около 180 МиБ, а backup_vmail.tar.zst, backup_crypt.tar.zst, backup_redis.tar.zst, backup_rspamd.tar.zst и backup_postfix.tar.zst весили 0 байт. Предыдущий исправный backup_vmail.tar.gz занимал 33 ГиБ. В журнале нашлись zstd: not found, Broken pipe и код дочернего процесса 127. На хосте zstd был установлен — это сначала и увело в сторону.

Проверка одноразового контейнера показала пустой вывод command -v zstd: локальный тег ghcr.io/mailcow/backup:latest указывал на старую сборку. Я не останавливал почту посреди заезда гостей и не чистил Docker целиком: загрузил образ, убедился в наличии zstd и сделал внеплановый полный бэкап днём, в тихие часы после выселения. В ближайшую ночь установку обновили до 2025-12a, чтобы следующие запуски сами сверяли дайджест образа.

Новый набор сформировался за 26 минут. backup_vmail.tar.zst занял 30 ГиБ, MariaDB — 182 МиБ, остальные компоненты — от нескольких килобайт до 350 МиБ. Все .zst прошли zstd -t; на изолированной тестовой VM восстановили конфигурацию и ящики booking@ и buh@, открыли их по IMAP и нашли письма прошлого сезона. Данные не потеряны, но окно без пригодной новой копии составило 51 час. Для хостела, где вся переписка с гостями и платформами бронирования живёт в почте, именно эту цифру мы занесли в отчёт, а не «cron отработал».

Название хостела условное, а конфигурация и порядок диагностики приведены как практический стенд. Размеры нельзя переносить на другой сервер как норматив: результат zstd зависит от вложений, уже сжатых сканов и PDF и состава почты.
Цифры и версии: Хостел «Ночлег на Таганке»: 8 рабочих мест и две пустые ночи — схема
Цифры и версии: Хостел «Ночлег на Таганке»: 8 рабочих мест и две пустые ночи. Открыть схему в полном размере

Другие причины нулевого архива, когда zstd в образе есть

Декабрьский дефект — самый заметный, но не единственный путь к пустому .tar.zst. Сначала отвечу на частый вопрос: переменной вроде MAILCOW_BACKUP_COMPRESSION в штатном скрипте нет. Документация описывает только MAILCOW_BACKUP_LOCATION и THREADS, а метод сжатия зашит в код: zstd --rsyncable с -T${THREADS} для томов. Переключить скрипт обратно на gzip переменной нельзя; pigz остался только в ветке восстановления старых .tar.gz. Если кто-то правил скрипт руками ради «своего» сжатия, при следующем update.sh эта правка конфликтует с репозиторием и будет отложена в git stash или потеряна — и бэкап молча вернётся к штатному поведению.

Первая по частоте причина после обновления — место. Скрипт не проверяет свободный объём перед стартом, а tar пишет поток прямо в каталог назначения. Когда диск заканчивается, в журнале появляется No space left on device, а файл остаётся нулевым или обрезанным. Особенно легко попасть в это, если MAILCOW_BACKUP_LOCATION в cron указывает на точку монтирования, которая в момент запуска не смонтирована: копия пишется на системный диск и забивает его. Проверяю так:

df -h /srv/mailcow-backup /var/lib/docker
docker system df
du -sh /srv/mailcow-backup/mailcow-* | sort -h | tail -n 5

Вторая группа — Docker. Контейнер создаётся с фиксированным именем mailcow-backup: если предыдущий запуск был прерван и контейнер завис, следующий docker run завершится ошибкой конфликта имени. Том ищется по шаблону ^${COMPOSE_PROJECT_NAME}_vmail-vol-1$: после переименования каталога или смены COMPOSE_PROJECT_NAME в mailcow.conf подстановка вернёт пустую строку, и архив получится пустым или без данных. Наконец, если registry недоступен, prefetch_image откатывается на локальный образ — а он может быть тем самым старым.

docker ps -a --filter name=mailcow-backup
grep COMPOSE_PROJECT_NAME /opt/mailcow-dockerized/mailcow.conf
docker volume ls --format '{{.Name}}' | grep -E 'vmail|crypt|redis|rspamd|postfix|mysql'

Третья группа — права и SELinux. Тома подключаются с суффиксом :z, то есть Docker перемаркирует каталог назначения общей меткой SELinux. На Debian без SELinux это ни на что не влияет, но на RHEL-подобных хостах или при каталоге на NFS/SMB, где метки не поддерживаются, запись в /backup может быть запрещена: tar сообщит Permission denied, файл останется нулевым. То же бывает, когда сетевое хранилище смонтировано с root_squash или только на чтение. Для NFS я предпочитаю делать копию на локальный диск и уже потом отправлять её наружу.

Удалять зависший `mailcow-backup` можно только после того, как вы убедились, что это не идущее прямо сейчас задание: `docker ps` покажет время запуска. Прерванный посреди записи бэкап даст ещё один битый набор.

Как доказать, что следующий бэкап действительно живой

Проверка -s отсечёт нулевые файлы, но её недостаточно. Архив может быть ненулевым и при этом оборванным из-за заполнения диска. Я проверяю контрольную целостность каждого потока внутри актуального backup-образа — так zstd на хост устанавливать по-прежнему не требуется.

backup_root=/srv/mailcow-backup
latest_dir=$(find "$backup_root" -mindepth 1 -maxdepth 1 -type d \
  -name 'mailcow-*' -printf '%T@ %p\n' | sort -nr | head -n1 | cut -d' ' -f2-)
docker run --rm -v "$latest_dir:/backup:ro" \
  --entrypoint /bin/sh ghcr.io/mailcow/backup:latest -c '
set -eu
for archive in /backup/*.tar.zst; do
  echo "checking $archive"
  zstd -t "$archive"
done
'

Следом просматриваю структуру хотя бы критичных архивов. Для vmail должны быть видны каталоги почтового хранилища, а не только успешный тест сжатого потока. Это чтение, оно не меняет рабочие volumes.

docker run --rm -v "$latest_dir:/backup:ro" \
  --entrypoint /bin/tar ghcr.io/mailcow/backup:latest \
  --use-compress-program='zstd -d' \
  -tf /backup/backup_vmail.tar.zst | sed -n '1,30p'

В ежедневной автоматизации я контролирую четыре вещи: появился полный ожидаемый комплект файлов, ни один обязательный архив не равен нулю, zstd -t завершился успешно, а объём vmail не выпал из разумного коридора относительно предыдущих дней. Порог по размеру — лишь индикатор: удаление старых ящиков или миграция данных могут законно уменьшить архив. А вот нулевой vmail при десятках активных пользователей законным быть не может.

И последнее. Локальная копия на том же сервере защищает от ошибки обновления, но не от отказа хранилища, шифровальщика или удаления инфраструктуры. Я оставляю минимум одну копию вне узла mailcow и регулярно провожу тестовое восстановление. Для почтового администратора это и есть критерий готовности: не «архив лежит», а «из него удалось поднять выбранный ящик и прочитать письмо».

Удаляйте два пустых набора только после получения и проверки новой полной копии. До этого сохраните последний исправный `.tar.gz`: актуальный скрипт восстановления распознаёт как `.tar.zst`, так и прежние `.tar.gz`.
Памятка: Как доказать, что следующий бэкап действительно живой — схема
Памятка: Как доказать, что следующий бэкап действительно живой. Открыть схему в полном размере

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

Нужно ли устанавливать zstd на хост mailcow?

Для штатного `backup_and_restore.sh` — нет. Сжатие выполняется внутри `ghcr.io/mailcow/backup:latest`. Проверять и обновлять нужно этот образ.

Нужно ли останавливать mailcow перед `docker pull` backup-образа?

Нет. Backup-контейнер временный, поэтому адресная загрузка его образа не требует остановки SMTP, IMAP и остальных рабочих сервисов. Полное обновление mailcow лучше проводить в окно обслуживания.

Можно ли восстановить файл .tar.zst размером 0 байт?

Нет. Это пустой файл без потока zstd и данных tar. Используйте предыдущую исправную копию и немедленно создайте новый полный набор после исправления.

Почему MariaDB сохранилась, а vmail оказался пустым?

Компоненты проходят разными ветками скрипта. SQL-копирование выполняется `mariabackup` в образе MariaDB из `docker-compose.yml`, тогда как vmail, crypt, Redis, Rspamd и Postfix архивируются во временном контейнере `ghcr.io/mailcow/backup`. Поэтому результаты могут различаться.

Достаточно ли проверить, что архив ненулевой?

Нет. Выполните `zstd -t`, просмотрите оглавление tar и периодически делайте тестовое восстановление на изолированной системе. Ненулевой, но оборванный архив также непригоден.

Можно ли переключить сжатие обратно на gzip переменной окружения?

Нет. Штатный скрипт знает только `MAILCOW_BACKUP_LOCATION` и `THREADS`; сжатие zstd зашито в код. Прежний pigz используется лишь при восстановлении старых `.tar.gz`.

Сколько потоков ставить в THREADS?

Документация советует число ядер минус два. Для небольшой VM на 4 vCPU это 2; значения проверяются скриптом в диапазоне 1–99, по умолчанию 1.

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

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

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

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

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

Источники

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