Восстановление 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, или наоборот, — гарантированный сбой на любом следующем шаге, поэтому разработчики решили падать сразу и внятно, а не через десять минут на середине восстановления базы.
Три причины, по которым переменная оказывается пустой
Первая и самая частая сейчас причина — свежая установка. Логика определения типа 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_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, который я даю клиентам
Этот список я довёл до состояния регламента после третьего переезда клиента на новый VPS — до этого каждый раз находили что-то новое уже после того, как почта легла. Сейчас на весь список уходит минут пятнадцать перед запуском restore, и это дешевле, чем объяснять команде, почему рассылка не ушла. Логика та же, что я применяю к резервному копированию баз 1С: бэкап не бэкап, пока не проверен реальным восстановлением, а не только наличием файла в папке.
Отдельно фиксирую время каждого шага в тикете: сколько шёл backup на старом сервере, сколько restore на новом — эти цифры потом пригождаются, когда считаю клиенту допустимое окно простоя для следующего переезда или планового апгрейда VPS. И раз в квартал прогоняю плановое тестовое восстановление бэкапа на отдельном стенде — не дожидаясь аварии, чтобы узнать, что архив на самом деле не разворачивается.
- На старом сервере: backup_and_restore.sh backup all, дождаться завершения без ошибок для каждого компонента
- Скопировать папку с архивом (mailcow_DATE) на новый сервер, не переименовывая её
- На новом сервере: установить mailcow с нуля (git clone, generate_config.sh, docker compose up -d) и дождаться, что стек живой
- Проверить grep DOCKER_COMPOSE_VERSION mailcow.conf — значение должно быть native или standalone, без опечаток
- Сверить docker compose version / docker-compose version с тем, что фактически стоит на новом хосте
- Только после этого запускать helper-scripts/backup_and_restore.sh restore и выбирать all или нужные компоненты
- После восстановления сверить MAILDIR_SUB, TZ и занятость портов 25/80/443/587/993 на новом сервере
Частые вопросы
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) или выбирать по одному, если нужен только конкретный участок.
Источники
- GitHub issue #7065 — Cannot restore because DOCKER_COMPOSE_VERSION is empty — Проверил текст ошибки, окружение (mailcow 2026-01, CentOS Stream 9→10, Docker 29.2.1, Compose v5.0.2), разбор причины в комментариях (generate_config.sh не вызывает get_compose_type) и статус — закрыт ботом как not planned 28.05.2026. https://github.com/mailcow/mailcow-dockerized/issues/7065
- mailcow docs — Restore — Проверил требование «mailcow must be initialized and running» перед restore, синтаксис backup_and_restore.sh restore, переменные MAILCOW_BACKUP_LOCATION и THREADS. https://docs.mailcow.email/backup_restore/b_n_r-restore/
- generate_config.sh (master, GitHub) — Сверил строку DOCKER_COMPOSE_VERSION=${COMPOSE_VERSION} и то, что скрипт вызывает get_installed_tools и get_docker_version, но не get_compose_type. https://github.com/mailcow/mailcow-dockerized/blob/master/generate_config.sh
- helper-scripts/backup_and_restore.sh (master, GitHub) — Сверил функцию restore(): развилку native → 'docker compose', standalone → 'docker-compose', и точный текст сообщения об ошибке. https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/backup_and_restore.sh
- GitHub issue #5004 — DOCKER_COMPOSE_VERSION not correctly set in mailcow.conf — Проверил исторический баг января 2023 года: update.sh присваивал значение COMPOSE_VERSION вместо DOCKER_COMPOSE_VERSION; issue закрыт как исправленный в тот же день. https://github.com/mailcow/mailcow-dockerized/issues/5004
- mailcow Moocember 2025-12 release notes — Проверил: в 2025-12 бэкап перешёл с pigz на zstd, в 2025-12a (12.12.2025) бэкап-система предварительно скачивает образ перед работой. https://mailcow.email/posts/2025/release-2025-12/



