Patroni es un gestor de alta disponibilidad para PostgreSQL. Cada nodo ejecuta un agente de Patroni que arranca PostgreSQL, configura la replicación en streaming y compite por un bloqueo de líder guardado en un almacén distribuido (etcd). Si el primario cae, el bloqueo expira y Patroni promociona automáticamente a la réplica más actualizada, sin intervención manual.

En este tutorial montarás en Ubuntu 24.04 un clúster de tres nodos con PostgreSQL 16, etcd y Patroni, todo desde los repositorios de Ubuntu, y pondrás HAProxy delante para que las aplicaciones siempre se conecten al primario actual. Al final provocarás un failover y un switchover para ver el clúster en acción.

Requisitos previos

  • Tres servidores con Ubuntu 24.04 LTS para la base de datos, por ejemplo VPS de CubePath, con al menos 2 GB de RAM y un usuario no root con sudo.
  • Un cuarto servidor pequeño para HAProxy (también puede ir en el servidor de la aplicación).
  • Red privada entre todos ellos. En esta guía etcd y Patroni hablan por HTTP sin cifrar, así que no expongas sus puertos a Internet.

Valores usados en los ejemplos:

NodoIP privadaFunción
pg110.0.0.11PostgreSQL + Patroni + etcd
pg210.0.0.12PostgreSQL + Patroni + etcd
pg310.0.0.13PostgreSQL + Patroni + etcd
haproxy10.0.0.20HAProxy

Puertos necesarios en la red privada:

PuertoServicio
2379/tcpetcd, clientes
2380/tcpetcd, comunicación entre miembros
5432/tcpPostgreSQL
8008/tcpAPI REST de Patroni

Paso 1: Instalar los paquetes en los nodos de base de datos

Ejecuta en pg1, pg2 y pg3:

sudo apt update
sudo apt install postgresql-16 patroni etcd-server etcd-client

El paquete de PostgreSQL crea y arranca un clúster por defecto (16/main). Patroni necesita gestionar PostgreSQL él mismo y crear el directorio de datos desde cero, así que elimina ese clúster y desactiva el servicio de Ubuntu. En cada nodo:

sudo pg_dropcluster 16 main --stop
sudo systemctl disable --now postgresql

Comprueba que no queda ningún clúster:

pg_lsclusters
Ver Cluster Port Status Owner Data directory Log file

El paquete de etcd también arranca un miembro independiente. Páralo; lo reconfigurarás en el siguiente paso:

sudo systemctl stop etcd

Abre los puertos para la red privada:

sudo ufw allow from 10.0.0.0/24 to any port 2379,2380,5432,8008 proto tcp

Paso 2: Configurar el clúster etcd

etcd guarda el estado del clúster y el bloqueo de líder. Con tres miembros tolera la caída de uno. En Ubuntu el servicio lee su configuración como variables de entorno desde /etc/default/etcd.

En pg1, sustituye el contenido del fichero:

sudo nano /etc/default/etcd
ETCD_NAME="pg1"
ETCD_DATA_DIR="/var/lib/etcd/patroni"
ETCD_LISTEN_PEER_URLS="http://10.0.0.11:2380"
ETCD_LISTEN_CLIENT_URLS="http://10.0.0.11:2379,http://127.0.0.1:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.0.0.11:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://10.0.0.11:2379"
ETCD_INITIAL_CLUSTER="pg1=http://10.0.0.11:2380,pg2=http://10.0.0.12:2380,pg3=http://10.0.0.13:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="patroni-etcd"

Se usa un directorio de datos nuevo (/var/lib/etcd/patroni) para no reutilizar el del miembro independiente que arrancó el paquete, que pertenece a otro clúster.

En pg2 y pg3 usa el mismo fichero cambiando ETCD_NAME y las IP de las cuatro líneas LISTEN/ADVERTISE por las del propio nodo. Por ejemplo, en pg2:

ETCD_NAME="pg2"
ETCD_DATA_DIR="/var/lib/etcd/patroni"
ETCD_LISTEN_PEER_URLS="http://10.0.0.12:2380"
ETCD_LISTEN_CLIENT_URLS="http://10.0.0.12:2379,http://127.0.0.1:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.0.0.12:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://10.0.0.12:2379"
ETCD_INITIAL_CLUSTER="pg1=http://10.0.0.11:2380,pg2=http://10.0.0.12:2380,pg3=http://10.0.0.13:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="patroni-etcd"

