REX - PostgreSQL HA avec repmgr & Docker

Par Jean-Philippe Prokosch — 18 mars 2026 — ~15 min de lecture
🔗
github.com/Gipipi/repmgr_nodes


Les notes de terrain d'un DBA Senior sur la mise en place d'un cluster 3 nœuds avec repmgr sous Docker Compose : ce que la documentation ne dit pas sur le split-brain, le failover, .pgpass, primary_conninfo et le routage des connexions.

Prérequis : vous savez ce qu'est repmgr, vous avez lu la documentation au moins une fois, et vous avez déjà connu la douleur d'un standby qui refuse de se cloner ou d'un failover qui ne fait silencieusement rien.

Architecture : Primary / Standby / Witness

La topologie 3 nœuds est le minimum viable pour un failover automatique avec repmgr :

[ postgresql1 ]  ⟷  [ postgresql2 ]  ⟷  [ postgresql3 ]
    primary             standby              witness
  lecture/écriture  réplication streaming   quorum seulement

repmgrd tourne sur les 3 nœuds — quorum = 2/3

Le nœud witness est souvent mal compris. Il ne réplique pas les données — c'est une instance PostgreSQL légère dont le seul rôle est de participer aux votes de quorum. Sans lui, un cluster à 2 nœuds ne peut pas distinguer un primary tombé d'une partition réseau, ce qui mène au split-brain. Avec un witness, le failover nécessite l'accord de 2 nœuds sur 3.


Configuration de repmgr : séquence complète

Avant d'aborder ce qui casse, voici la séquence de configuration complète nécessaire pour que repmgr fonctionne correctement. C'est ce qu'implémente le dépôt repmgr_nodes de bout en bout.

1. Prérequis PostgreSQL

repmgr nécessite des paramètres PostgreSQL spécifiques, à mettre en place avant toute commande repmgr :

# postgresql.conf — requis pour repmgr
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
hot_standby = on
wal_log_hints = on          # requis pour pg_rewind (ou activer les checksums)
archive_mode = on           # optionnel mais recommandé
archive_command = 'cd .'    # no-op si pas d'archivage WAL

2. pg_hba.conf — accès réplication et utilisateur repmgr

La connexion de réplication et la connexion aux métadonnées repmgr doivent être autorisées pour tous les nœuds :

# pg_hba.conf — autoriser la réplication entre tous les nœuds
host    replication     repmgr      192.168.1.0/24      scram-sha-256
host    repmgr          repmgr      192.168.1.0/24      scram-sha-256
host    repmgr          repmgr      127.0.0.1/32        scram-sha-256

Dans un setup Docker Compose, le sous-réseau correspond au réseau interne Docker. À adapter selon votre configuration.

3. Création de l'utilisateur et de la base repmgr

À exécuter sur le primary uniquement :

CREATE USER repmgr WITH REPLICATION LOGIN PASSWORD 'votre_mot_de_passe';
CREATE DATABASE repmgr OWNER repmgr;
-- Le superuser est nécessaire pour que repmgr gère les métadonnées du cluster
ALTER USER repmgr SUPERUSER;

4. repmgr.conf — un fichier par nœud

Chaque nœud a son propre repmgr.conf. Les paramètres qui diffèrent par nœud sont node_id, node_name et conninfo :

# repmgr.conf — postgresql1 (primary)
node_id=1
node_name='postgresql1'
conninfo='host=postgresql1 port=5432 user=repmgr dbname=repmgr connect_timeout=2'
data_directory='/var/lib/postgresql/17/main'

# repmgrd — failover automatique
failover=automatic
promote_command='repmgr standby promote -f /etc/repmgr.conf --log-to-file'
follow_command='repmgr standby follow -f /etc/repmgr.conf --log-to-file --upstream-node-id=%n'
reconnect_attempts=4
reconnect_interval=5

# Gestion du service — critique sur Debian/Ubuntu (voir Pièges #1)
service_start_command='pg_ctlcluster 17 main start'
service_stop_command='pg_ctlcluster 17 main stop'
service_restart_command='pg_ctlcluster 17 main restart'
service_reload_command='pg_ctlcluster 17 main reload'

