Почему после 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. Поэтому ненулевой архив базы — не повод считать набор исправным. Один удачный компонент не делает набор пригодным для полного восстановления.
- 0 байт у `backup_vmail.tar.zst` — переписка не сохранена.
- Наличие каталога `mailcow-ДАТА` не подтверждает успех задания.
- Ненулевой архив MariaDB не компенсирует отсутствие `vmail` и `crypt`.
- Строка `zstd: not found` указывает на отсутствующий исполняемый файл в контейнере.
Почему 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, а не модифицировать контейнер вручную.
- Хост: Docker Engine, каталоги назначения и управляющий скрипт.
- Временный контейнер: `/bin/tar`, `zstd` и подключённые тома mailcow.
- Локальный кеш образов: причина запуска старого содержимого под прежним тегом `latest`.
Как я подтверждаю диагноз за пять минут
Сначала фиксирую версию репозитория и сведения о локальном образе. Затем запускаю одноразовый контейнер из того же тега и спрашиваю, видит ли он 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- Проверить версию репозитория mailcow.
- Проверить наличие `zstd` именно в `ghcr.io/mailcow/backup:latest`.
- Прочитать stderr задания целиком.
- Сопоставить размеры всех компонентов одного набора.
Исправление: обновить 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- Сначала сохранить журнал неудачного запуска и проверить свободное место.
- В плановое окно обновить mailcow до исправленной или более новой стабильной версии.
- При срочном исправлении выполнить явный `docker pull` backup-образа.
- Повторить полный бэкап в новый каталог, не дописывая старый набор.
Хостел «Ночлег на Таганке»: 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 отработал».
- 4 vCPU и 8 ГБ RAM на почтовой VM, `THREADS=2`.
- 41 ГиБ исходного `vmail` на 13 ящиков.
- Пять нулевых архивов при ненулевой копии MariaDB.
- 51 час без новой пригодной точки восстановления.
- Тестовое восстановление двух общих ящиков после исправления.
Другие причины нулевого архива, когда 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 я предпочитаю делать копию на локальный диск и уже потом отправлять её наружу.
- `No space left on device` — нет места в `MAILCOW_BACKUP_LOCATION`.
- `Conflict. The container name "/mailcow-backup" is already in use` — висит прерванный контейнер.
- Пустой список томов по шаблону — не совпадает `COMPOSE_PROJECT_NAME`.
- `Permission denied` при записи в `/backup` — права, `root_squash` или SELinux.
- Сообщение prefetch об использовании кешированного образа — registry недоступен.
Как доказать, что следующий бэкап действительно живой
Проверка -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 и регулярно провожу тестовое восстановление. Для почтового администратора это и есть критерий готовности: не «архив лежит», а «из него удалось поднять выбранный ящик и прочитать письмо».
- Ежедневно: наличие, ненулевой размер, журнал и `zstd -t`.
- Периодически: просмотр содержимого tar и сравнение объёмов.
- По регламенту: восстановление на изолированном стенде.
- Постоянно: отдельная копия вне сервера mailcow.
Частые вопросы
Нужно ли устанавливать 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.
Источники
- mailcow: dockerized documentation — Backup — разделы Manual, Variables for backup/restore script, Multithreading, Backup path и Cronjob; актуальная документация: https://docs.mailcow.email/backup_restore/b_n_r-backup/
- mailcow release notes — Moozember 2025 / 2025-12 и 2025-12a: переход с pigz на zstd, Debian Trixie и предварительная загрузка backup-образа; https://mailcow.email/posts/2025/release-2025-12/
- mailcow GitHub release — Release 2025-12a от 12 декабря 2025 года, пункт backup: add image prefetch function to verify latest image is used; https://github.com/mailcow/mailcow-dockerized/releases/tag/2025-12a
- mailcow GitHub commit — Commit d977ddb — backup: add image prefetch function to verify latest image is used; https://github.com/mailcow/mailcow-dockerized/commit/d977ddb
- GitHub issue #6952 — Error with backup_and_restore.sh helper script — журнал `zstd: not found`, `Cannot write: Broken pipe`, код 127 после обновления до 2025-12; https://github.com/mailcow/mailcow-dockerized/issues/6952
- Исходный код backup_and_restore.sh — Команды docker run для томов и MariaDB, `zstd --rsyncable -T${THREADS}`, проверка THREADS, `prefetch_image`, распознавание .tar.zst/.tar.gz при восстановлении; https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/backup_and_restore.sh
- GitHub issue #7035 — backup_and_restore.sh: should catch tar error code 1 — обработка кодов возврата tar, январь 2026; https://github.com/mailcow/mailcow-dockerized/issues/7035
- mailcow community — Обсуждение #5675 Backup does not work anymore after update to mailcow 2025-12, журнал `zstd: not found` и подтверждение причины разработчиком; https://community.mailcow.email/d/5675-backup-does-not-work-anymore-after-update-to-mailcow-2025-12