Arranca etcd en los tres nodos con poca diferencia de tiempo. El primero esperará a que haya quórum antes de responder:

sudo systemctl enable --now etcd

Comprueba desde cualquier nodo que los tres miembros están sanos:

etcdctl --endpoints=http://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379 endpoint health
http://10.0.0.11:2379 is healthy: successfully committed proposal: took = 2.10ms
http://10.0.0.12:2379 is healthy: successfully committed proposal: took = 2.43ms
http://10.0.0.13:2379 is healthy: successfully committed proposal: took = 2.87ms
etcdctl --endpoints=http://127.0.0.1:2379 member list -w table

La tabla debe mostrar los tres miembros en estado started.

Paso 3: Configurar Patroni

El paquete de Ubuntu incluye un servicio patroni.service que lee /etc/patroni/config.yml. Crea ese fichero en pg1:

sudo nano /etc/patroni/config.yml
scope: pg-cluster
namespace: /service/
name: pg1

restapi:
  listen: 10.0.0.11:8008
  connect_address: 10.0.0.11:8008

etcd3:
  hosts: 10.0.0.11:2379,10.0.0.12:2379,10.0.0.13:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        wal_level: replica
        hot_standby: "on"
        max_wal_senders: 10
        max_replication_slots: 10
        wal_log_hints: "on"
  initdb:
    - encoding: UTF8
    - data-checksums

postgresql:
  listen: 0.0.0.0:5432
  connect_address: 10.0.0.11:5432
  data_dir: /var/lib/postgresql/16/main
  bin_dir: /usr/lib/postgresql/16/bin
  pgpass: /var/lib/postgresql/.pgpass_patroni
  authentication:
    superuser:
      username: postgres
      password: your_superuser_password
    replication:
      username: replicator
      password: your_replication_password
  parameters:
    unix_socket_directories: /var/run/postgresql
  pg_hba:
    - local all all peer
    - host all all 127.0.0.1/32 scram-sha-256
    - host replication replicator 10.0.0.0/24 scram-sha-256
    - host all all 10.0.0.0/24 scram-sha-256

tags:
  nofailover: false
  noloadbalance: false
  clonefrom: false
  nosync: false

Las partes importantes:

  • scope es el nombre del clúster y debe ser igual en los tres nodos. name es único por nodo.
  • etcd3 usa la API v3 de etcd, la única activa por defecto en etcd 3.4.
  • bootstrap.dcs se escribe en etcd solo al crear el clúster. ttl: 30 es el tiempo que tarda en expirar el bloqueo del líder, y por tanto el tiempo máximo aproximado hasta un failover. maximum_lag_on_failover (en bytes) impide promocionar una réplica demasiado retrasada.
  • use_pg_rewind permite que un antiguo primario vuelva como réplica sin copiar todos los datos; necesita wal_log_hints o checksums, y aquí están activos ambos.
  • authentication define el superusuario y el usuario de replicación. Patroni crea replicator al inicializar el clúster. Sustituye ambas contraseñas por valores seguros.
  • pg_hba es el contenido completo de pg_hba.conf, que Patroni escribe en cada nodo. La línea de 127.0.0.1 es necesaria porque Patroni se conecta a su PostgreSQL local por TCP.

En pg2 y pg3 usa el mismo fichero cambiando name, restapi.listen, restapi.connect_address y postgresql.connect_address a los valores del nodo. Por ejemplo, en pg2:

name: pg2

restapi:
  listen: 10.0.0.12:8008
  connect_address: 10.0.0.12:8008
postgresql:
  listen: 0.0.0.0:5432
  connect_address: 10.0.0.12:5432

El fichero contiene contraseñas, así que solo debe poder leerlo el usuario postgres. En cada nodo:

sudo chown postgres:postgres /etc/patroni/config.yml
sudo chmod 600 /etc/patroni/config.yml

Valida la sintaxis antes de arrancar:

sudo -u postgres patroni --validate-config /etc/patroni/config.yml

