Для приготовления auto-failover и auto-rejoin в docker swarm нам понадобится docker, postgres, repmgr, pgbouncer, runit и gluster. Можно также воспользоваться готовым образом.

Для начала на двух хостах (лучше железных) docker1 и docker2 организуем docker swarm так, чтобы оба были менеджерами.

Также, на оба хоста установим распределённую файловую систему glusterfs и подмонтируем его на обоих хостах в fstab так

localhost:/gfs /mnt/gfs glusterfs defaults,_netdev,backupvolfile-server=localhost 0 0

Потом на обоих хостах создадим раздел

docker volume create repmgr

и сеть

docker network create --attachable --driver overlay docker

Затем соберём образ с помощью докер-файла:

Dockerfile
FROM alpine
RUN set -ex     && apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing --virtual .locales-rundeps         musl-locales \ # добавлям локали (это не является необходимым, но для красоты картины пусть будет)
    && apk add --no-cache --virtual .postgresql-rundeps         openssh-client \ # добавляем ssh-клиента (это не является необходимым, но может понадобиться для генерации ключей)
        openssh-server \ # добавляем ssh-сервер (это не является необходимым, но требуется для switchover)
        pgbouncer \ # добавляем балансировщик
        postgresql \ # добавляем базу
        postgresql-contrib \ # добавляем различные полезные расширения (это не является необходимым, но для красоты картины пусть будет)
        repmgr \ # добавляем repmgr
        repmgr-daemon \ # и сервис для него
        rsync \ # добавляем синхронизатор (это не является необходимым, но для красоты картины пусть будет)
        runit \ # добавляем лёгкий супервизор
        shadow \ # добавляем управление пользователями
        tzdata \ # добавляем зоны (это не является необходимым, но для красоты картины пусть будет)
    && echo done
ADD bin /usr/local/bin # добавляем скрипты
ADD service /etc/service # добавляем сервисы для супервизора
CMD [ "runsvdir", "/etc/service" ] # будем запускать супервизор
ENTRYPOINT [ "docker_entrypoint.sh" ] # но после входа
ENV HOME=/var/lib/postgresql
ENV GROUP=postgres     PGDATA="${HOME}/pg_data"     USER=postgres
VOLUME "${HOME}"
WORKDIR "${HOME}"
RUN set -ex     && sed -i -e 's|#PasswordAuthentication yes|PasswordAuthentication no|g' /etc/ssh/sshd_config \ # запрещаем вход по паролю
    && sed -i -e 's|#   StrictHostKeyChecking ask|   StrictHostKeyChecking no|g' /etc/ssh/ssh_config \ # не будем ничего проверять
    && echo "   UserKnownHostsFile=/dev/null" >>/etc/ssh/ssh_config \ # и не будем ничего никуда добавлять
    && sed -i -e 's|postgres:!:|postgres::|g' /etc/shadow \ # разрешаем вход пользователю базы
    && chmod -R 0755 /etc/service /usr/local/bin     && echo done


Теперь запускаем по сервису на каждом хосте

service1.sh
docker service create     --constraint node.hostname==docker1 \ # размещаем контейнер на первом хосте
    --env GROUP_ID="$(id -g)" \ # задаём идентификатор группы (это не является необходимым и сделано для удобства)
    --env LANG=ru_RU.UTF-8 \ # задаём кодировку (это не является необходимым, но для красоты картины пусть будет)
    --env TZ=Asia/Yekaterinburg \ # задаём зону (это не является необходимым, но для красоты картины пусть будет)
    --env USER_ID="$(id -u)" \ # задаём идентификатор пользователя (это не является необходимым и сделано для удобства)
    --hostname tasks.repmgr1 \ # задаём хост
    --mount type=bind,source=/etc/certs,destination=/etc/certs,readonly \ # монтируем папку с сертификатами (это не является необходимым, но для красоты картины пусть будет)
    --mount type=bind,source=/mnt/gfs/repmgr,destination=/var/lib/postgresql/gfs \ # монтируем распределённую файловую систему
    --mount type=volume,source=repmgr,destination=/var/lib/postgresql \ # монтируем раздел
    --name repmgr1 \ # задаём имя сервиса
    --network name=docker \ # используем сеть
    --publish target=5432,published=5432,mode=host \ # публикуем порты базы
    --publish target=5433,published=5433,mode=host \ # публикуем порты балансировщика
    --replicas-max-per-node 1 \ # разрешаем только один конейнер на хосте
    rekgrpth/repmgr # берём готовый образ


На втором хосте запускаем второй сервис также, только вместо docker1 пишем docker2 и вместо repmgr1 пишем repmgr2