# Logs
log_file='/var/log/repmgr/repmgr.log'
log_level=INFO

Pour postgresql2 (standby) : même fichier avec node_id=2, node_name='postgresql2' et le conninfo correspondant.
Pour postgresql3 (witness) : même fichier avec node_id=3, node_name='postgresql3' et le conninfo correspondant.

5. Enregistrement du primary

repmgr -f /etc/repmgr.conf primary register
# Vérification
repmgr -f /etc/repmgr.conf cluster show

6. Clonage et enregistrement du standby

À exécuter sur postgresql2 :

# Clone depuis le primary — copie intégrale de PGDATA
repmgr -h postgresql1 -U repmgr -d repmgr \
  -f /etc/repmgr.conf standby clone

# Démarrage de PostgreSQL
pg_ctlcluster 17 main start

# Enregistrement du standby
repmgr -f /etc/repmgr.conf standby register

# Patch primary_conninfo immédiatement après (voir Pièges #4)
sed -i "s|primary_conninfo = '\(.*\)'|primary_conninfo = '\1 passfile=/var/lib/postgresql/.pgpass'|" \
  /var/lib/postgresql/17/main/postgresql.auto.conf

7. Enregistrement du witness

Le witness doit s'enregistrer contre le primary courant, jamais contre un hostname en dur — toujours le résoudre dynamiquement :

# Résolution dynamique du primary courant
PRIMARY_HOST=$(psql -h postgresql1 -U repmgr -d repmgr -tAc \
  "SELECT conninfo FROM repmgr.nodes WHERE type='primary' LIMIT 1" \
  2>/dev/null | grep -oP 'host=\K\S+')

repmgr -h "$PRIMARY_HOST" -U repmgr -d repmgr \
  -f /etc/repmgr.conf witness register --force

8. Démarrage de repmgrd sur tous les nœuds

repmgrd est le daemon qui surveille le cluster et déclenche le failover automatique. Il doit tourner sur tous les nœuds, y compris le witness :

repmgrd -f /etc/repmgr.conf --daemonize
# ou via pg_ctlcluster sur Debian :
# repmgrd -f /etc/repmgr.conf -d

9. Vérification du cluster

repmgr -f /etc/repmgr.conf cluster show

Résultat attendu :

 ID | Name        | Role    | Status    | Upstream    | Location | Priority
----+-------------+---------+-----------+-------------+----------+---------
 1  | postgresql1 | primary | * running |             | default  | 100
 2  | postgresql2 | standby |   running | postgresql1 | default  | 100
 3  | postgresql3 | witness | * running | postgresql1 | default  | 0

C'est la situation nominale. La suite décrit ce qui casse dès qu'on s'en écarte.


Split-Brain : le talon d'Achille de repmgr

Le failover automatique de repmgr fonctionne bien quand le primary est vraiment mort. Il est en revanche fragile face aux pannes partielles — partition réseau, charge élevée provoquant des heartbeats manqués, ou un primary vivant mais injoignable depuis le standby. Dans ces cas, repmgrd sur le standby peut déclencher une promotion quand même, aboutissant à deux nœuds se croyant simultanément primary : le split-brain classique.

Le witness atténue ce risque mais ne l'élimine pas totalement. Si le standby peut joindre le witness mais pas le primary, il se promouvra — même si le primary continue de servir des clients sur un autre segment réseau. La vraie protection passe par le contrôle au niveau réseau de quel nœud est accessible par les clients.

Keepalived + VIP flottante : le bon complément

Le complément le plus efficace à repmgr est Keepalived avec une IP Virtuelle (VIP) flottante. Le principe est simple : la VIP est toujours détenue par le primary courant, déterminé indépendamment via un health check pg_is_in_recovery(). Les applications clientes se connectent toujours à la VIP — elles n'ont jamais besoin de savoir quel nœud physique est primary.

