mailcow restore: пустой DOCKER_COMPOSE_VERSION
АйТи Фреш
Linux, Docker и DevOps

Восстановление mailcow на новом VPS встаёт из-за пустого DOCKER_COMPOSE_VERSION: что проверить

Автор: , директор ООО «АйТи-Фреш» · · ~14 мин чтения
Перенос почтового сервера mailcow на новый VPS с обрывом настройки DOCKER_COMPOSE_VERSION
Архив цел, стек поднят, но одна пустая переменная в конфиге останавливает восстановление.

Если restore mailcow на новом VPS обрывается фразой про пустую переменную DOCKER_COMPOSE_VERSION, хотя архив читается — дело не в бэкапе. Скрипту нечем собрать команду docker compose: в mailcow.conf не совпадает одна строка. Разбираю, откуда она берётся и что проверить перед переездом на новый сервер.

Симптом: архив цел, а restore не идёт дальше выбора компонентов

Ко мне обратился хакерспейс «Пайка и код» — 11 рабочих мест, своя почта на mailcow для рассылок по участникам, заказов деталей и переписки с арендодателем. Мы вели их почтовый сервер на аутсорсе полтора года на одном VPS, а в сентябре хостер объявил о выводе тарифа из продажи — надо было переезжать. План был обычный: поднять новый VPS, сделать бэкап backup_and_restore.sh backup all на старом, перенести архив, развернуть mailcow с нуля по инструкции (git clone, generate_config.sh, docker compose up -d) и восстановиться. На бумаге час работы.

На практике restore встал сразу после выбора точки восстановления и компонентов (Crypt, Rspamd, vmail, Redis, Postfix, SQL — выбрали all). Вместо копирования файлов скрипт вывалил красным: «Can not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...» — и упал с кодом выхода 1. Ни одного файла ещё не тронуто, архив цел, но восстановление просто отказывается стартовать.

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

Что вообще делает эта переменная и почему без неё скрипт останавливается

DOCKER_COMPOSE_VERSION — не декоративная запись, а переключатель, от которого зависит буквально следующая команда. Я посмотрел исходник helper-scripts/backup_and_restore.sh в функции restore(): там жёсткая развилка — если значение native, скрипт собирает COMPOSE_COMMAND="docker compose" (плагин, современный синтаксис); если standalone — COMPOSE_COMMAND="docker-compose" (старый отдельный бинарник). Если переменная не равна ни тому, ни другому (в том числе пустая строка или переменная вообще отсутствует в файле) — скрипт печатает ровно то сообщение, которое мы получили, и выходит, не запуская ни одной операции восстановления.

То есть с точки зрения mailcow это не ошибка бэкапа и не повреждение архива — это защита от неправильно собранной команды. Спрашивать docker compose там, где физически стоит только отдельный docker-compose, или наоборот, — гарантированный сбой на любом следующем шаге, поэтому разработчики решили падать сразу и внятно, а не через десять минут на середине восстановления базы.

Схема выбора команды Docker Compose скриптом backup_and_restore.sh по переменной DOCKER_COMPOSE_VERSION
Одна строка в конфиге решает, соберёт ли скрипт правильную команду или остановится с ошибкой.

Три причины, по которым переменная оказывается пустой

Первая и самая частая сейчас причина — свежая установка. Логика определения типа Compose в mailcow живёт в функции get_compose_type() в файле _modules/scripts/core.sh: она сначала пробует docker compose (плагин), потом отдельный docker-compose, и выставляет native или standalone. Но в актуальном generate_config.sh вызываются только get_installed_tools и get_docker_version, а get_compose_type — нет. В итоге шаблон конфига пишет строку DOCKER_COMPOSE_VERSION=${COMPOSE_VERSION} с пустой переменной, и в новом mailcow.conf оказывается DOCKER_COMPOSE_VERSION= без значения. Я сверил это по исходникам в ветке master — картина ровно такая, и именно это разобрали участники обсуждения issue #7065.

Почему это не всплывает у всех подряд: update.sh тоже вызывает get_compose_type(), и при запуске из него функция правит mailcow.conf через sed, подставляя native или standalone. Поэтому на сервере, который хоть раз обновлялся штатным скриптом, строка давно заполнена. А на только что развёрнутой установке, где update.sh ещё ни разу не запускали, она пустая — и restore, который по регламенту идёт сразу после чистой установки, упирается в неё первым. В нашем случае было именно так: новый VPS, свежий generate_config.sh, стек поднят и работает, а строка пустая.