Если научить docker резолвить hostname конейеров
так, то можно запустить один сервис сразу на обоих хостах
docker service create     --env GROUP_ID="$(id -g)" \ # задаём идентификатор группы (это не является необходимым и сделано для удобства)
    --env LANG=ru_RU.UTF-8 \ # задаём кодировку (это не является необходимым, но для красоты картины пусть будет)
    --env TZ=Asia/Yekaterinburg \ # задаём зону (это не является необходимым, но для красоты картины пусть будет)
    --env USER_ID="$(id -u)" \ # задаём идентификатор пользователя (это не является необходимым и сделано для удобства)
    --hostname repmgr-{{.Node.Hostname}} \ # задаём хост
    --mode global \ # запускаем по одному контейнеру на каждом хосте
    --mount type=bind,source=/etc/certs,destination=/etc/certs,readonly \ # монтируем папку с сертификатами (это не является необходимым, но для красоты картины пусть будет)
    --mount type=bind,source=/mnt/gfs/repmgr,destination=/var/lib/postgresql/gfs \ # монтируем распределённую файловую систему
    --mount type=volume,source=repmgr,destination=/var/lib/postgresql \ # монтируем раздел
    --name repmgr \ # задаём имя сервиса
    --network name=docker \ # используем сеть
    --publish target=5432,published=5432,mode=host \ # публикуем порты базы
    --publish target=5433,published=5433,mode=host \ # публикуем порты балансировщика
    rekgrpth/repmgr # берём готовый образ


Итак, вход в конейнер

/usr/local/bin/docker_entrypoint.sh
#!/bin/sh

exec 2>&1
set -ex
if [ -n "$GROUP" ] && [ -n "$GROUP_ID" ] && [ "$GROUP_ID" != "$(id -g "$GROUP")" ]; then # если задана группа и задан идентификатор группы и он другой, то
    groupmod --gid "$GROUP_ID" "$GROUP" # заменим идентификатор группы
    chgrp "$GROUP_ID" "$HOME" # и обновим группу у домашней директории
fi
if [ -n "$USER" ] && [ -n "$USER_ID" ] && [ "$USER_ID" != "$(id -u "$USER")" ]; then # если задан пользователь и задан идентификатор пользователя и он другой, то
    usermod --uid "$USER_ID" "$USER" # заменим идентификатор пользователя
    chown "$USER_ID" "$HOME" # и обновим пользователя у домашней директории
fi
exec "$@" # и выполняем команду


Супервизор запускает следующие сервисы

1) /etc/service/ssh
Этот сервис не является необходимым, но требуется для switchover. Супервизор выполняет файл

/etc/service/ssh/run
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
ssh-keygen -A # генерируем ключи хоста
exec /usr/sbin/sshd -D -e # запускаем ssh-сервер


Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)

/etc/service/ssh/log/run
#!/bin/sh

exec 2>&1
exec sed 's|^|ssh: |'



2) /etc/service/postgres
Супервизор выполняет файл

/etc/service/postgres/run
#!/bin/sh

exec 2>&1
install -d -m 0750 -o "$USER" -g "$GROUP" "$PGDATA" # устанавливаем домашнюю директорию базы
install -d -m 1775 -o "$USER" -g "$GROUP" /run/postgresql /var/log/postgresql # устанавливаем директории запуска и логов
rm -f /run/postgresql/postgres.run # удаляем файл блокировки
realpath "$0"
set -ex
chmod 755 supervise
chown "$USER":"$GROUP" supervise/ok supervise/control supervise/status # разрешаем управлять сервисом пользователю
rm -f "$PGDATA/postmaster.pid" # удаляем pid
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" || echo -n "$(hostname)" >"$HOME/gfs/primary" # если мастер не задан, то мастер - текущий хост
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" # удостоверяемся, что теперь мастер задан
chown "$USER":"$GROUP" "$HOME/gfs/primary" # правим права
if [ "$primary" != "$(hostname)" ]; then # если текущий хост не мастер, то
    test -d "$PGDATA/base" || /etc/service/postgres/standby # если нет директории базы - создаём её
    test -f "$PGDATA/standby.signal" || /etc/service/postgres/rejoin # если текущий хост раньше был мастером, то переключаем его на новый мастер
else # иначе (текущий хост - мастер)
    test -d "$PGDATA/base" || /etc/service/postgres/primary # если нет директории базы - создаём её
fi
test -d "$PGDATA/base" # удостоверяемся, что теперь есть директория базы
exec chpst -u "$USER":"$GROUP" -L /run/postgresql/postgres.run postmaster # запускаем базу


Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)

/etc/service/postgres/log/run
#!/bin/sh

exec 2>&1
exec sed 's|^|postgres: |'


Создаём директорию базы не мастера так

