La replicación en streaming de PostgreSQL envía el registro de escritura anticipada (WAL) del servidor primario a uno o varios servidores standby, que lo aplican de forma continua y quedan como copia casi exacta en tiempo real. El standby admite consultas de solo lectura (hot standby) y puede promocionarse a primario si el principal falla. En este tutorial configurarás un primario y un standby con PostgreSQL 16 en Ubuntu 24.04, con un slot de replicación para que el primario no borre WAL que el standby todavía necesita.

Requisitos previos

Para seguir esta guía necesitas:

  • Dos servidores con Ubuntu 24.04 LTS, por ejemplo dos VPS de CubePath, idealmente conectados por una red privada. En los ejemplos:
    • Primario: pg1, IP privada 10.0.0.2.
    • Standby: pg2, IP privada 10.0.0.3.
  • Un usuario no root con privilegios sudo en ambos.
  • PostgreSQL 16 instalado en los dos con sudo apt install postgresql. La versión mayor debe ser la misma en ambos servidores.
  • UFW activo en ambos servidores.

Sustituye las IP por las tuyas en todos los comandos. Los datos del standby se sustituirán por completo por los del primario, así que no uses un servidor con datos que quieras conservar.

Paso 1: Configurar el servidor primario

En Ubuntu, la configuración de PostgreSQL 16 está en /etc/postgresql/16/main/. En pg1, abre postgresql.conf:

sudo nano /etc/postgresql/16/main/postgresql.conf

