Простой в работе email — это то, без чего я не могу представить свою бизнес-деятельность.

После катастрофического сбоя сервера, из-за которого моя установка Mailcow была полностью недоступна почти 48 часов, я понял, что мне нужно решение лучше, чем стандартные резервные копии.

Проблема заключалась в поиске надежного способа поддержания точной копии моего рабочего сервера Mailcow, на который я мог бы моментально переключиться в случае новой катастрофы.

Я искал существующие инструменты и скрипты, но ничто не соответствовало моим требованиям к настоящей синхронизации в реальном времени, которая бы поддерживала все почтовые данные, конфигурации и тома Docker в идеальном соответствии между серверами.

Большинство решений, которые я нашел, были либо слишком упрощенными (просто резервное копирование файлов конфигурации), либо чрезмерно сложными (требовали специализированного кластерного программного обеспечения).

Поэтому я решил разработать собственный скрипт синхронизации, который был бы одновременно полноценным и простым в реализации.

Особенно ценным это решение делает то, что оно полностью работает по принципу «настроил и забыл» — после настройки оно тихо поддерживает ваш резервный сервер как точную копию вашей основной установки Mailcow.

А если случится катастрофа? Я просто перенаправляю DNS моего домена на IP-адрес резервного сервера, и я снова в деле в течение нескольких минут, а не часов или дней.

В этой статье я проведу вас через весь процесс настройки и поделюсь полным скриптом, который успешно использую уже больше года.

Полное решение синхронизации Mailcow

СИНХРОНИЗАЦИЯ В РЕАЛЬНОМ ВРЕМЕНИ
ЗЕРКАЛИРОВАНИЕ СЕРВЕРА MAILCOW
Архитектура синхронизации Mailcow

Sync script создает точное зеркало вашего основного сервера Mailcow практически без необходимости ручной настройки на резервном сервере.

ОСНОВНОЙ СЕРВЕР
/opt/mailcow-dockerized
Тома Docker
РЕЗЕРВНЫЙ СЕРВЕР
/opt/mailcow-dockerized
Тома Docker
Процесс синхронизации
Все данные синхронизируются через SSH
Конфиг
Данные
Тома
1. Установка без подготовки
Настройка Docker
2. Остановка Docker резервного сервера
Защита данных
3. Синхронизация каталога Mailcow
/opt/mailcow-dockerized
4. Синхронизация томов Docker
Умные исключения
5. Перезапуск сервисов резервного сервера
Готов к фейловер
Технические особенности:
Этот подход к синхронизации сохраняет все критические элементы, включая права доступа к файлам, владение и тома Docker. Тщательно выстроенная последовательность обеспечивает целостность данных, поддерживая рабочий резервный server.
Визуализация процесса синхронизации Mailcow | Создано hostbor

Прежде чем погрузиться в детали, позвольте мне объяснить, что на самом деле делает мое решение.

Этот скрипт выполняет полную синхронизацию вашего основного сервера Mailcow с резервным сервером, обеспечивая идеальное отражение всех данных, конфигураций и томов Docker.

Вот что включает в себя процесс синхронизации:

  • Установка Docker и Docker Compose на резервном сервере (при необходимости)
  • Остановка Docker на резервном сервере перед синхронизацией
  • Синхронизация всей структуры директорий Mailcow
  • Синхронизация всех томов Docker (с определенными исключениями, где необходимо)
  • Перезапуск Docker и контейнеров Mailcow на резервном сервере
  • Надежная обработка ошибок с уведомлениями через Pushover

Давайте разберем скрипт синхронизации шаг за шагом.

Настройка скрипта - начальная конфигурация

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

Чтобы гарантировать подключение скрипта к правильному резервному серверу, я устанавливаю переменную `TARGET_SERVER`. Вам нужно будет изменить это:

TARGET_SERVER=""    # Имя хоста или IP-адрес резервного сервера

Замените заполнитель на фактический IP-адрес или имя хоста вашего резервного сервера.

Далее я определяю пользователя SSH для подключения к резервному серверу:

TARGET_USER="root"                          # Пользователь SSH на резервном сервере

В моей настройке я использую root, потому что скрипту необходим доступ к системным каталогам и возможность перезапуска служб, но вы можете использовать другого пользователя с привилегиями sudo, если предпочитаете.

Затем я указываю стандартный каталог Mailcow и расположения томов Docker:

MAILCOW_DIR="/opt/mailcow-dockerized"
DOCKER_VOLUMES="/var/lib/docker/volumes"

По моему опыту, это расположение по умолчанию для большинства установок Mailcow, но вы должны скорректировать их, если установили Mailcow в другом месте.

Одно важное исключение, которое мне нужно было добавить, было для тома rspamd:

EXCLUDES="--exclude rspamd-vol-1"            # Исключить том Rspamd при необходимости

