backup_and_restore.sh mailcow падает с empty section between colons, хотя Docker-тома на месте
Бэкап mailcow падает каждую ночь с ошибкой Docker «empty section between colons», хотя docker volume ls честно показывает нужные тома на месте. Причина не в дисках и не в правах — backup_and_restore.sh ищет том по имени, которое собирает из COMPOSE_PROJECT_NAME в mailcow.conf, и это имя разошлось с тем, что реально закреплено за контейнерами. Разбираю, как это находить и чинить, не трогая данные.
Кейс: бэкап «Экспресс-Курьера» падает с одной и той же ошибкой каждую ночь
Служба доставки «Экспресс-Курьер», 33 рабочих места, держит почту на собственном mailcow — я разворачивал и настраивал корпоративную почту на mailcow два года назад, включая ночной бэкап через cron. Сервер переносили на новую площадку полгода назад: снимали образ, поднимали на новом VPS, часть конфигурации подтягивали заново через generate_config.sh. Почта после переноса ходила без нареканий — полгода без единой жалобы. А вот бэкап, который раньше отрабатывал за 12 минут, в какой-то момент стал падать в логе cron одинаковой ошибкой.
Ошибка в логе выглядела так: docker: invalid spec: :/vmail:ro,z: empty section between colons. Первая мысль — Docker не видит том, том потерян, данные под угрозой. Я проверил docker volume ls — все двенадцать томов mailcow на месте, включая vmail-vol-1 с реальными данными внутри (смотрел через docker run --rm -v <том>:/data alpine du -sh /data). Почта при этом продолжала прекрасно работать: входящие приходили, IMAP отвечал, SOGo открывался. Несостыковка — сервис жив, а бэкап не видит те же самые тома — и есть весь фокус этой поломки.
Цена такой поломки не в моменте, а в отложенном риске. Пока бэкап падает молча в cron, а не присылает алерт, о проблеме узнают обычно одним способом — когда бэкап реально нужен: после сбоя диска, ошибки администратора или атаки шифровальщика. У «Экспресс-Курьера» я нашёл это не по инциденту, а по плановой сверке логов, которую делаю раз в месяц по каждому клиенту на обслуживании — к тому моменту бэкап не делался уже три недели, и без сверки поломка вполне могла прожить ещё полгода незамеченной.
Что означает empty section between colons и откуда берётся пустая часть тома
Сообщение empty section between colons — это диагностика самого Docker-движка, а не mailcow. Docker получает спецификацию монтирования вида <источник>:<точка_монтирования>:<опции>, например vmail-vol-1:/var/vmail:ro,z. Если часть до первого двоеточия пустая — :/var/vmail:ro,z — движок не может понять, что монтировать, и честно называет секцию между двоеточиями пустой. Источник ошибки — не диск и не Docker, а скрипт, который *собрал* эту пустую строку вместо имени тома.
Разбор helper-scripts/backup_and_restore.sh в исходниках mailcow показывает, откуда берётся имя: скрипт ищет том командой docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$. Если под таким именем тома не существует, команда возвращает пустую строку, эта пустая строка подставляется в docker run -v ${VOLUME}:/vmail:ro,z ..., и получаем ровно ту ошибку, что упала в лог «Экспресс-Курьера». Тома при этом реально есть — просто не под тем именем, которое вычислил скрипт. Для MySQL скрипт точно так же ищет ещё и сеть — docker network ls -qf name=^${CMPS_PRJ}_mailcow-network$, — так что при расхождении имени проекта падает не один компонент, а весь backup all: vmail, crypt, redis, rspamd, postfix и база.
Как compose и backup-скрипт вычисляют имя тома через COMPOSE_PROJECT_NAME
Переменная CMPS_PRJ в скрипте — это очищенное значение COMPOSE_PROJECT_NAME из mailcow.conf: CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]"). Файл .env, который использует Docker Compose для той же цели, в инсталляции mailcow — не отдельный файл, а симлинк на mailcow.conf; это прямо проверяется в generate_config.sh, который отказывается работать, если в каталоге нет .env -> mailcow.conf. То есть backup-скрипт и сам Compose читают ровно одну и ту же переменную из одного и того же файла — расхождения тут в теории быть не должно.
В generate_config.sh значение по умолчанию прямо закомментировано как «Fixed project name»: COMPOSE_PROJECT_NAME=mailcowdockerized. А реальные тома в docker-compose.yml объявлены без явного name: — просто vmail-vol-1, mysql-vol-1, redis-vol-1 и так далее, — и Docker Compose сам добавляет к ним префикс проекта при создании. Так что для свежей установки том должен называться mailcowdockerized_vmail-vol-1. Если стек разворачивался по пошаговому регламенту установки mailcow, это значение выставляется один раз и в норме не трогается. Расхождение появляется не в момент установки, а позже: когда COMPOSE_PROJECT_NAME в mailcow.conf меняется после того, как контейнеры и тома уже созданы под старым именем.
Здесь же стоит понимать общее поведение Docker Compose, а не только специфику mailcow: имя проекта — это не просто текстовая метка. Compose использует его как префикс для сети, томов и меток com.docker.compose.project на каждом контейнере в момент создания ресурса. Раз созданные, контейнер или том этот префикс не меняют, даже если переменная окружения в .env позже поменяется — расхождение проявится только там, где имя проекта вычисляется заново на лету, как в backup-скрипте.
Почему имя проекта в mailcow.conf разъехалось с реальными томами
У «Экспресс-Курьера» так и вышло. При переносе на новую площадку сервер переехал образом целиком — вместе с Docker-томами, контейнерами и старым mailcow.conf, — и стек поднялся под тем же именем проекта, что и на старой машине. Через пару месяцев mailcow.conf решили «причесать»: сгенерировали свежий файл через generate_config.sh на чистой копии репозитория и перенесли в рабочий конфиг новые переменные, вручную сверяя домены и пароли БД. Строка COMPOSE_PROJECT_NAME при этом приехала из свежего шаблона — mailcowdockerized, — а не осталась от исходной инсталляции. docker compose up -d после правки никто не запускал, обновлений с тех пор не ставили, поэтому работающий стек ничего не заметил. А первый же ночной бэкап после правки упал.
Важно понимать асимметрию: уже запущенные контейнеры не перечитывают mailcow.conf при каждом обращении. Docker Compose привязывает метки com.docker.compose.project к контейнеру и тому один раз, в момент создания. Если после этого поменять COMPOSE_PROJECT_NAME в файле и не выполнить docker compose up -d заново, работающий стек продолжит крутиться как ни в чём не бывало — на старом имени проекта. А backup-скрипт, который при каждом запуске делает source mailcow.conf и вычисляет CMPS_PRJ заново при каждом запуске, тут же начинает искать несуществующий том. Разошлись не тома — разошлось то, что реально создано, и то, что написано в текущем конфиге.
У «Экспресс-Курьера» реальный префикс оказался просто mailcow — судя по всему, остаток от старой инсталляции, которую переносили ещё несколько лет назад, когда COMPOSE_PROJECT_NAME мог задаваться иначе, чем в текущем шаблоне generate_config.sh. Похожая путаница с именами и версиями всплывает и при обновлении mailcow через update.sh — там Compose тоже чувствителен к тому, что реально создано, а не к тому, что написано в свежем конфиге. Разбираться, откуда взялось конкретно это значение, было не обязательно — важно было зафиксировать, что реально работает, и подогнать конфиг под факт, а не под ожидание.
Как найти реальный префикс томов и не потерять данные при «исправлении»
Первым делом я смотрю не на mailcow.conf, а на то, с чем реально работают контейнеры. Проще всего — спросить у самого работающего dovecot-контейнера, какие тома у него смонтированы:
docker inspect $(docker ps -qf name=dovecot-mailcow) --format '{{range .Mounts}}{{.Name}} -> {{.Destination}}{{println}}{{end}}'Обратите внимание: здесь я намеренно беру docker ps, а не docker compose ps. Compose ищет контейнеры по имени проекта из того же mailcow.conf, и при расхождении честно покажет пустой список — это, кстати, второй быстрый признак проблемы, а docker compose ls в этот момент покажет реально запущенный проект под его настоящим именем. Команда выше покажет реальные имена томов, закреплённые за работающим сервисом — например mailcow_vmail-vol-1 -> /var/vmail вместо ожидаемого mailcowdockerized_vmail-vol-1. Дальше сверяю это с текущим значением COMPOSE_PROJECT_NAME в mailcow.conf (grep COMPOSE_PROJECT_NAME mailcow.conf) — и вижу расхождение чёрным по белому. Список самих томов для полноты картины смотрю через docker volume ls — двенадцать штук, как и должно быть в актуальном docker-compose.yml: vmail-vol-1, vmail-index-vol-1, mysql-vol-1, mysql-socket-vol-1, redis-vol-1, rspamd-vol-1, postfix-vol-1, postfix-tlspol-vol-1, crypt-vol-1, sogo-web-vol-1, sogo-userdata-backup-vol-1, clamd-db-vol-1.
Здесь легко сделать вторую ошибку — попытаться «починить» это через docker compose up -d, надеясь, что Compose сам пересоздаст недостающие тома под правильным именем. Формально он это сделает: создаст новые пустые тома с префиксом из текущего mailcow.conf. Только это будут пустые тома, а не те, где лежит почта — реальные данные так и останутся в томах со старым префиксом, никуда не денутся, но перестанут быть видны стеку, который теперь смотрит на новые имена. Внешне всё «заработает» — контейнеры поднимутся, — а через какое-то время окажется, что почта за последние месяцы пропала. Я эту дорожку не рекомендую даже как временную меру.
Чиню mailcow.conf, а не тома: пошагово
Правильный порядок — подогнать COMPOSE_PROJECT_NAME под то, что реально использует работающий стек, а не наоборот. Для «Экспресс-Курьера» это выглядело так: беру значение префикса, которое docker inspect показал для реального тома (mailcow), и прописываю его в mailcow.conf вместо того, что туда попало при последней пересборке конфига.
# смотрим текущее значение
grep COMPOSE_PROJECT_NAME mailcow.conf
# правим под реальный префикс томов (пример)
sed -i 's/^COMPOSE_PROJECT_NAME=.*/COMPOSE_PROJECT_NAME=mailcow/' mailcow.conf
# тот же поиск, что делает скрипт, должен вернуть имя тома
docker volume ls -qf name=^mailcow_vmail-vol-1$
# compose снова видит работающие контейнеры
docker compose psПосле правки поиск docker volume ls с тем же фильтром, что использует скрипт, должен вернуть имя тома, а docker compose ps — показать работающие контейнеры: значит, Compose и backup-скрипт снова смотрят на один и тот же проект. Если список пустой, имя всё ещё не совпадает с реальным. Дальше — не трогая контейнеры (docker compose up -d в этот момент делать не обязательно, если сервисы и так работают на старом имени и ничего не сломано) — пробую сам бэкап заново. Если правка верна, backup_and_restore.sh находит те же тома, что видит работающий dovecot, и ошибка empty section between colons уходит.
Отдельно я не тороплюсь запускать docker compose up -d сразу после правки COMPOSE_PROJECT_NAME, если стек и так работал: если где-то в конфиге остался ещё один рассинхрон (например, сеть создана под другим именем проекта), пересоздание может зацепить не только тома. Сначала снимаю полный бэкап уже исправленным скриптом, потом уже спокойно разбираюсь с остальными несостыковками по одной.
Как я настраиваю бэкапы mailcow с нуля, чтобы это не повторилось
После этого случая я стал явно фиксировать COMPOSE_PROJECT_NAME в собственном чек-листе внедрения и никогда не даю generate_config.sh перегенерировать его на существующей инсталляции без предварительного сравнения с текущим mailcow.conf. Это одна строка, а цена ошибки — сломанный бэкап, который тихо не работает месяцами, пока не понадобится восстановление.
Сам бэкап у клиентов на mailcow я настраиваю через переменные окружения, а не интерактивный ввод, чтобы cron отрабатывал без вмешательства человека:
MAILCOW_BACKUP_LOCATION=/opt/backup THREADS=4 /opt/mailcow-dockerized/helper-scripts/backup_and_restore.sh backup all --delete-days 14MAILCOW_BACKUP_LOCATION задаёт каталог назначения без интерактивного запроса, THREADS включает параллельную обработку (я обычно ставлю число ядер минус два — так рекомендует сама документация mailcow), а --delete-days 14 чистит бэкапы старше двух недель, чтобы диск не переполнился. Каталог с результатом я всегда копирую за пределы сервера — это отдельная гигиена от вопроса, чем полезен архив vmail сам по себе, — mailcow сам этого не делает, а локальный бэкап на том же хосте бесполезен, если ляжет весь сервер. Ну и главное — раз в месяц я реально прогоняю тестовое восстановление на отдельной машине: бэкап, который никто не пробовал развернуть, с тем же успехом может не существовать вовсе.
Тестовое восстановление я делаю не в проде: поднимаю чистый mailcow на отдельной VM, кладу туда бэкап и прохожу восстановление командой backup_and_restore.sh restore, после чего логинюсь в SOGo и проверяю, что письма и календари на месте. Это отдельная задача от самого бэкапа, но именно она подтверждает, что цепочка mailcow.conf → CMPS_PRJ → реальный том не разъехалась снова — а вместе с алертом на ненулевой код возврата cron-задачи (документация mailcow прямо показывает, как слать письмо только при ошибке) поломку у «Экспресс-Курьера» увидели бы на следующее утро, а не через три недели на плановой сверке логов.
Частые вопросы
Что означает ошибка Docker «empty section between colons»?
Docker получил спецификацию монтирования, в которой часть до двоеточия пустая — обычно это значит, что имя тома, подставленное скриптом, оказалось пустой строкой, потому что том с таким именем не найден.
Данные в mailcow потеряны, если бэкап падает с этой ошибкой?
Нет. Ошибка возникает на этапе поиска тома для бэкапа, а не при работе самих сервисов — почта, что смонтирована у dovecot и postfix, продолжает быть на месте. Страдает только сам процесс резервного копирования.
Почему docker volume ls показывает тома, а backup_and_restore.sh их не находит?
Скрипт ищет том по regex-шаблону, который строит из COMPOSE_PROJECT_NAME в mailcow.conf. Если это значение не совпадает с префиксом, под которым тома реально были созданы Docker Compose, поиск возвращает пустую строку, хотя сами тома существуют под другим именем.
Можно ли просто пересоздать тома с правильным именем через docker compose up -d?
Не как первый шаг. Compose создаст новые пустые тома под текущим именем проекта, а реальные данные останутся в старых томах, к которым стек больше не будет обращаться. Сначала нужно найти настоящий префикс через docker inspect работающего контейнера и подогнать под него COMPOSE_PROJECT_NAME, а не наоборот.
Как узнать реальное имя тома, которое использует работающий mailcow?
Через docker inspect контейнера, который этот том монтирует, например dovecot-mailcow: docker inspect $(docker ps -qf name=dovecot-mailcow) --format '{{range .Mounts}}{{.Name}} -> {{.Destination}}{{println}}{{end}}' покажет фактическое имя тома. Берите именно docker ps: docker compose ps при разошедшемся имени проекта вернёт пустой список.
Источники
- GitHub Issue #5795, mailcow-dockerized — Проверено: точный текст ошибки «docker: invalid spec: :/vmail:ro,z: empty section between colons» и расхождение COMPOSE_PROJECT_NAME=mailcowdockerized с реальным префиксом томов mailcow_. https://github.com/mailcow/mailcow-dockerized/issues/5795
- backup_and_restore.sh, исходник mailcow-dockerized — Проверено: строка вычисления CMPS_PRJ через tr -cd и запрос docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$, аналогичный поиск сети ^${CMPS_PRJ}_mailcow-network$ для MySQL. https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/backup_and_restore.sh
- generate_config.sh, исходник mailcow-dockerized — Проверено: значение по умолчанию COMPOSE_PROJECT_NAME=mailcowdockerized («Fixed project name») и требование симлинка .env -> mailcow.conf. https://github.com/mailcow/mailcow-dockerized/blob/master/generate_config.sh
- docker-compose.yml, исходник mailcow-dockerized — Проверено: полный список из 12 томов сервисов (vmail-vol-1, mysql-vol-1, redis-vol-1 и другие) без явного name:, префикс добавляет Compose. https://github.com/mailcow/mailcow-dockerized/blob/master/docker-compose.yml
- Backup — mailcow: dockerized documentation — Проверено: синтаксис backup_and_restore.sh backup all --delete-days N, переменные MAILCOW_BACKUP_LOCATION и THREADS. https://docs.mailcow.email/backup_restore/b_n_r-backup/
- Docker Blog — Top Tips and Use Cases for Managing Your Volumes — Проверено: команды docker volume ls и docker volume inspect для диагностики реальных томов. https://www.docker.com/blog/top-tips-and-use-cases-for-managing-your-volumes/