В issue #7065 автор описывает ту же картину при переносе mailcow версии 2026-01 с CentOS Stream 9 на 10, Docker 29.2.1 и Compose 5.0.2: бэкап-образ ghcr.io/mailcow/backup:latest успешно скачан, точка восстановления и набор «0 — all» выбраны, а дальше — та же ошибка. Мейнтейнеры сначала ответили, что переменная бывает только native или standalone; исправляющего коммита в тикете нет, и в мае 2026 года он закрыт ботом как устаревший (not planned). Поэтому надеяться, что это починят в очередном релизе, я бы не стал — проверяю строку руками при каждом переезде.

Есть и старые, исторические варианты той же проблемы. В январе 2023 года в issue #5004 описали баг update.sh: скрипт присваивал значение переменной COMPOSE_VERSION вместо DOCKER_COMPOSE_VERSION, и строка в конфиге оставалась пустой — исправили в тот же день, но конфиги, прошедшие через ту версию, ещё встречаются. А в issue #6187 (конец 2024 года) generate_config.sh падал с синтаксической ошибкой на проверке версии Docker, потому что docker -v | grep возвращал несколько строк. Вывод у меня один: одной проверки «пусто или нет» мало — сверяю значение с тем, что реально установлено на хосте, каждый раз.

Обязательное условие restore, которое часто пропускают

Отдельная и важная деталь из официальной инструкции по restore: скрипт восстановления нельзя запускать в вакууме. Документация прямо требует: «To restore a backup on a new system, mailcow must be initialized and running» — то есть на новом сервере сначала разворачивается пустой mailcow по обычной установке (тот самый generate_config.sh + docker compose up -d), дожидаются, что стек поднялся и работает, и только после этого запускают restore поверх пустой, но живой установки. Восстановление поверх ещё не поднятого стека или перенос одного backup_and_restore.sh без остальной установки — гарантированный источник странных ошибок, включая нашу.

У нас на новом VPS mailcow действительно был поднят и работал — контейнеры стартовали, веб-интерфейс открывался, mailcow.conf был сгенерирован заново, как и положено. То есть формально условие «mailcow инициализирован и запущен» выполнено, а вот строка, которую restore проверяет первой, осталась пустой, потому что генератор конфига её не заполнил. Это два разных требования — «стек живой» и «конфиг полный», — и в документации они не разведены так явно, как хотелось бы. Поэтому между установкой и restore у меня теперь всегда стоит отдельный шаг проверки конфига.

Как чиню за 10 минут: пошагово

Первым делом смотрю, что реально записано в конфиге на новом сервере:

grep DOCKER_COMPOSE_VERSION /opt/mailcow-dockerized/mailcow.conf

Если строки нет вообще или значение пустое (DOCKER_COMPOSE_VERSION=) — смотрю, что фактически установлено на хосте, командой docker compose version (для плагина) и docker-compose version (для отдельного бинарника, если он есть). На современных серверах почти всегда стоит только плагин — тогда прописываю DOCKER_COMPOSE_VERSION=native через редактор или командой ниже (если строки нет совсем, sed её не создаст — тогда просто дописываю её в конец файла). Альтернатива — один раз запустить ./update.sh: он вызывает ту же get_compose_type() и сам проставит значение, но на переезде я предпочитаю точечную правку, а не обновление посреди восстановления:

sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' /opt/mailcow-dockerized/mailcow.conf

Если стоит именно отдельный docker-compose (бинарник, не плагин) — соответственно standalone. После правки перезапускаю restore тем же способом, каким его запускали (./helper-scripts/backup_and_restore.sh restore, при необходимости с MAILCOW_BACKUP_LOCATION и THREADS, если бэкап лежит не в дефолтной папке) — в нашем случае со второй попытки скрипт дошёл до выбора точки восстановления и компонентов, отработал все шесть разделов (Crypt, Rspamd, vmail, Redis, Postfix, SQL) и почта у хакерспейса поднялась на новом VPS в тот же вечер, простой уложился в неполных три часа с момента остановки на ошибке.

Отдельно проверяю сам факт, что установлено на новом хосте, до правки конфига, а не после — иначе легко прописать значение, которое не соответствует реальности:

Если первая команда отвечает версией, а вторая говорит, что бинарника нет — значит, стоит только плагин, и в конфиг идёт native. Если наоборот — standalone. Если отвечают обе (бывает на серверах, где ставили и то, и другое в разное время) — ориентируюсь на логику самого mailcow: get_compose_type() первым проверяет плагин docker compose и при его наличии выбирает native. Так же поступаю и я: ставлю native, а старый отдельный бинарник со временем убираю, чтобы не путал ни людей, ни скрипты.

Если первая команда отвечает версией, а вторая говорит, что бинарника нет — значит, стоит только плагин, и в конфиг идёт native. Если наоборот — standalone. Если отвечают обе (бывает на серверах, где ставили и то, и другое в разное время) — смотрю, какой из них использовался при первом запуске generate_config.sh на этом сервере, обычно это видно по тому, какая команда фигурирует в systemd-юнитах и алиасах администратора.