Busca y ajusta estas opciones (descomenta las líneas que empiecen por #):

listen_addresses = 'localhost,10.0.0.2'
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
max_slot_wal_keep_size = 10GB
  • listen_addresses: por defecto PostgreSQL solo escucha en localhost; añade la IP privada para que el standby pueda conectarse.
  • wal_level = replica, max_wal_senders y max_replication_slots: son los valores por defecto en PostgreSQL 16, pero conviene dejarlos explícitos.
  • max_slot_wal_keep_size: límite de WAL que un slot puede retener. Sin él, un standby caído durante días podría llenar el disco del primario.

Paso 2: Crear el rol de replicación y permitir la conexión

Crea un rol con el atributo REPLICATION. El comando te pedirá la contraseña dos veces; usa una contraseña larga y aleatoria:

sudo -u postgres createuser --replication --pwprompt replicador

Ahora autoriza al standby en pg_hba.conf, que controla quién puede conectarse y cómo:

sudo nano /etc/postgresql/16/main/pg_hba.conf

Añade esta línea al final del archivo:

host    replication     replicador      10.0.0.3/32     scram-sha-256

La palabra replication en la columna de base de datos se refiere a las conexiones de replicación física, no a una base de datos con ese nombre. scram-sha-256 es el método de contraseña por defecto de PostgreSQL 16.

Reinicia PostgreSQL (el cambio de listen_addresses requiere reinicio) y abre el puerto solo para el standby:

sudo systemctl restart postgresql
sudo ufw allow from 10.0.0.3 to any port 5432 proto tcp

Comprueba que PostgreSQL escucha en la IP privada:

sudo ss -tlnp | grep 5432
LISTEN 0      200        10.0.0.2:5432      0.0.0.0:*    users:(("postgres",pid=5210,fd=7))
LISTEN 0      200       127.0.0.1:5432      0.0.0.0:*    users:(("postgres",pid=5210,fd=6))

Paso 3: Preparar el standby

En pg2, detén PostgreSQL. El clúster creado al instalar el paquete está vacío y se sustituirá por una copia del primario:

sudo systemctl stop postgresql

Guarda la contraseña del rol de replicación en el archivo .pgpass del usuario postgres, para que el standby pueda reconectarse sin intervención:

sudo -u postgres nano /var/lib/postgresql/.pgpass
10.0.0.2:5432:replication:replicador:your_strong_password
sudo chmod 600 /var/lib/postgresql/.pgpass

El campo replication hace que la línea se aplique a las conexiones de replicación. PostgreSQL ignora el archivo si sus permisos son más abiertos que 600.

Comprueba la conexión desde el standby al primario:

sudo -u postgres psql "host=10.0.0.2 user=replicador dbname=replication replication=true" -c "IDENTIFY_SYSTEM;"
      systemid       | timeline |  xlogpos  | dbname
---------------------+----------+-----------+--------
 7419876543210987654 |        1 | 0/3000148 |
(1 row)

Mueve el directorio de datos vacío a un lado; pg_basebackup necesita un directorio de destino vacío o inexistente:

sudo mv /var/lib/postgresql/16/main /var/lib/postgresql/16/main.orig

Paso 4: Clonar el primario con pg_basebackup

pg_basebackup copia todo el clúster del primario mientras sigue en funcionamiento. Ejecútalo en pg2:

sudo -u postgres pg_basebackup -h 10.0.0.2 -U replicador \
  -D /var/lib/postgresql/16/main \
  -X stream -R -C -S standby_pg2 -P

Qué hace cada opción:

  • -X stream: transmite también el WAL generado durante la copia, así la copia es consistente por sí sola.
  • -R: crea el archivo standby.signal y escribe primary_conninfo en postgresql.auto.conf, de modo que el servidor arrancará como standby conectado al primario.
  • -C -S standby_pg2: crea en el primario un slot de replicación llamado standby_pg2 y lo configura como primary_slot_name. El slot garantiza que el primario conserve el WAL hasta que el standby lo haya recibido.
  • -P: muestra el progreso.
30452/30452 kB (100%), 1/1 tablespace

Comprueba que se creó la configuración de standby:

sudo ls /var/lib/postgresql/16/main/standby.signal
sudo cat /var/lib/postgresql/16/main/postgresql.auto.conf
/var/lib/postgresql/16/main/standby.signal
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=replicador passfile=''/var/lib/postgresql/.pgpass'' channel_binding=prefer host=10.0.0.2 port=5432 sslmode=prefer ...'
primary_slot_name = 'standby_pg2'

Paso 5: Arrancar el standby

En Ubuntu, postgresql.conf vive en /etc, no en el directorio de datos, así que la configuración del standby es la que ya tenía pg2. La opción hot_standby está activada por defecto y permite consultas de solo lectura. Arranca PostgreSQL:

sudo systemctl start postgresql

Revisa el registro del clúster:

sudo tail -n 5 /var/log/postgresql/postgresql-16-main.log
LOG:  entering standby mode
LOG:  redo starts at 0/2000028
LOG:  consistent recovery state reached at 0/2000100
LOG:  database system is ready to accept read-only connections
LOG:  started streaming WAL from primary at 0/3000000 on timeline 1

Cuando compruebes que todo funciona, puedes borrar el directorio antiguo con sudo rm -rf /var/lib/postgresql/16/main.orig.

Paso 6: Verificar la replicación

En el primario (pg1), consulta los standby conectados:

sudo -u postgres psql -x -c "SELECT client_addr, state, sync_state, replay_lag FROM pg_stat_replication;"
-[ RECORD 1 ]---------
client_addr | 10.0.0.3
state       | streaming
sync_state  | async
replay_lag  | 00:00:00.000812

Y el estado del slot:

sudo -u postgres psql -c "SELECT slot_name, active, wal_status FROM pg_replication_slots;"
  slot_name  | active | wal_status
-------------+--------+------------
 standby_pg2 | t      | reserved
(1 row)

En el standby (pg2), confirma que está en modo recuperación:

sudo -u postgres psql -c "SELECT pg_is_in_recovery(), status, sender_host FROM pg_stat_wal_receiver;"
 pg_is_in_recovery |  status   | sender_host
-------------------+-----------+-------------
 t                 | streaming | 10.0.0.2
(1 row)

Ahora prueba con datos reales. En el primario:

sudo -u postgres psql -c "CREATE TABLE prueba_replica (id int PRIMARY KEY, nota text);"
sudo -u postgres psql -c "INSERT INTO prueba_replica VALUES (1, 'hola desde pg1');"

En el standby:

sudo -u postgres psql -c "SELECT * FROM prueba_replica;"
 id |      nota
----+----------------
  1 | hola desde pg1
(1 row)

Un intento de escritura en el standby falla, como debe:

sudo -u postgres psql -c "INSERT INTO prueba_replica VALUES (2, 'desde pg2');"
ERROR:  cannot execute INSERT in a read-only transaction

Borra la tabla en el primario con sudo -u postgres psql -c "DROP TABLE prueba_replica;".

Paso 7: Promocionar el standby a primario

Si el primario falla, promociona el standby para que acepte escrituras. En pg2:

sudo -u postgres psql -c "SELECT pg_promote();"
 pg_promote
------------
 t
(1 row)

pg_is_in_recovery() devolverá ahora f, y el archivo standby.signal desaparece. Apunta tus aplicaciones a 10.0.0.3.

Si alguna vez retiras el standby de forma definitiva sin promocionarlo, borra su slot en el primario para que no retenga WAL:

sudo -u postgres psql -c "SELECT pg_drop_replication_slot('standby_pg2');"

Solución de problemas

FATAL: no pg_hba.conf entry for replication connection from host "10.0.0.3": falta la línea host replication en pg_hba.conf del primario, o la IP no coincide. Corrígela y recarga con sudo systemctl reload postgresql.

FATAL: password authentication failed for user "replicador": la contraseña de .pgpass no coincide o el archivo tiene permisos distintos de 600. Puedes cambiar la contraseña en el primario con sudo -u postgres psql -c "\password replicador".

pg_basebackup: error: directory "/var/lib/postgresql/16/main" exists but is not empty: no se movió el directorio de datos en el paso 3. Comprueba que PostgreSQL está detenido y muévelo.

wal_status pasa a lost en pg_replication_slots: el standby estuvo desconectado y superó max_slot_wal_keep_size, por lo que el primario descartó WAL que necesitaba. Borra el slot, detén el standby y repite los pasos 3 a 5.

El retraso (replay_lag) crece de forma continua: el standby no da abasto aplicando cambios o la red está saturada. Revisa la carga de disco del standby con iostat -x 1 (paquete sysstat) y las consultas largas en él, que pueden retrasar la aplicación del WAL.

Conclusión

Tienes un standby de PostgreSQL 16 que recibe el WAL del primario en streaming, protegido con un slot de replicación con límite de retención y listo para promocionarse. Como siguientes pasos, puedes enviar las consultas de solo lectura al standby, activar la replicación síncrona con synchronous_standby_names si no puedes permitirte perder ninguna transacción, o automatizar la conmutación por error con una herramienta como Patroni.