Si no muestra ningún error, la configuración es válida.

Paso 4: Arrancar Patroni

Arranca primero en pg1. Al no encontrar ningún clúster en etcd, tomará el bloqueo de líder y ejecutará initdb:

sudo systemctl enable --now patroni

Sigue el arranque en el log:

sudo journalctl -u patroni -f

Cuando veas no action. I am (pg1), the leader with the lock, pulsa Ctrl+C y arranca Patroni en pg2 y pg3:

sudo systemctl enable --now patroni

Estos nodos encontrarán un líder, clonarán sus datos con pg_basebackup y arrancarán como réplicas. Comprueba el estado del clúster desde cualquier nodo con patronictl:

sudo -u postgres patronictl -c /etc/patroni/config.yml list
+ Cluster: pg-cluster (7419263518471032371) -+-----------+----+-----------+
| Member | Host      | Role    | State     | TL | Lag in MB |
+--------+-----------+---------+-----------+----+-----------+
| pg1    | 10.0.0.11 | Leader  | running   |  1 |           |
| pg2    | 10.0.0.12 | Replica | streaming |  1 |         0 |
| pg3    | 10.0.0.13 | Replica | streaming |  1 |         0 |
+--------+-----------+---------+-----------+----+-----------+

Un líder running, dos réplicas streaming y lag 0 indican que la replicación funciona.

Paso 5: Probar la replicación

Crea una tabla en el primario (pg1):

sudo -u postgres psql -c "CREATE TABLE ha_test (id serial PRIMARY KEY, creado timestamptz DEFAULT now());"
sudo -u postgres psql -c "INSERT INTO ha_test DEFAULT VALUES;"

Léela en una réplica (pg2):

sudo -u postgres psql -c "SELECT * FROM ha_test;"
 id |            creado
----+-------------------------------
  1 | 2026-09-25 10:42:11.52131+00
(1 row)

Las réplicas son de solo lectura. Un INSERT en pg2 falla con cannot execute INSERT in a read-only transaction.

Paso 6: Poner HAProxy delante

Las aplicaciones no deberían saber qué nodo es el primario. La API REST de Patroni responde en /primary con código 200 solo en el líder, y en /replica con 200 solo en réplicas sanas. HAProxy usa esas respuestas como health check para enviar cada conexión al nodo adecuado.

En el servidor haproxy:

sudo apt update
sudo apt install haproxy

Añade al final de la configuración:

sudo nano /etc/haproxy/haproxy.cfg
listen postgres_primary
    bind *:5000
    mode tcp
    option httpchk GET /primary
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server pg1 10.0.0.11:5432 check port 8008
    server pg2 10.0.0.12:5432 check port 8008
    server pg3 10.0.0.13:5432 check port 8008

listen postgres_replicas
    bind *:5001
    mode tcp
    balance roundrobin
    option httpchk GET /replica
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server pg1 10.0.0.11:5432 check port 8008
    server pg2 10.0.0.12:5432 check port 8008
    server pg3 10.0.0.13:5432 check port 8008
  • El puerto 5000 siempre lleva al primario (lectura y escritura).
  • El puerto 5001 reparte las conexiones entre las réplicas (solo lectura).
  • on-marked-down shutdown-sessions corta las conexiones abiertas contra un nodo que deja de ser primario, para que los clientes reconecten al nuevo.

Valida y recarga:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
Configuration file is valid

Permite el acceso a los puertos 5000 y 5001 solo desde la red de tus aplicaciones:

sudo ufw allow from 10.0.0.0/24 to any port 5000,5001 proto tcp

Comprueba desde el servidor de HAProxy (instala el cliente con sudo apt install postgresql-client) que el puerto 5000 llega al primario y el 5001 a una réplica. pg_is_in_recovery() devuelve f en el primario y t en las réplicas:

psql -h 10.0.0.20 -p 5000 -U postgres -c "SELECT inet_server_addr(), pg_is_in_recovery();"
psql -h 10.0.0.20 -p 5001 -U postgres -c "SELECT inet_server_addr(), pg_is_in_recovery();"
 inet_server_addr | pg_is_in_recovery
------------------+-------------------
 10.0.0.11        | f
(1 row)

 inet_server_addr | pg_is_in_recovery