Cela résout deux problèmes critiques d'un coup. Premièrement, après un failover ou un switchover, les requêtes clientes sont automatiquement routées vers le nouveau primary sans aucune reconfiguration côté application. Deuxièmement, même en cas de split-brain où deux nœuds se croient momentanément primary, seul celui qui détient la VIP est joignable — les clients ne peuvent tout simplement pas atteindre le mauvais nœud.

Quand repmgr promeut un standby, Keepalived détecte le changement de rôle via son health check et migre la VIP vers le nouveau primary en quelques secondes, de façon transparente pour l'application. Un article dédié couvrira la configuration complète de Keepalived et son intégration avec les hooks de promotion repmgr.


Tester le failover correctement

La plupart des tutoriels testent le failover en faisant docker stop postgresql1 et vérifiant que postgresql2 se promeut. C'est insuffisant. Voici ce qu'il faut réellement valider :

Scénario Ce qu'il faut vérifier Échec courant
Arrêt du container primary Promotion automatique dans reconnect_attempts × reconnect_interval ; migration de la VIP repmgrd pas démarré ou failover=automatic absent
Retour de l'ancien primary Rejoint le cluster en standby via repmgr node rejoin L'ancien primary redémarre en standalone — split-brain
Partition réseau (primary isolé) Witness + standby atteignent le quorum et promeuvent Witness injoignable — pas de quorum, pas de failover
DDL pendant la fenêtre de failover DDL restreint au primary uniquement L'application exécute du DDL sur le standby avant mise à jour de la connection string

Protéger le DDL contre une exécution sur un standby

Dans tout setup où votre application se connecte à une adresse fixe (VIP ou hostname), il faut s'assurer que le DDL ne s'exécute jamais sur un standby. La protection explicite via pg_is_in_recovery() :

-- Garde-fou à placer avant toute opération DDL ou script de migration
DO $$
BEGIN
  IF pg_is_in_recovery() THEN
    RAISE EXCEPTION 'Ce nœud est un standby. Le DDL doit s''exécuter sur le primary uniquement.';
  END IF;
END;
$$;

Le problème du rejoin après failover

Après un failover, l'ancien primary a divergé en termes de WAL. Le redémarrer ne le transforme pas automatiquement en standby — il reviendra en instance standalone. Il faut utiliser pg_rewind via repmgr :

# À exécuter sur l'ancien primary une fois le nouveau primary confirmé
repmgr node rejoin \
  -h postgresql2 \
  -U repmgr \
  -d repmgr \
  --force-rewind \
  --config-files=postgresql.conf,pg_hba.conf
⚠️ Critique : passez toujours --config-files. Sans ça, pg_rewind écrase postgresql.conf avec la version du nouveau primary, effaçant vos paramètres spécifiques au nœud.

--force-rewind nécessite wal_log_hints=on ou les checksums activés — à configurer avant d'en avoir besoin, pas pendant un incident.


Pièges : les vrais problèmes

1. pg_ctlcluster vs pg_ctl — un problème d'OS, pas de Docker

C'est probablement le problème le plus mal attribué dans les setups repmgr sous Docker. Quand les commandes de gestion du service échouent pendant un switchover ou failover, le réflexe est de blâmer Docker. En réalité, c'est presque toujours une spécificité de l'image OS.

Sur Debian/Ubuntu, PostgreSQL est géré via pg_ctlcluster, et non pg_ctl directement. pg_ctlcluster est un wrapper propre à Debian qui connaît la structure des clusters sous /etc/postgresql/, gère pg_lsclusters et les répertoires runtime. Appeler pg_ctl directement sur une image Debian/Ubuntu échoue souvent ou laisse le cluster dans un état incohérent — que ce soit sous Docker ou sur bare metal.

# repmgr.conf — correct pour les images Debian/Ubuntu
service_start_command   = 'pg_ctlcluster 17 main start'
service_stop_command    = 'pg_ctlcluster 17 main stop'
service_restart_command = 'pg_ctlcluster 17 main restart'
service_reload_command  = 'pg_ctlcluster 17 main reload'

