Mailcow: холодный резерв не находит external volume
АйТи Фреш
Linux, Docker и DevOps

Холодный резерв mailcow скопирован, а Compose не находит external volume: что проверить перед запуском

Автор: , директор ООО «АйТи-Фреш» · · ~16 мин чтения
Резервный сервер mailcow с томом данных, который не подключается к контейнеру после копирования холодного резерва
Данные скопированы один в один, а Compose всё равно не узнаёт свои тома.

Если после копирования холодного резерва mailcow команда docker compose up на запасном сервере сначала ругается «volume already exists but was not created by Docker Compose», а после правки docker-compose.yml — «external volume not found», дело не в повреждённом бэкапе. Разбираю, почему так происходит и как запускать резерв, не редактируя compose-файл вручную.

Резерв скопирован, а Compose спотыкается на volumes — в чём симптом

Служба доставки «Курьер Плюс» — 30 рабочих мест, собственная почта на mailcow для диспетчерской, бухгалтерии и уведомлений курьерам о статусе доставки — обратилась ко мне в сентябре с обычным DR-тестом: раз в квартал разворачиваем холодный резерв на втором VPS в другом дата-центре и проверяем, что он реально поднимается, а не просто лежит папкой на диске. Мы делаем такие проверки для клиентов в рамках работ по резервному копированию и защите почтовой инфраструктуры регулярно, потому что разница между «бэкап есть» и «бэкап поднимается за 20 минут без сюрпризов» — это ровно то, что решает исход простоя, а не факт наличия файлов на резервном диске.

У «Курьер Плюс» резервная копия создавалась штатным create_cold_standby.sh из репозитория mailcow-dockerized, синхронизация по rsync на резервный сервер отрабатывала без ошибок каждую ночь. А вот при первой попытке реально поднять резерв — docker compose up -d на втором сервере — Docker Compose выдал серию предупреждений вида «volume … already exists but was not created by Docker Compose. Use external: true to use an existing volume» на добрый десяток томов: vmail, mysql, redis, postfix, clamd и так далее. Администратор клиента, недолго думая, добавил external: true во все секции volumes в docker-compose.yml — и получил уже не предупреждение, а фатальную ошибку: external volume «postfix-vol-1» not found, при этом сам том в docker volume ls был на месте.

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

Откуда берётся предупреждение «not created by Docker Compose»

Первое предупреждение — не признак поломки, а особенность того, как Docker Compose именует и опознаёт тома. По умолчанию Compose привязывает имя каждого тома к имени проекта: реальное имя в Docker получается как <имя_проекта>_<имя_тома_из_yml>, например mailcowdockerized_vmail-vol-1, а не просто vmail-vol-1, как написано в самом docker-compose.yml. Имя проекта Compose берёт из переменной COMPOSE_PROJECT_NAME (в mailcow она задаётся в mailcow.conf) или, если её нет, из имени каталога, где лежит compose-файл. Если вы только начинаете разбираться, как Compose вообще управляет томами и сетями, полезно сначала закрыть базу — у нас есть разбор основных команд Docker для новичков, где эта механика объяснена без привязки к конкретному проекту.

Когда старая версия create_cold_standby.sh готовит резервный сервер, тома на нём появляются до первого запуска Compose и без его служебных меток проекта — в терминах Compose это тома, которые существуют на Docker-хосте, но не были заведены именно этим проектом через docker compose up или docker compose create. Отсюда и формулировка предупреждения: Docker честно говорит, что видит том с нужным именем, но не может подтвердить, что он его создатель, и предлагает явно объявить том внешним (external: true), если это ожидаемо.

Проблема в том, что это предупреждение — а не ошибка. Сами контейнеры при этом обычно всё равно запускаются и примонтируют существующие тома, потому что Compose использует уже существующий том с совпадающим именем, если явно не указано иное. То есть у «Курьер Плюс» до правки docker-compose.yml резерв формально мог бы подняться и с одними предупреждениями в логе — административная паника из-за текста warning, а не из-за реальной невозможности запуска, встречается на практике чаще, чем кажется.