------------------+-------------------
 10.0.0.12        | t
(1 row)

Te pedirá la contraseña del superusuario. En producción, crea usuarios propios para las aplicaciones en lugar de usar postgres.

Paso 7: Probar el failover automático

Simula la caída del primario parando Patroni en pg1. Patroni detiene PostgreSQL de forma ordenada y libera el bloqueo:

sudo systemctl stop patroni

Desde pg2, observa el clúster. En unos segundos otra réplica pasa a líder:

sudo -u postgres patronictl -c /etc/patroni/config.yml list
+ Cluster: pg-cluster (7419263518471032371) -+-----------+----+-----------+
| Member | Host      | Role    | State     | TL | Lag in MB |
+--------+-----------+---------+-----------+----+-----------+
| pg2    | 10.0.0.12 | Leader  | running   |  2 |           |
| pg3    | 10.0.0.13 | Replica | streaming |  2 |         0 |
+--------+-----------+---------+-----------+----+-----------+

La línea temporal (TL) sube a 2 porque ha habido una promoción. Si repites la consulta por el puerto 5000 de HAProxy, ahora responderá 10.0.0.12.

Vuelve a arrancar Patroni en pg1:

sudo systemctl start patroni

Patroni detecta que ya no es el líder, usa pg_rewind para alinear sus datos con el nuevo primario y se une como réplica. patronictl list mostrará de nuevo tres miembros, con pg1 como Replica.

Si en lugar de parar el servicio el servidor se cae de golpe, el failover tarda algo más: hay que esperar a que expire el bloqueo del líder (ttl, 30 segundos).

Paso 8: Hacer un switchover planificado

Para tareas de mantenimiento en el primario, cambia de líder de forma controlada con switchover, que no pierde transacciones:

sudo -u postgres patronictl -c /etc/patroni/config.yml switchover pg-cluster --leader pg2 --candidate pg1 --force

Comprueba el resultado con patronictl list: pg1 vuelve a ser Leader. Sin --force, patronictl pide confirmación y permite programar el cambio para una hora concreta.

Para reiniciar todos los nodos de forma ordenada (por ejemplo, tras cambiar un parámetro que lo requiera), usa:

sudo -u postgres patronictl -c /etc/patroni/config.yml restart pg-cluster

Los parámetros de PostgreSQL comunes a todo el clúster se cambian con patronictl edit-config, que los guarda en etcd y los aplica en todos los nodos; no edites postgresql.conf a mano, porque Patroni lo sobrescribe.

Solución de problemas

Patroni no arranca y el log dice que el directorio de datos no está vacío: queda un clúster previo en /var/lib/postgresql/16/main. Asegúrate de haber ejecutado pg_dropcluster 16 main --stop en el paso 1.

patronictl list muestra un nodo en estado start failed o sin lag: revisa sudo journalctl -u patroni en ese nodo. Las causas habituales son una línea de pg_hba que no permite la conexión de replicación desde su IP o una contraseña de replicator distinta entre nodos.

Failed to get list of machines from ... en el log de Patroni: Patroni no llega a etcd. Comprueba etcdctl endpoint health, el puerto 2379 y los valores de etcd3.hosts.

etcd no arranca con member ... has already been bootstrapped: el directorio de datos contiene un clúster anterior. Para el servicio, borra /var/lib/etcd/patroni en ese nodo y vuelve a arrancarlo junto a los demás (solo durante la creación inicial; en un clúster en marcha, sustituye el miembro con etcdctl member remove y member add).

Conclusión

Tienes un clúster PostgreSQL 16 de alta disponibilidad en Ubuntu 24.04: Patroni gestiona la replicación y el failover, etcd guarda el estado y el bloqueo de líder, y HAProxy ofrece a las aplicaciones un punto de conexión fijo hacia el primario y otro hacia las réplicas. Como siguientes pasos puedes:

  • Activar TLS en etcd, en la API REST de Patroni y en PostgreSQL si el tráfico sale de una red privada.
  • Configurar copias de seguridad continuas con pgBackRest o WAL-G para poder recuperar a un punto en el tiempo.
  • Duplicar HAProxy con keepalived y una IP virtual para que el balanceador no sea un punto único de fallo.