/etc/service/postgres/standby
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" /etc/service/repmgr/conf # создаём конфигурацию для repmgr
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" # удостоверяемся, что мастер задан
chpst -u "$USER":"$GROUP" repmgr standby clone --config-file="$HOME/repmgr.conf" --verbose --fast-checkpoint --dbname="host=$primary user=repmgr dbname=repmgr connect_timeout=2" # клонируем базу из мастера


Переключаем старый мастер на новый так

/etc/service/postgres/rejoin
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" # удостоверяемся, что мастер задан
chpst -u "$USER":"$GROUP" pg_ctl --options="-c listen_addresses=''" --wait start # запускаем базу локально (это нужно для pg_rewind)
chpst -u "$USER":"$GROUP" pg_ctl --wait --mode=fast stop # выключаем базу (это нужно для pg_rewind)
chpst -u "$USER":"$GROUP" repmgr node rejoin --config-file="$HOME/repmgr.conf" --verbose --force-rewind --no-wait --dbname="host=$primary user=repmgr dbname=repmgr connect_timeout=2" || mv -f "$PGDATA" "${PGDATA}_$(date "+%F %T")" # переключаем базу на новый мастер (в случае неудачи - сохраняем базу и будем клонировать из мастера)


Создаём директорию базы мастера так

/etc/service/postgres/primary
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
/etc/service/postgres/init # инициализируем базу
/etc/service/repmgr/init # инициализируем repmgr


Инициализируем базу так

/etc/service/postgres/init
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
cd "$PGDATA" && chpst -u "$USER":"$GROUP" initdb # инициализируем базу
chpst -u "$USER":"$GROUP" /etc/service/postgres/conf # дополняем конфигурацию базы
chpst -u "$USER":"$GROUP" /etc/service/postgres/hba # дополняем файл доступов


Дополняем конфигурацию базы так

/etc/service/postgres/conf
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
cat >>"$PGDATA/postgresql.conf" <<EOF

archive_command = '/bin/true' # фейковая архивация
archive_mode = on # но всё-таки включена
datestyle = 'iso, dmy' # задаём стиль даты
listen_addresses = '*' # слушаем всё
max_logical_replication_workers = 0 # отключаем логическую репликацию
max_sync_workers_per_subscription = 0 # отключаем логическую репликацию
ssl_ca_file = '/etc/certs/ca.pem' # задаём корневой сертификат
ssl_cert_file = '/etc/certs/cert.pem' # задаём сертификат
ssl_key_file = '/etc/certs/key.pem' # задаём приватный ключ
ssl = on # включаем ssl
EOF


Дополняем файл доступов так

/etc/service/postgres/hba
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
echo >>"$PGDATA/pg_hba.conf"
ip -o -f inet addr show | awk '/scope global/ {print $4}' | sed -E 's|.\d+/|.0/|' | while read -r net; do # для всех локальных сетей
    echo "host all all $net trust" # разрешаем все соединения
done >>"$PGDATA/pg_hba.conf"



3) /etc/service/repmgr
Супервизор выполняет файл

/etc/service/repmgr/run
#!/bin/sh

exec 2>&1
install -d -m 1775 -o "$USER" -g "$GROUP" /run/postgresql # устанавливаем директории запуска
rm -f /run/postgresql/repmgr.run # удаляем файл блокировки
test -f /run/postgresql/postgres.run || exit $? # удостоверяемся, что запущен сервис базы
realpath "$0"
set -ex
chmod 755 supervise
chown "$USER":"$GROUP" supervise/ok supervise/control supervise/status # разрешаем управлять сервисом пользователю
chpst -u "$USER":"$GROUP" pg_ctl status # удостоверяемся, что запущен база
primary="$(test -f "$HOME/gfs/primary" && cat "$HOME/gfs/primary")" # получаем имя хоста мастера
test -n "$primary" # удостоверяемся, что мастер задан
if [ "$primary" != "$(hostname)" ]; then # если текущий хост не мастер, то
    test -f "$HOME/repmgr.conf" || /etc/service/repmgr/standby # если не задана конфигурация, то задаём её
    chpst -u "$USER":"$GROUP" repmgr standby register --config-file="$HOME/repmgr.conf" --verbose --force # принудительно регистрируемся как не мастер
else # иначе (текущий хост - мастер)
    test -f "$HOME/repmgr.conf" || /etc/service/repmgr/primary # если не задана конфигурация, то задаём её
    test ! -f "$PGDATA/standby.signal" || /etc/service/repmgr/promote # если текущий хост раньше был не мастером, то превращаем его в мастер
    chpst -u "$USER":"$GROUP" repmgr primary register --config-file="$HOME/repmgr.conf" --verbose --force # принудительно регистрируемся как мастер