# Sur Alpine ou images minimales sans pg_ctlcluster :
# service_start_command = 'pg_ctl -D $PGDATA -l $PGDATA/pg_log/startup.log start'
ℹ️ Dans les containers, systemd ne tourne généralement pas du tout. Utilisez toujours pg_ctlcluster sur les containers Debian/Ubuntu, ou un wrapper fin appelant pg_ctl avec le bon chemin $PGDATA.

2. L'image officielle PostgreSQL Docker est incompatible avec repmgr

L'image officielle postgres sur Docker Hub utilise le processus PostgreSQL comme PID 1 dans le container. Quand repmgr doit arrêter et redémarrer PostgreSQL pendant un switchover ou failover, il tue ce processus — ce que Docker interprète comme une sortie du container, qu'il termine immédiatement.

Il n'existe pas de contournement propre dans l'image officielle. La solution correcte est d'utiliser une image Debian 13 (Trixie) avec PostgreSQL installé depuis le dépôt apt PGDG. Cela vous donne :

  • pg_ctlcluster pour la gestion propre du service
  • Un processus init (tini ou équivalent) en PID 1, pour qu'un redémarrage de PostgreSQL ne tue pas le container
  • Une compatibilité complète avec le cycle stop/start/restart de repmgr
# Dockerfile — Debian 13 Trixie + PGDG PostgreSQL 17
FROM debian:trixie-slim