Я обнаружил, что rspamd иногда может вызывать проблемы при синхронизации между серверами с разными архитектурами, поэтому исключение его было более безопасным вариантом в моей настройке.

Для безопасности я использую нестандартный порт SSH и выделенный SSH-ключ:

SSH_PORT=47825                              # SSH-порт для резервного сервера
SSH_KEY="/root/.ssh/id_ed25519_mailcow"      # Путь к пользовательскому SSH-ключу

Я настоятельно рекомендую использовать выделенный SSH-ключ и нестандартный порт для повышения безопасности, особенно поскольку этот скрипт будет работать автоматически.

Параметры rsync, которые я использую, гарантируют сохранение разрешений, владения и жестких ссылок:

RSYNC_OPTS="-aHhP --numeric-ids --delete -e 'ssh -i $SSH_KEY -p $SSH_PORT'"

Флаг `--numeric-ids` был особенно важен в моем тестировании, так как он гарантирует, что идентификаторы пользователей и групп точно соответствуют между серверами.

Для уведомлений об ошибках я настроил интеграцию с Pushover:

PUSHOVER_API_KEY="<ваш-ключ-api-pushover>"
PUSHOVER_USER_KEY="<ваш-пользовательский-ключ-pushover>"

Вам нужно будет заменить их своими собственными ключами Pushover для получения уведомлений.

Наконец, я указываю расположение файла журнала:

LOG_FILE="/var/log/sync_mailcow.log"

Это фиксирует все действия по синхронизации, что было бесценно для устранения неполадок.

Надежная обработка ошибок и уведомления

После нескольких итераций моего скрипта я понял, что правильная обработка ошибок абсолютно критична для такого автоматизированного процесса.

При запуске скрипта через cron никто не следит за выводом терминала, поэтому мне нужен был способ обнаружить любые сбои и получать уведомления немедленно.

Сначала я создал функцию для отправки уведомлений через Pushover:

send_pushover_notification() {
	local message="$1"
	curl -s \
		--form-string "token=$PUSHOVER_API_KEY" \
		--form-string "user=$PUSHOVER_USER_KEY" \
		--form-string "message=$message" \
		https://api.pushover.net/1/messages.json > /dev/null
}

Эта функция использует curl для отправки сообщения в API Pushover всякий раз, когда она вызывается.

Для ведения журнала я создал простую функцию логирования:

log() {
	echo "$(date +'%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}

Это добавляет временные метки ко всем записям журнала и выводит их как в терминал, так и в файл журнала.

Сердцем моей системы обработки ошибок является функция `handle_error`:

handle_error() {
	local last_command="$1"
	log "ERROR: Command failed: $last_command"
	send_pushover_notification "Mailcow Sync Error: Command failed: $last_command"
	exit 1
}

Эта функция принимает неудачную команду в качестве аргумента, регистрирует ошибку, отправляет уведомление, а затем завершает скрипт с кодом статуса 1.

Но самая мощная часть — это то, как я перехватываю ошибки по всему скрипту:

trap 'handle_error "$BASH_COMMAND"' ERR

Эта команда `trap` работает как страховочная сетка, которая автоматически ловит любую команду в скрипте, которая завершается неудачно (выходит с ненулевым статусом) и немедленно выполняет мою пользовательскую функцию `handle_error`.

Я нашел этот подход особенно полезным, потому что он использует встроенную переменную `$BASH_COMMAND` чтобы сообщить мне, какая именно команда не удалась, что значительно облегчает устранение неполадок.

Когда происходит ошибка, скрипт немедленно останавливается, регистрирует ошибку и отправляет мне уведомление – предотвращая потенциально каскадные сбои или неполные синхронизации.

💡
В производственной среде вы должны заменить плейсхолдеры `PUSHOVER_API_KEY` и `PUSHOVER_USER_KEY` на собственные ключи, чтобы уведомления работали. Вы также можете адаптировать эту функцию для использования других методов уведомления, если предпочитаете.

Подготовка SSH-ключа

Перед началом основных задач синхронизации я проверяю, что SSH-ключ имеет правильные разрешения:

log "Setting correct permissions for SSH key..."
chmod 600 "$SSH_KEY"

Это лучшая практика безопасности, которой я всегда следую — приватные ключи SSH должны быть доступны для чтения только владельцу.

Я узнал это на собственном опыте, когда мой скрипт изначально не работал из-за неправильных разрешений SSH-ключа после того, как я скопировал его с другой системы.

💪
Перед первым запуском этого скрипта вам необходимо сгенерировать пару SSH-ключей и скопировать открытый ключ на ваш резервный сервер. Я рекомендую использовать выделенную пару ключей для этого скрипта.

Вы можете сгенерировать новую пару ключей с помощью этой команды:

ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519_mailcow -C "mailcow-sync"

Затем скопируйте открытый ключ на ваш резервный сервер:

ssh-copy-id -i /root/.ssh/id_ed25519_mailcow.pub -p <ваш-ssh-порт> root@

Я рекомендую протестировать SSH-соединение перед запуском полного скрипта, чтобы убедиться, что все настроено правильно:

ssh -i /root/.ssh/id_ed25519_mailcow -p <ваш-ssh-порт> root@ "echo Соединение успешно"

Настройка Docker и Docker Compose на резервном сервере

Следующая часть моего скрипта гарантирует, что Docker и Docker Compose правильно установлены на резервном сервере:

log "Installing Docker and Docker Compose on the backup server..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" <<'EOF'
  # Install Docker using the recommended method
  curl -sSL https://get.docker.com/ | CHANNEL=stable sh
  # Enable and start Docker service
  systemctl enable --now docker
  # Install the latest Docker Compose
  curl -L https://github.com/docker/compose/releases/download/v$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose
  chmod +x /usr/local/bin/docker-compose
  # Verify installations
  docker --version || { echo "Docker installation failed!" >&2; exit 1; }
  docker-compose --version || { echo "Docker Compose installation failed!" >&2; exit 1; }
EOF

Этот раздел невероятно мощный, потому что он обеспечивает то, что я называю настройкой резервного сервера с "нулевой подготовкой".

Он автоматически устанавливает Docker и правильную версию Docker Compose, если они еще не присутствуют на резервном сервере.

Создание этой возможности "нулевой подготовки" было значительной целью для меня при разработке этого скрипта.

Это означает, что ваш резервный сервер требует почти никакой ручной подготовки перед запуском скрипта синхронизации в первый раз.

Все, что вам нужно — это базовая установка ОС — я лично протестировал и подтвердил, что это работает плавно на последних версиях Ubuntu и Debian — и настроенный SSH-доступ. Вот и все! Не нужно вручную устанавливать Docker, настраивать Docker Compose или беспокоиться о совместимости версий.

Этот подход экономит значительное время и усилия при первоначальном развертывании по сравнению с ручной настройкой сред Docker на обоих серверах.

Этот раздел использует heredoc (EOF) для выполнения нескольких команд на удаленном сервере в одном SSH-сеансе.

Сначала я устанавливаю Docker, используя официальный скрипт установки, затем включаю и запускаю службу Docker.

Далее я устанавливаю последнюю версию Docker Compose, совместимую с Mailcow, используя проверку версии Servercow для обеспечения совместимости.

Наконец, я проверяю, что как Docker, так и Docker Compose установлены правильно, проверяя их версии.

Я добавил шаг проверки после того, как столкнулся с разочаровывающей проблемой, когда Docker, казалось, установился, но на самом деле не работал должным образом.

✔️
Этот подход означает, что вы можете взять свежий сервер с ничем, кроме SSH-доступа, запустить этот скрипт и получить полноценно функционирующий резервный сервер Mailcow без других ручных шагов настройки. Это сэкономило мне бесчисленные часы при настройке новых резервных сред!

Остановка Docker на резервном сервере

Перед синхронизацией каких-либо данных я останавливаю Docker на резервном сервере, чтобы предотвратить любые конфликты:

log "Stopping Docker on the backup server..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" "systemctl stop docker.service && docker ps -a" || handle_error "Stop Docker"

Я нашел этот шаг необходимым, потому что попытка синхронизировать тома Docker, когда контейнеры работают, может привести к повреждению данных.

Команда `docker ps -a` включена для проверки, что ни один контейнер не работает после остановки службы.

Я также добавил короткую задержку, чтобы убедиться, что Docker полностью остановился перед тем, как продолжить:

log "Waiting for Docker to stop..."
sleep 10

Эта пауза предотвратила несколько проблем с состоянием гонки, которые я испытал в ранних версиях моего скрипта.

Синхронизация директории Mailcow

Далее я синхронизирую всю директорию Mailcow с резервным сервером:

log "Syncing /opt/mailcow-dockerized..."
rsync -aHhP --numeric-ids --delete -e "ssh -i $SSH_KEY -p $SSH_PORT" "$MAILCOW_DIR/" "$TARGET_USER@$TARGET_SERVER:$MAILCOW_DIR/" || handle_error "Sync /opt/mailcow-dockerized"

Эта команда rsync использует несколько важных флагов:

  • `-a`: Режим архивирования, который сохраняет разрешения, владение, временные метки и т.д.
  • `-H`: Сохраняет жесткие ссылки
  • `-h`: Вывод в человекочитаемом формате
  • `-P`: Показывает прогресс и сохраняет частичные передачи
  • `--numeric-ids`: Сохраняет идентификаторы пользователей и групп
  • `--delete`: Удаляет файлы в пункте назначения, которые не существуют в источнике

Флаг `--numeric-ids` особенно важен для файлов, связанных с Docker, поскольку он обеспечивает точное соответствие идентификаторов пользователей и групп между серверами.

Завершающие слэши в путях имеют решающее значение — они указывают rsync копировать содержимое директории, а не саму директорию.

Синхронизация томов Docker

После директории Mailcow я синхронизирую все тома Docker:

log "Syncing /var/lib/docker/volumes..."
rsync -aHhP --numeric-ids --delete --exclude="rspamd-vol-1" -e "ssh -i $SSH_KEY -p $SSH_PORT" "$DOCKER_VOLUMES/" "$TARGET_USER@$TARGET_SERVER:$DOCKER_VOLUMES/" || handle_error "Sync /var/lib/docker/volumes"

Эта команда похожа на предыдущую rsync, но с одним важным дополнением: флагом `--exclude="rspamd-vol-1"`.

Я обнаружил, что rspamd иногда может вызывать проблемы при синхронизации между серверами с разными архитектурами, поэтому его исключение было более безопасным в моей настройке.

💪
Если вы запускаете другие контейнеры Docker на том же сервере, что и Mailcow, вы можете добавить больше исключений, чтобы предотвратить синхронизацию томов этих контейнеров.

Вы могли бы изменить команду, чтобы включать только тома, связанные с Mailcow, но я обнаружил, что исключение конкретных проблемных томов было проще и надежнее в моем случае.

Запуск Docker и инициализация Mailcow

После завершения синхронизации я запускаю Docker на резервном сервере:

log "Starting Docker on the backup server..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" "systemctl start docker.service" || handle_error "Start Docker"

После запуска Docker я жду немного, чтобы он инициализировался:

log "Waiting for Docker to initialize..."
sleep 15

Эта задержка критически важна — я обнаружил, что попытки взаимодействия с Docker слишком быстро после запуска службы часто приводят к ошибкам.

Далее я загружаю последние образы Docker Mailcow на резервном сервере:

log "Pulling Mailcow Docker images on the backup server..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" "cd $MAILCOW_DIR && docker-compose pull" || handle_error "Pull Mailcow Docker images"

Это гарантирует, что все контейнеры используют правильные образы перед запуском.

Наконец, я запускаю стек Mailcow на резервном сервере:

log "Starting Mailcow stack on the backup server..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" "cd $MAILCOW_DIR && docker-compose up -d" || handle_error "Start Mailcow containers"

Это поднимает все контейнеры Mailcow, делая резервный сервер полностью функциональным и готовым к работе при необходимости.

Я завершаю скрипт финальной записью в журнале:

log "Backup server synchronization and Docker restart completed successfully!"

Настройка автоматизации с помощью Cron

Чтобы эта синхронизация действительно работала по принципу "настроил и забыл", мне нужно было автоматизировать ее с помощью cron.

Сначала я сохранил мой скрипт в файл (я назвал его `/root/C-mailcow_sync_and_reboot_backup_nf.sh`) и сделал его исполняемым:

chmod +x /root/C-mailcow_sync_and_reboot_backup_nf.sh

Затем я добавил задание cron для запуска его ежедневно в 2 часа ночи:

crontab -e

И добавил эту строку:

0 2 * * * /root/C-mailcow_sync_and_reboot_backup_nf.sh > /dev/null 2>&1

В моей производственной среде я фактически запускаю синхронизацию два раза в день, чтобы обеспечить минимальную потерю данных в случае сбоя:

0 2,14 * * * /root/C-mailcow_sync_and_reboot_backup_nf.sh > /dev/null 2>&1

✔️
Перенаправление `> /dev/null 2>&1` подавляет весь вывод от задания cron, поскольку уведомления об ошибках обрабатываются через Pushover, а вся важная информация записывается в файл журнала.

Вы также можете запускать синхронизацию чаще, если объем вашей почты высок, и вы хотите минимизировать потенциальную потерю данных.

Соображения по файрволу

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

Если вы используете UFW (Uncomplicated Firewall), вы можете добавить правило вроде этого на вашем резервном сервере:

ufw allow from  to any port <ваш-ssh-порт> proto tcp

Это разрешает только SSH-соединения с IP-адреса вашего основного сервера на указанный порт, повышая безопасность.

В моей настройке я также дополнительно ограничиваю SSH-доступ, используя только аутентификацию по SSH-ключу (отключая аутентификацию по паролю).

Полный скрипт синхронизации

Вот полный скрипт с заполнителями для конфиденциальной информации:

#!/bin/bash
# --- Конфигурация ---
# --- Замените заполнители ниже своими фактическими значениями ---
TARGET_SERVER="" # Имя хоста или IP-адрес резервного сервера
TARGET_USER="root"                                  # Пользователь SSH на резервном сервере (убедитесь, что этот пользователь имеет необходимые разрешения)
MAILCOW_DIR="/opt/mailcow-dockerized"               # Каталог установки Mailcow по умолчанию (измените, если отличается)
DOCKER_VOLUMES="/var/lib/docker/volumes"            # Каталог томов Docker по умолчанию (измените, если отличается)
EXCLUDES="--exclude rspamd-vol-1"                   # Тома, которые нужно исключить из синхронизации (добавьте больше флагов --exclude при необходимости, например, --exclude plausible_event-data)
SSH_PORT=<ваш-ssh-порт>                            # SSH-порт для резервного сервера (например, 22 или пользовательский порт)
SSH_KEY="<путь-к-вашему-приватному-ssh-ключу>"            # Полный путь к приватному SSH-ключу для подключения к резервному серверу (например, /root/.ssh/id_ed25519_mailcow)
PUSHOVER_API_KEY="<ваш-ключ-api-pushover>"          # Ваш ключ приложения Pushover API/токен (оставьте пустым или закомментируйте строки Pushover, если не используется)
PUSHOVER_USER_KEY="<ваш-пользовательский-ключ-pushover>"        # Ваш пользовательский ключ Pushover (оставьте пустым или закомментируйте строки Pushover, если не используется)
LOG_FILE="/var/log/sync_mailcow.log"                # Путь к файлу журнала для этого скрипта
RSYNC_OPTS="-aHhP --numeric-ids --delete -e 'ssh -i $SSH_KEY -p $SSH_PORT'" # Параметры rsync по умолчанию
# Временный файл для захвата stderr rsync для подробного сообщения об ошибке
RSYNC_ERR_LOG="/tmp/sync_mailcow_rsync_error.log"
# --- Функции ---
# Функция для отправки уведомлений Pushover (используется только при ошибках)
# Измените или замените эту функцию, если вы используете другой метод уведомления
send_pushover_notification() {
    # Проверяем, заданы ли ключи перед попыткой отправки
    if [ -n "$PUSHOVER_API_KEY" ] && [ -n "$PUSHOVER_USER_KEY" ]; then
        local message="$1"
        curl -s \
            --form-string "token=$PUSHOVER_API_KEY" \
            --form-string "user=$PUSHOVER_USER_KEY" \
            --form-string "message=$message" \
            https://api.pushover.net/1/messages.json > /dev/null
    else
        log "Ключи Pushover не заданы, пропуск уведомления."
    fi
}
# Функция журналирования: Добавляет временную метку и записывает в файл журнала и консоль
log() {
    echo "$(date +'%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
# Обработчик ошибок: Журналирует ошибку, отправляет уведомление, очищает временный файл и выходит
handle_error() {
    local exit_code=$? # Захватываем код выхода неудачной команды
    local last_command="$1"
    local error_message # Переменная для хранения итогового сообщения для Pushover
    log "ОШИБКА: Команда '$last_command' завершилась с кодом выхода $exit_code." # Журналируем неудачную команду и код выхода
    # Проверяем, существует ли конкретный журнал ошибок rsync и имеет ли он содержимое
    if [[ -s "$RSYNC_ERR_LOG" ]]; then
        # Читаем первые несколько строк (например, 5) из файла ошибок, чтобы сделать уведомление кратким
        local specific_error=$(head -n 5 "$RSYNC_ERR_LOG")
        log "Детали конкретной ошибки: $specific_error" # Журналируем захваченные детали
        # Подготавливаем детальное сообщение Pushover
        error_message="Ошибка синхронизации Mailcow: '$last_command' не удалась. Детали: $specific_error"
    else
        # Подготавливаем общее сообщение Pushover, если детали не были захвачены
        error_message="Ошибка синхронизации Mailcow: '$last_command' не удалась (Код выхода: $exit_code). Конкретные детали rsync не захвачены."
        log "Конкретные детали ошибки rsync не захвачены в $RSYNC_ERR_LOG."
    fi
    # Отправляем уведомление
    send_pushover_notification "$error_message"
    # Очищаем временный файл ошибок
    rm -f "$RSYNC_ERR_LOG"
    exit 1 # Выходим из скрипта
}
# --- Основной скрипт ---
# Перехватываем ошибки и вызываем обработчик ошибок
# Команда 'trap' гарантирует, что если какая-либо команда не удается (выходит с ненулевым статусом),
# функция 'handle_error' вызывается автоматически, передавая неудавшуюся команду ($BASH_COMMAND)
trap 'handle_error "$BASH_COMMAND"' ERR
# Убеждаемся, что SSH-ключ существует и имеет правильные разрешения
if [ ! -f "$SSH_KEY" ]; then
    log "ОШИБКА: Файл SSH-ключа не найден в $SSH_KEY"
    # Отправляем уведомление, даже если функция журнала позже не удастся
    send_pushover_notification "Ошибка синхронизации Mailcow: Файл SSH-ключа не найден в $SSH_KEY"
    exit 1
fi
log "Установка правильных разрешений для SSH-ключа..."
chmod 600 "$SSH_KEY"
if [ $? -ne 0 ]; then
    # Обрабатываем сбой chmod специально, так как trap может не поймать его, если скрипт выходит здесь
    log "ОШИБКА: Не удалось установить разрешения на SSH-ключ $SSH_KEY"
    send_pushover_notification "Ошибка синхронизации Mailcow: Не удалось установить разрешения на SSH-ключ $SSH_KEY"
    exit 1
fi
log "Запуск процесса синхронизации Mailcow..."
# Убеждаемся, что Docker и Docker Compose установлены на резервном сервере
# Это использует heredoc (<<'EOF') для запуска нескольких команд на удаленном сервере через SSH.
# Он устанавливает Docker, включает/запускает службу, устанавливает последний совместимый Docker Compose,
# и проверяет обе установки. Это позволяет резервному серверу быть настроенным автоматически.
log "Проверка и установка Docker и Docker Compose на резервном сервере..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" <<'EOF'
  # Устанавливаем Docker рекомендуемым методом
  echo "Проверка и установка Docker при необходимости..."
  if ! command -v docker > /dev/null; then
    curl -fsSL https://get.docker.com -o get-docker.sh
    sh get-docker.sh
    rm get-docker.sh
  else
    echo "Docker уже установлен."
  fi
  # Включаем и запускаем службу Docker
  echo "Проверка, что служба Docker включена и запущена..."
  sudo systemctl enable --now docker
  # Устанавливаем последний Docker Compose, совместимый с Mailcow
  echo "Проверка и установка Docker Compose при необходимости..."
  COMPOSE_URL="https://github.com/docker/compose/releases/download/v$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m)"
  COMPOSE_DEST="/usr/local/bin/docker-compose"
  if ! command -v docker-compose > /dev/null || ! docker-compose version | grep -q "$(curl -Ls https://www.servercow.de/docker-compose/latest.php)"; then
      echo "Загрузка Docker Compose из $COMPOSE_URL..."
      sudo curl -L "$COMPOSE_URL" -o "$COMPOSE_DEST"
      sudo chmod +x "$COMPOSE_DEST"
  else
      echo "Docker Compose уже установлен и кажется актуальным для Mailcow."
  fi
  # Проверяем установки
  echo "Проверка установок..."
  docker --version || { echo "Проверка Docker не удалась!" >&2; exit 1; }
  docker-compose --version || { echo "Проверка Docker Compose не удалась!" >&2; exit 1; }
  echo "Настройка Docker и Docker Compose проверена."
EOF
# Примечание: Trap ERR поймает сбои в самой команде SSH (например, отказ в соединении)
# или если удаленный скрипт, вызванный SSH, выходит с ненулевым статусом (из-за 'exit 1' в heredoc).
# Останавливаем Docker на резервном сервере перед синхронизацией томов
log "Остановка Docker на резервном сервере..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" "sudo systemctl stop docker.service && docker ps -a" # docker ps -a подтверждает, что ни один контейнер не работает
# Ждем момент, чтобы убедиться, что Docker полностью остановился
log "Ожидание остановки Docker..."
sleep 10
# Синхронизируем директорию /opt/mailcow-dockerized (конфигурации и т.д.)
log "Синхронизация $MAILCOW_DIR..."
# Используем $RSYNC_OPTS, определенные выше. Сохраняем разрешения, числовые ID, удаляем лишние файлы на цели.
rsync $RSYNC_OPTS "$MAILCOW_DIR/" "$TARGET_USER@$TARGET_SERVER:$MAILCOW_DIR/"
# Синхронизируем директорию /var/lib/docker/volumes (почтовые данные, базы данных и т.д.)
log "Синхронизация $DOCKER_VOLUMES..."
# Очищаем предыдущий журнал ошибок rsync
> "$RSYNC_ERR_LOG"
# Используем $RSYNC_OPTS и $EXCLUDES, определенные выше. Перенаправляем stderr для подробного отчета об ошибках.
rsync $RSYNC_OPTS $EXCLUDES "$DOCKER_VOLUMES/" "$TARGET_USER@$TARGET_SERVER:$DOCKER_VOLUMES/" 2> "$RSYNC_ERR_LOG"
# Обработка ошибок для этого конкретного rsync выполняется через механизм || и trap, используя захваченный stderr
# Удаляем временный журнал ошибок, если rsync был успешным (опционально, handle_error также удаляет его при сбое)
if [ $? -eq 0 ]; then
    rm -f "$RSYNC_ERR_LOG"
fi
log "Синхронизация успешно завершена!"
# Запускаем Docker на резервном сервере
log "Запуск Docker на резервном сервере..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" "sudo systemctl start docker.service"
# Ждем, пока Docker правильно инициализируется
log "Ожидание инициализации Docker..."
sleep 15
# Подтягиваем последние образы Docker Mailcow на резервном сервере
log "Загрузка образов Docker Mailcow на резервном сервере..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" "cd '$MAILCOW_DIR' && docker-compose pull"
# Запускаем контейнеры Mailcow на резервном сервере
log "Запуск стека Mailcow на резервном сервере..."
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$TARGET_USER@$TARGET_SERVER" "cd '$MAILCOW_DIR' && docker-compose up -d"
log "Синхронизация резервного сервера и перезапуск Docker успешно завершены!"

Полный скрипт также доступен на нашем GitHub по адресу https://github.com/hostbor/mailcowsync/ для удобного ознакомления и обновлений.

Стратегия аварийного переключения

БЫСТРАЯ СТРАТЕГИЯ ФЕЙЛОВЕР
ПОДХОД С МИНИМАЛЬНЫМ ПРОСТОЕМ
ДО ФЕЙЛОВЕР
ЗАПИСИ DNS
ОСНОВНОЙ
РЕЗЕРВНЫЙ
Записи DNS указывают на основной server. Почтовый трафик идет нормально. Резервный server синхронизирован, но неактивен.
ПОСЛЕ ФЕЙЛОВЕР
ЗАПИСИ DNS
(ОФФЛАЙН)
ОСНОВНОЙ
(ОНЛАЙН)
РЕЗЕРВНЫЙ
Записи DNS обновлены и указывают на резервный server. Идентичная среда обеспечивает минимальные неудобства для пользователей.
Хронология Фейловер
0:00
Основной server недоступен
Оповещение от мониторинга или пользователей
0:02
Первичная оценка
Быстрая диагностика возможности исправления
0:05
Решение о Фейловер
Запуск процедуры фейловер при вероятности долгого простоя
0:06
Обновление DNS
Обновление A/AAAA записей почты на IP резервного server
0:15
Сервисы восстановлены
Почта доступна по мере обновления DNS у пользователей
Ключевые преимущества:
Этот подход обеспечивает восстановление за 5-15 минут (зависит от DNS TTL). Так как резервный server - точная копия, переход происходит плавно и без потерь данных.
Визуализация стратегии фейловер Mailcow | Создано hostbor

Реальная ценность этой настройки становится очевидной, когда ваш основной сервер выходит из строя.

Вот мой процесс аварийного переключения:

  1. Когда я обнаруживаю, что основной сервер не работает (либо через оповещения мониторинга, либо через сообщения пользователей), я сначала пытаюсь выполнить базовое устранение неполадок.
  2. Если проблему нельзя быстро решить, я вхожу в панель управления своего DNS-провайдера.
  3. Я обновляю A/AAAA-записи для своего почтового домена(ов), чтобы они указывали на IP-адрес резервного сервера.
  4. В зависимости от вашего DNS-провайдера и настроек TTL, изменение распространяется в течение минут или часов.
  5. Почтовая служба восстанавливается после завершения распространения DNS.

По моему опыту, при правильно настроенных TTL DNS, служба может быть восстановлена в течение 5-15 минут после внесения изменений в DNS.

✔️
Для еще более быстрого аварийного переключения вы можете использовать плавающий IP-адрес, который можно быстро переназначить с одного сервера на другой, устраняя необходимость ждать распространения DNS.

Часто задаваемые вопросы

Что делать, если я запускаю другие контейнеры Docker на том же сервере, где работает Mailcow?

Если вы запускаете другие контейнеры Docker на том же сервере, вам следует исключить их тома из синхронизации, чтобы избежать потенциальных проблем.

Вы можете добавить больше исключений в команду rsync, аналогично тому, как мы исключили том rspamd:

rsync -aHhP --numeric-ids --delete --exclude="rspamd-vol-1" --exclude="other-container-vol" --exclude="another-container-vol" -e "ssh -i $SSH_KEY -p $SSH_PORT" "$DOCKER_VOLUMES/" "$TARGET_USER@$TARGET_SERVER:$DOCKER_VOLUMES/"

В качестве альтернативы, вы могли бы изменить скрипт, чтобы синхронизировать только конкретные тома, связанные с Mailcow, но это потребовало бы большего обслуживания, поскольку обновления Mailcow могут добавлять новые тома.

Могу ли я использовать другие уведомления, кроме Pushover?

Да, вы можете легко изменить скрипт для использования различных методов уведомления.

Например, чтобы использовать уведомления по электронной почте вместо этого, вы можете заменить функцию `send_pushover_notification` на что-то вроде:

send_email_notification() {
    local message="$1"
    echo "$message" | mail -s "Mailcow Sync Error" [email protected]
}

Или для уведомлений Telegram:

send_telegram_notification() {
    local message="$1"
    curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
        -d chat_id="${TELEGRAM_CHAT_ID}" \
        -d text="$message"
}

Просто не забудьте обновить функцию `handle_error`, чтобы вызывать вашу новую функцию уведомления.

Что делать, если выходит новое обновление Mailcow?

Когда доступно новое обновление Mailcow, я рекомендую сначала обновить резервный сервер, чтобы проверить, все ли работает правильно.

После того, как вы убедились, что обновление работает должным образом на резервном сервере, вы можете обновить основной сервер.

Crontab синхронизации может оставаться как есть - во время следующей синхронизации любые обновленные файлы конфигурации или образы Docker будут автоматически синхронизированы с резервным сервером.

Этот подход минимизирует риск простоя, если обновление вызывает проблемы.

Как обрабатывать учетные данные базы данных и другую конфиденциальную информацию?

Скрипт автоматически синхронизирует все файлы конфигурации, включая учетные данные базы данных и другую конфиденциальную информацию.

Это преднамеренно и необходимо для правильной работы резервного сервера.

Для обеспечения безопасности:

  • Используйте сильные, уникальные пароли для всех служб
  • Ограничьте доступ к обоим серверам с помощью правильных правил файрвола
  • Используйте только аутентификацию по SSH-ключу (отключите аутентификацию по паролю)
  • Убедитесь, что оба сервера имеют актуальные обновления безопасности
💪
Помните, что ваш резервный сервер имеет тот же уровень доступа к вашим почтовым данным, что и основной сервер, поэтому он должен быть соответствующим образом защищен.

Как этот скрипт справляется с большими объемами почты?

Как этот скрипт справляется с большими объемами почты? Иллюстрация почтового сервера mailcow с логотипом коровы, демонстрирующая синхронизацию электронной почты.

Скрипт использует rsync, который чрезвычайно эффективен для передачи только изменений с момента последней синхронизации.

В моей производственной среде с несколькими гигабайтами почтовых данных начальная синхронизация заняла около часа, но последующие синхронизации обычно завершаются всего за несколько минут.

Если у вас очень большие объемы почты, вы можете:

  • Выполнять начальную синхронизацию вручную, чтобы отслеживать ее прогресс
  • Рассмотреть возможность использования выделенного сетевого интерфейса или увеличенной пропускной способности между серверами
  • Запускать синхронизацию чаще, чтобы уменьшить объем данных, передаваемых при каждом запуске

Заключение

После внедрения этого скрипта синхронизации я испытал значительное улучшение моего душевного спокойствия в отношении моей почтовой инфраструктуры.

Знание того, что у меня есть точная копия моего рабочего сервера Mailcow, готовая взять на себя управление в любой момент, оказалось бесценным.

Скрипт надежно работает более года, и я успешно тестировал процесс аварийного переключения несколько раз во время запланированных окон обслуживания.

MAILCOW SYNC
КЛЮЧЕВЫЕ ПРЕИМУЩЕСТВА И РЕЗУЛЬТАТЫ
Минимум простоя
Благодаря синхронизированным серверам время восстановления сокращается до минут вместо часов или дней, минимизируя влияние сбоев на бизнес.
Без потери данных
Полная синхронизация гарантирует сохранность всех писем и конфигураций, исключая риск потери важной информации.
Простая настройка
Автоматизированный script требует минимум настроек и управляет всей синхронизацией Docker и томов с умными параметрами по умолчанию.
Показатели производительности
Время восст.
5-15 мин
вместо 24-48ч (backup)
Точность
100%
Полное зеркало
Обслуживание
Почти ноль
Автоматически
Частота sync
Настраиваемая
1-2 раза/день
Долгосрочная дорожная карта устойчивости
СРАЗУ
Внедрение регулярной sync
Настройте автоматическую синхронизацию с помощью предоставленного script с расписанием дважды в день.
БЛИЖАЙШИЕ
Тестирование процесса failover
Проводите регулярные плановые тесты failover во время окон обслуживания для проверки процессов.
СРЕДНИЕ
Автоматический мониторинг
Внедрите систему мониторинга, которая автоматически обнаруживает сбой основного server.
ДАЛЬНИЕ
Автоматическое failover
Расширьте систему автоматическими обновлениями DNS, запускаемыми проверками состояния server.
Личный опыт:
"После внедрения этого script синхронизации я стал гораздо спокойнее за свою почтовую инфраструктуру. Когда у провайдера моего основного server случился длительный сбой, переход на backup прошел незаметно для пользователей."
Обзор преимуществ синхронизации Mailcow | Создано hostbor

Мне даже пришлось использовать его в реальной чрезвычайной ситуации, когда провайдер моего основного сервера испытал продолжительный сбой, и переход был плавным с точки зрения моих пользователей.

Это решение обеспечивает идеальный баланс между простотой и комплексностью — его легко настроить и поддерживать, но при этом оно обеспечивает правильную синхронизацию всех критически важных компонентов вашей установки Mailcow.

Независимо от того, используете ли вы Mailcow для малого бизнеса или крупной организации, наличие надежной стратегии резервного копирования необходимо, и я надеюсь, что этот скрипт поможет вам достичь этой цели так же эффективно, как и мне.

Пусть ваша электронная почта работает бесперебойно, даже когда случается катастрофа!