fi
exec chpst -u "$USER":"$GROUP" -L /run/postgresql/repmgr.run repmgrd --config-file="$HOME/repmgr.conf" --verbose --daemonize=false # запускаем сервис repmgr


Также, супервизор запускает логер для этого сервиса (это не является необходимым, но так удобнее различать логи разных сервисов)

/etc/service/repmgr/log/run
#!/bin/sh

exec 2>&1
exec sed 's|^|repmgr: |'


Задаём конфигурацию не мастера так
/etc/service/repmgr/standby
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" /etc/service/repmgr/conf # задаём конфигурацию


Задаём конфигурацию мастера так

/etc/service/repmgr/primary
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" createuser --superuser repmgr # создаём пользователя
chpst -u "$USER":"$GROUP" createdb repmgr --owner=repmgr # создаём базу
chpst -u "$USER":"$GROUP" /etc/service/repmgr/conf # задаём конфигурацию


Превращаем не мастер в мастер так

/etc/service/repmgr/promote
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" repmgr standby promote --config-file="$HOME/repmgr.conf" --verbose # превращаем не мастер в мастер


Задаём конфигурацию так
/etc/service/repmgr/conf
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
cat >"$HOME/repmgr.conf" <<EOF
conninfo='host=$(hostname) user=repmgr dbname=repmgr connect_timeout=2' # задаём строку подключения к локальной базе
data_directory='$(echo -n "$PGDATA")' # задаём директорию локальной базы
event_notification_command='/etc/service/repmgr/event "%n" "%e" "%s" "%t" "%d"' # задаём команду уведомлений
failover='automatic' # задаём автоматический failover
failover_validation_command='/etc/service/repmgr/valid "%n" "%a"' # задаём команду проверки
follow_command='repmgr standby follow --config-file="$(echo -n "$HOME")/repmgr.conf" --wait --upstream-node-id=%n' # задаём команду следования
node_id=$(hostname | grep -oE '\d+') # задаём номер узла
node_name='$(hostname)' # задаём имя узла
promote_command='repmgr standby promote --config-file="$(echo -n "$HOME")/repmgr.conf"' # задаём команду промотки
repmgrd_service_start_command='sv start repmgr' # задаём команду запуска сервиса
repmgrd_service_stop_command='sv force-stop repmgr' # задаём команду остановки сервиса
service_promote_command='pg_ctl --wait promote' # задаём команду промотки базы
service_reload_command='sv reload postgres' # задаём команду обновления конфигурации базы
service_restart_command='sv force-restart postgres' # задаём команду перезагрузки базы
service_start_command='sv start postgres' # задаём команду запуска базы
service_stop_command='sv force-stop postgres' # задаём команду остановки базы
ssh_options='-q -o ConnectTimeout=10' # задаём опции ssl
standby_disconnect_on_failover=true # отключаемся при failover
use_replication_slots=true # используем слоты репликации
EOF


Инициализируем repmgr так

/etc/service/repmgr/init
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
chpst -u "$USER":"$GROUP" /etc/service/repmgr/hba # дополняем файл доступа базы
chpst -u "$USER":"$GROUP" cat >>"$PGDATA/postgresql.conf" <<EOF # дополняем файл конфигурации базы

hot_standby = on # включаем горячий резерв
max_replication_slots = 10 # задаём максимальное количество слотов репликации
max_wal_senders = 10 # задаём максимальное количество отправщиков
shared_preload_libraries = 'repmgr' # загружаем repmgr
wal_level = 'hot_standby' # задаём уровень горячий резерв
wal_log_hints = on # включаем подсказки для pg_rewind
EOF


Дополняем файл доступа базы так

/etc/service/repmgr/hba
#!/bin/sh

exec 2>&1
realpath "$0"
set -ex
echo >>"$PGDATA/pg_hba.conf"
ip -o -f inet addr show | awk '/scope global/ {print $4}' | sed -E 's|.\d+/|.0/|' | while read -r net; do # для всех локальных сетей
    echo "host replication repmgr $net trust" # разрешаем репликацию для repmgr
    echo "host repmgr repmgr $net trust" # разрешаем repmgr для repmgr
done >>"$PGDATA/pg_hba.conf"
cat >>"$PGDATA/pg_hba.conf" <<EOF

local replication repmgr trust # разрешаем репликацию для repmgr
host replication repmgr 127.0.0.1/32 trust # разрешаем репликацию для repmgr
host replication repmgr ::1/128 trust # разрешаем репликацию для repmgr

local repmgr repmgr trust # разрешаем repmgr для repmgr
host repmgr repmgr 127.0.0.1/32 trust # разрешаем repmgr для repmgr
host repmgr repmgr ::1/128 trust # разрешаем repmgr для repmgr
EOF