RUN apt-get update && apt-get install -y curl ca-certificates gnupg2 && \
  curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
    | gpg --dearmor -o /usr/share/keyrings/pgdg.gpg && \
  echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] \
    https://apt.postgresql.org/pub/repos/apt trixie-pgdg main" \
    > /etc/apt/sources.list.d/pgdg.list && \
  apt-get update && apt-get install -y \
    postgresql-17 postgresql-17-repmgr sudo tini && \
  rm -rf /var/lib/apt/lists/*

ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/entrypoint.sh"]
N'utilisez pas l'image officielle postgres de Docker Hub avec repmgr. Tout failover ou switchover déclenchant un redémarrage de PostgreSQL terminera le container.

3. .pgpass — environnements de test uniquement

repmgr écrit dans ~/.pgpass lors du standby clone et du standby register. Si votre entrypoint écrit aussi dans .pgpass, vous aurez des entrées dupliquées ou des écrasements selon l'ordre d'exécution. La correction : une fonction idempotente :

ensure_pgpass_entry() {
  local host="$1" port="$2" db="$3" user="$4" pass="$5"
  local entry="${host}:${port}:${db}:${user}:${pass}"
  local pgpass="${PGPASSFILE:-$HOME/.pgpass}"
  [ -f "$pgpass" ] || { touch "$pgpass"; chmod 600 "$pgpass"; }
  grep -qF "$entry" "$pgpass" || echo "$entry" >> "$pgpass"
}
⚠️ .pgpass est une commodité pour les environnements de test, pas un coffre-fort de credentials. Il n'a aucun mécanisme de rotation — une image ou un volume compromis expose tous les credentials en clair. En production, utilisez un gestionnaire de secrets (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) avec rotation automatisée.

4. primary_conninfo doit inclure passfile

Après repmgr standby register, le primary_conninfo généré dans postgresql.auto.conf ressemble généralement à :

primary_conninfo = 'host=postgresql1 port=5432 user=repmgr dbname=repmgr'

Pas de passfile. La réplication échoue silencieusement au redémarrage. Corrigez-le explicitement après chaque enregistrement :

# Ajouter passfile à primary_conninfo après standby register ou standby clone
sed -i "s|primary_conninfo = '\(.*\)'|primary_conninfo = '\1 passfile=/var/lib/postgresql/.pgpass'|" \
  $PGDATA/postgresql.auto.conf
⚠️ Ceci doit s'exécuter après chaque standby register ou standby clone — ces commandes régénèrent postgresql.auto.conf et écraseront votre modification manuelle. Intégrez-le systématiquement dans votre entrypoint, après l'appel repmgr.

5. Le piège de l'enregistrement du witness

Le nœud witness doit s'enregistrer contre le primary courant, pas contre un hostname en dur. Après un failover, le primary change — un enregistrement en dur échouera silencieusement au redémarrage du container :

# Toujours résoudre le primary courant dynamiquement
PRIMARY_HOST=$(psql -h postgresql1 -U repmgr -d repmgr -tAc \
  "SELECT conninfo FROM repmgr.nodes WHERE type='primary' LIMIT 1" \
  2>/dev/null | grep -oP 'host=\K\S+')

if [ -z "$PRIMARY_HOST" ]; then
  echo "Primary introuvable — abandon de l'enregistrement du witness"
  exit 1
fi

repmgr witness register -h "$PRIMARY_HOST" --force

Le flag --force est nécessaire quand l'entrée du witness existe déjà dans le catalogue (ex. après un redémarrage du container).

6. Persistance de postgresql.conf face aux opérations repmgr

Si vous montez un postgresql.conf personnalisé directement dans $PGDATA via un bind mount, pg_rewind et standby clone l'écraseront. Conservez votre configuration personnalisée en dehors de $PGDATA et incluez-la via include_dir :

# docker-compose.yml
volumes:
  - pgdata_primary:/var/lib/postgresql/data             # PGDATA — laissez repmgr gérer ça
  - ./config/custom.conf:/etc/postgresql/custom.conf:ro # hors PGDATA — survit à pg_rewind
# Dans votre postgresql.conf de base :
include_dir = '/etc/postgresql'   # tout fichier .conf ici est inclus automatiquement

7. pg_stat_wal_receiver a changé en PostgreSQL 18

PostgreSQL 18 a supprimé received_lsn de pg_stat_wal_receiver. Les requêtes de supervision qui en dépendent cassent silencieusement après une mise à jour. Requête compatible PG 17 et 18, à exécuter sur le primary :

SELECT
  application_name,
  pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes,
  write_lsn,
  flush_lsn,
  replay_lsn,
  now() - pg_last_xact_replay_timestamp() AS replay_delay
FROM pg_stat_replication;

8. Idempotence de sudoers

repmgr a besoin d'un accès sudo pour la gestion du service. Le pattern naïf d'ajout crée des doublons à chaque redémarrage du container :

# Mauvais — ajoute une ligne à chaque démarrage du container
echo "postgres ALL=(ALL) NOPASSWD: /usr/bin/pg_ctlcluster" >> /etc/sudoers

# Correct — idempotent
LINE="postgres ALL=(ALL) NOPASSWD: /usr/bin/pg_ctlcluster"
grep -qF "$LINE" /etc/sudoers || echo "$LINE" >> /etc/sudoers

Ce que j'aurais fait différemment

Ajouter Keepalived dès le départ. Le pattern VIP est peu coûteux à mettre en place et comble la lacune la plus dangereuse de repmgr — le split-brain en cas de panne partielle. Le retrofitter plus tard implique de coordonner des changements de connection string côté application et une fenêtre de maintenance.

Activer les checksums dès initdb. Requis pour pg_rewind sans wal_log_hints=on. Les activer après coup nécessite un arrêt de service (avant le mode online de PostgreSQL 17). Il n'y a aucune bonne raison de ne pas les activer d'emblée : initdb --data-checksums.

Remplacer .pgpass par un gestionnaire de secrets avant la mise en production. Concevez le mécanisme d'injection de secrets tôt ; le changer ensuite touche chaque entrypoint et chaque pipeline CI.


Prochain article : Sauvegarder sans solliciter le primary — intégration de Barman avec un standby repmgr. Comment pointer Barman exclusivement sur le nœud standby pour le streaming WAL et les sauvegardes de base, en gardant toute la charge des sauvegardes hors du primary, et comment automatiser les politiques de rétention et les tests de restauration.

Jean-Philippe Prokosch — DBA Senior, 15 ans d'expérience sur SQL Server et PostgreSQL. Disponible pour des missions de conseil en architecture HA, migrations et optimisation des performances.
LinkedIn · GitHub — repmgr_nodes