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:
| Nodo | IP privada | Función |
|---|---|---|
pg1 | 10.0.0.11 | PostgreSQL + Patroni + etcd |
pg2 | 10.0.0.12 | PostgreSQL + Patroni + etcd |
pg3 | 10.0.0.13 | PostgreSQL + Patroni + etcd |
haproxy | 10.0.0.20 | HAProxy |
Puertos necesarios en la red privada:
| Puerto | Servicio |
|---|---|
| 2379/tcp | etcd, clientes |
| 2380/tcp | etcd, comunicación entre miembros |
| 5432/tcp | PostgreSQL |
| 8008/tcp | API 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:
scopees el nombre del clúster y debe ser igual en los tres nodos.namees único por nodo.etcd3usa la API v3 de etcd, la única activa por defecto en etcd 3.4.bootstrap.dcsse escribe en etcd solo al crear el clúster.ttl: 30es 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_rewindpermite que un antiguo primario vuelva como réplica sin copiar todos los datos; necesitawal_log_hintso checksums, y aquí están activos ambos.authenticationdefine el superusuario y el usuario de replicación. Patroni creareplicatoral inicializar el clúster. Sustituye ambas contraseñas por valores seguros.pg_hbaes el contenido completo depg_hba.conf, que Patroni escribe en cada nodo. La línea de127.0.0.1es 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-sessionscorta 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.