Схема причины предупреждения Docker Compose о томе, не созданном Compose, после копирования резерва mailcow по rsync
Том на месте физически, но Compose не считает себя его владельцем — отсюда и предупреждение.

Почему external: true в лоб — не решение, а новая проблема

Разбор issue #5970 в репозитории mailcow-dockerized описывает почти дословно ситуацию «Курьер Плюс»: автор на mailcow 2024-06c, Docker 27.1.1, Docker Compose 2.27.3 и Ubuntu 22.04 LTS получил серию предупреждений о 14 томах после запуска резервной копии, созданной create_cold_standby.sh, и последовал совету Docker — добавил external: true в секции volumes. Результат — ошибка external volume 'postfix-vol-1' not found, хотя в docker volume ls том был — только под именем mailcowdockerized_postfix-vol-1. Запуститься автору удалось, лишь когда он прописал префикс mailcowdockerized_ во всех ссылках на тома по compose-файлам, — но такая правка живёт ровно до следующего обновления, о чём он сам и написал в issue.

Причина рассинхрона — в том, как Compose трактует поле name у внешнего тома. По спецификации Compose File Reference, если у тома указано external: true без отдельного поля name, Compose ищет том именно с тем именем, что указано как ключ в секции volumes того сервиса (то есть postfix-vol-1 буквально), а не с автоматически подставленным префиксом проекта — в отличие от обычного, не-внешнего тома, где префикс подставляется автоматически. Если реальный том в Docker называется mailcowdockerized_postfix-vol-1 (с префиксом), а в yml просто написано postfix-vol-1: {external: true} — Compose ищет ровно postfix-vol-1 и не находит его, потому что физически он называется иначе. Отдельное поле name: в связке с external: true как раз и существует для того, чтобы явно указать Compose фактическое имя тома на диске, когда оно отличается от ключа в yml — но угадать его без проверки через docker volume ls и docker inspect почти невозможно, а ошибиться легко в обе стороны.

Issue #5970 в итоге закрыли с пометками «not-a-bug» и «support» — вопрос перенаправили в каналы поддержки сообщества, то есть формально признали это не багом самого mailcow, а следствием ручного редактирования compose-файла в обход штатного сценария восстановления. Для практики это значит: ручная правка external: true по томам холодного резерва — рабочий, но хрупкий путь, который легко сломать на следующем обновлении mailcow, потому что update.sh переписывает docker-compose.yml поверх ваших правок.

Сравнение ручной правки docker-compose.yml через external true и штатного сценария docker compose create для холодного резерва mailcow
Штатный путь без ручного external: true короче и не ломается при следующем обновлении.

Что на самом деле должен делать штатный сценарий cold standby

Если вы разворачивали mailcow с нуля по пошаговому регламенту — с дефолтным COMPOSE_PROJECT_NAME и без правок docker-compose.yml — вероятность упереться в эту проблему заметно ниже: имена томов предсказуемы, а обновления идут штатно. Официальная инструкция mailcow по холодному резерву прямо предупреждает: скрипт create_cold_standby.sh рассчитан на установки по умолчанию и «может ломаться при нестандартных переопределениях томов» (volume overrides). Скрипт определяет реальные пути монтирования каждого тома через docker volume ls -qf name=<имя_проекта> и docker inspect (поле Mountpoint), а не полагается на жёстко зашитые пути — это важно понимать, если у вас том вынесен на отдельный диск или в NFS через override, как описано в официальном руководстве по переносу Maildir.