Чек-лист из шести пунктов для проверки mailcow.conf и Docker Compose перед restore на новом сервере
Пятнадцать минут проверки перед restore экономят часы простоя почты после переезда.

Что ещё сверяю в mailcow.conf при переезде на новый сервер

DOCKER_COMPOSE_VERSION — не единственное поле, которое стоит проверить глазами, а не доверять переносу «как есть». Смотрю на MAILDIR_SUB: документация отдельно предупреждает старые установки — если в прежнем mailcow.conf этого параметра не было, его нельзя задавать и на новом сервере, иначе Dovecot будет искать письма в подкаталоге, которого нет, и не покажет ни одного письма. Подвох в том, что свежий generate_config.sh сам пишет MAILDIR_SUB=Maildir, так что на переезде со старой установки его приходится убирать руками. Это неприятная ошибка, которая маскируется под «письма пропали».

Второе — архитектура процессора. Если переезжаете, например, с обычного x86_64 VPS на сервер с ARM (бывает при экономии на облаке), restore может автоматически пропустить несовместимые бэкапы Rspamd — это штатное поведение, не баг, но по факту означает, что обученные байесовские фильтры и часть статистики придётся набирать заново. Третье — часовой пояс TZ и порты (HTTP_PORT, HTTPS_PORT, SMTP_PORT и соседние): если на новом сервере уже что-то слушает 25 или 443 порт (например, стоит другой сайт), restore пройдёт, а вот сама почта не поднимется — это уже отдельная диагностика через docker compose logs, но проверяю сразу, чтобы не тратить второй вечер.

Ещё одна мелочь, о которую спотыкаются чаще, чем кажется, — DNS и MX-записи. Restore восстанавливает базу, ящики и правила, но не трогает записи у регистратора домена: пока MX и SPF/DKIM/DMARC указывают на старый VPS, письма продолжат идти туда, даже если новый сервер уже полностью готов принимать почту. У хакерспейса мы заранее снизили TTL записей до 300 секунд за сутки до переезда, а переключили MX только после того, как restore прошёл целиком и тестовое письмо доехало через веб-интерфейс — так простой ужался до времени самого restore, а не растянулся на сутки ожидания обновления DNS-кеша у провайдеров.

Сравнение переезда mailcow на новый VPS до и после исправления DOCKER_COMPOSE_VERSION в mailcow.conf
Один параметр в конфиге отделяет трёхчасовой простой от восстановления почты в тот же вечер.

Чек-лист миграции mailcow на новый VPS, который я даю клиентам

Этот список я довёл до состояния регламента после третьего переезда клиента на новый VPS — до этого каждый раз находили что-то новое уже после того, как почта легла. Сейчас на весь список уходит минут пятнадцать перед запуском restore, и это дешевле, чем объяснять команде, почему рассылка не ушла. Логика та же, что я применяю к резервному копированию баз 1С: бэкап не бэкап, пока не проверен реальным восстановлением, а не только наличием файла в папке.

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

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

DOCKER_COMPOSE_VERSION пустая, но мне лень разбираться — можно просто поставить native?

Можно, но только если на сервере действительно установлен Docker Compose как плагин (проверяется командой docker compose version). Если у вас стоит только отдельный бинарник docker-compose, пропишите standalone — иначе restore упадёт на первой же команде compose.

Восстановление можно запускать сразу после git clone, без generate_config.sh и docker compose up?

Нет. Документация mailcow прямо требует, чтобы на новом сервере mailcow был инициализирован и запущен в пустом состоянии, и только потом поверх него выполняется restore. И даже тогда сверьте строку DOCKER_COMPOSE_VERSION: свежий generate_config.sh может оставить её пустой.

Бэкап и restore точно совместимы между разными архитектурами процессора?

Частично. При восстановлении на другой архитектуре (например, с x86_64 на ARM) скрипт может автоматически пропустить несовместимые бэкапы Rspamd — это ожидаемое поведение, но статистику байесовского фильтра придётся собирать заново.

Можно перенести только helper-scripts/backup_and_restore.sh на новый сервер, не разворачивая весь mailcow?

Нет, в документации отдельно предупреждают не копировать этот скрипт в другое место — он рассчитан на запуск внутри полной установки mailcow-dockerized и обращается к соседним файлам и docker compose стеку.

Где почитать официальный список компонентов, которые вообще можно восстановить по отдельности?

В том же руководстве по restore перечислены Crypt, Rspamd, vmail (почтовые ящики), Redis, Postfix и SQL-база — их можно восстанавливать все сразу (all) или выбирать по одному, если нужен только конкретный участок.

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

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

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

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

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

Источники

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