В репозитории есть отдельный исправленный сценарий именно для проблемы «Курьер Плюс»: в декабре 2024 года в create_cold_standby.sh внесли изменение (объединённый пул-реквест с формулировкой «prevent need for external: true»), которое запускает docker compose create на удалённом Docker сразу после копирования каталога mailcow-dockerized — ещё до синхронизации содержимого томов и до первого реального docker compose up (PR #6203 от codiflow, смёржен 10.12.2024). Смысл в том, что docker compose create — часть штатного жизненного цикла Compose, готовящая контейнеры к запуску, — создаёт сети, тома и контейнеры от имени этого Compose-проекта, а rsync потом заливает данные уже в «правильные» тома. Команда идемпотентна: при повторных прогонах скрипта она ничего не меняет. После этого шага Docker уже считает тома «своими», предупреждение не появляется, и никакого external: true в yml прописывать не нужно вообще.

Отсюда практический вывод: если у вас актуальная версия mailcow (собранная после этого исправления) и вы восстанавливаетесь строго по официальной инструкции — через сам скрипт и docker compose up -d сразу после синхронизации, без промежуточных ручных команд — проблема с external volume в принципе не должна возникать. Она проявляется в двух случаях: версия mailcow старше исправления (ветка 2024-06c, как в Issue #5970, вышла раньше декабря 2024), либо восстановление сделано не по сценарию — например, вручную через rsync без последующего docker compose create, или с уже отредактированным до этого docker-compose.yml.

Чек-лист: что проверить перед запуском резерва

Прежде чем разбираться с external volumes и compose-файлом, я всегда прохожу по короткому списку проверок — почти всегда причина в одном из этих пунктов, а не в повреждённых данных:

Во-первых, версия mailcow на источнике и на резервном сервере должна совпадать или быть достаточно свежей, чтобы включать исправление create_cold_standby.sh с docker compose create (по факту — любой релиз после 10 декабря 2024 года; версию видно в веб-интерфейсе mailcow, а дату последнего коммита — командой git -C /opt/mailcow-dockerized log -1 --format=%cd). Во-вторых, стоит явно проверить docker volume ls | grep <имя_проекта> на резервной машине и сравнить список с исходным сервером — если каких-то томов не хватает, значит проблема не в именовании, а в неполной синхронизации rsync, и лечить нужно её, а не compose-файл.

cd /opt/mailcow-dockerized
cat mailcow.conf | grep COMPOSE_PROJECT_NAME
docker volume ls -q --filter name=$(grep COMPOSE_PROJECT_NAME mailcow.conf | cut -d'=' -f2)
docker inspect <имя_тома> --format '{{ .Mountpoint }}'

В-третьих, если восстановление делалось не строго по официальной инструкции (например, резерв поднимали вручную из бэкапа, а не через create_cold_standby.sh + docker compose up -d на чистой синхронизированной копии), выполните docker compose create на резервном сервере из каталога /opt/mailcow-dockerized до первого docker compose up -d — это тот самый шаг, которого не хватает, и он безопасен: он не запускает контейнеры, а только регистрирует сети, тома и сами контейнеры как принадлежащие текущему Compose-проекту. И только если после этого предупреждения остаются — а такое бывает при действительно нестандартных volume overrides, например томах на отдельном примонтированном диске, — стоит переходить к ручному external: true с явным name:, сверенным через docker inspect, а не наугад.

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

Как разобрали у «Курьер Плюс»: откат правок и повторный запуск

У «Курьер Плюс» версия mailcow оказалась из середины 2024 года — обновления откладывали почти год, потому что «и так работает», это классика для инфраструктуры в 30 рабочих мест без выделенного админа. Сначала я вернул docker-compose.yml на резервном сервере к оригинальному состоянию из репозитория (без ручных external: true, которые администратор клиента успел проставить), затем обновил саму установку mailcow на источнике до актуальной сборки через штатный update.sh — это заодно подтянуло исправленный create_cold_standby.sh. Заодно проверил, что update.sh не спотыкается о версию Docker Compose — с этим у клиента раньше уже была похожая история, поэтому сверка версий вошла в чек-лист отдельным пунктом.

После этого пересоздал резерв с нуля: удалил старые тома на резервном сервере командой docker volume rm (предварительно убедившись, что источник — основной сервер — не пострадает, резервная копия одноразовая и создаётся заново), прогнал create_cold_standby.sh повторно с исходного сервера, а на резервном сразу после синхронизации выполнил docker compose create вручную — не потому что скрипт этого не делал сам, а чтобы наглядно показать администратору клиента шаг, из-за пропуска которого возникла путаница в первый раз.

Тот же принцип холодного резерва я разбирал на примере сервера 1С в другом дата-центре: скопированные файлы — это ещё не резерв, резерв — это то, что реально поднялось и приняло нагрузку. docker compose up -d на резерве поднялся без единого предупреждения про volumes. Полная проверка — остановка контейнеров на основном сервере, переключение MX и A-записи на резервный IP, отправка тестового письма через диспетчерскую почту — заняла около 25 минут, что укладывается в целевой RTO, который мы для «Курьер Плюс» и согласовывали изначально. По итогу договорились об автообновлении mailcow не реже раза в квартал именно для того, чтобы подобные исправления в скриптах восстановления не залёживались на старых версиях.

Что делать, если проблема осталась после всех проверок

Если версия свежая, docker compose create выполнен, а предупреждения всё равно есть — почти наверняка у вас действительно нестандартный volume override: том вынесен на отдельный примонтированный диск, в сетевое хранилище или переопределён через docker-compose.override.yml, как это делается, например, при переносе почтового хранилища (vmail) на отдельный раздел по официальному руководству mailcow. В этом случае предупреждение Compose корректно — такой том действительно создан не текущим Compose-проектом, а руками администратора заранее, и external: true — правильный, штатный способ его подключить, при условии что имя указано точно.

В такой ситуации я всегда сверяю фактическое имя тома командой docker volume ls, а не полагаюсь на память или на то, как том называется в исходном docker-compose.yml, и прописываю его через связку external: true плюс отдельное поле name: с этим точным именем — так Compose ищет том по имени из name, а внутренний ключ секции volumes остаётся человекочитаемым и используется только для ссылок внутри самого yml. И важная оговорка: любая ручная правка docker-compose.yml в mailcow должна попадать не в основной файл (его перезаписывает update.sh при каждом обновлении), а в docker-compose.override.yml рядом с ним — Compose автоматически объединяет оба файла, а override переживает обновления, в отличие от правок в самом docker-compose.yml.

Не редактируйте docker-compose.yml в mailcow напрямую — при следующем update.sh файл будет перезаписан из репозитория, и все ручные external: true исчезнут вместе с настройкой. Любые постоянные изменения — только в docker-compose.override.yml.

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

Почему Docker Compose вообще предупреждает про тома после холодного резерва mailcow?

Потому что тома физически появились на резервном сервере через rsync раньше, чем Compose успел зарегистрировать их как созданные именно этим проектом. Это стандартное поведение Compose для любых заранее существующих томов, а не специфика mailcow.

Обязательно ли прописывать external: true в docker-compose.yml mailcow?

Нет, если восстанавливаетесь по официальному сценарию create_cold_standby.sh на актуальной версии: скрипт сам выполняет docker compose create на резервной стороне, и Compose начинает считать тома своими без ручных правок yml.

Что означает ошибка external volume "postfix-vol-1" not found, если том виден в docker volume ls?

Обычно это расхождение имён: Compose при external:true ищет том с именем ровно как в ключе секции volumes, а реальный том в Docker называется с префиксом проекта (например, mailcowdockerized_postfix-vol-1). Решение — указать отдельным полем name: фактическое имя, сверенное через docker volume ls.

Где безопасно вносить правки в docker-compose.yml mailcow, чтобы они не терялись при обновлении?

Только в docker-compose.override.yml рядом с основным файлом. Update.sh перезаписывает docker-compose.yml из репозитория при каждом обновлении, а override.yml Compose подхватывает и объединяет с основным файлом автоматически, не трогая его.

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

docker volume ls -q --filter name=<имя_проекта_из_COMPOSE_PROJECT_NAME> покажет список, а docker inspect <имя_тома> --format '{{ .Mountpoint }}' — фактический путь на диске. Тем же способом пользуется сам create_cold_standby.sh для определения точек монтирования.

Может ли проблема быть в неполной синхронизации rsync, а не в именовании томов?

Да, и это стоит исключить в первую очередь: сравните списки docker volume ls на источнике и на резерве построчно. Если тома вовсе нет на резервной машине — дело не в external volumes, а в том, что синхронизация не завершилась или упала с ошибкой.

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

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

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

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

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

Источники

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