PostgreSQL offers two replication models: streaming replication and logical replication. What follows covers streaming replication on PostgreSQL 13, targeting an asynchronous primary-to-secondary setup.

The reference environment runs CentOS 8.3 across two virtual machines; the same steps are expected to apply on RHEL. The primary listens on 10.0.3.200 (node1) and the secondary on 10.0.3.201 (node2).

PostgreSQL streaming replication

Installing the server software

After the operating system is in place, install PostgreSQL by following the instructions for your platform. Both nodes need the binaries before any replication configuration starts.

1

2

3

4

5

6

7

8

9

10

11

sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-

redhat-repo-latest.noarch.rpm

sudo dnf -qy module disable postgresql

sudo dnf install -y postgresql13-server

# can be skipped on the 2nd node

sudo /usr/pgsql-13/bin/postgresql-13-setup initdb

sudo systemctl enable postgresql-13

# can be skipped on the 2nd node

sudo systemctl start postgresql-13

Verification on node1 looks like this:

1

2

3

4

5

6

7

8

9

10

[root@node1 ~]# ps axf | grep post

5542 pts/1 S+ 0:00 _ grep --color=auto post

5215 ? Ss 0:00 /usr/pgsql-13/bin/postmaster -D /var/lib/pgsql/13/data/

5217 ? Ss 0:00 _ postgres: logger

5219 ? Ss 0:00 _ postgres: checkpointer

5220 ? Ss 0:00 _ postgres: background writer

5221 ? Ss 0:00 _ postgres: walwriter

5222 ? Ss 0:00 _ postgres: autovacuum launcher

5223 ? Ss 0:00 _ postgres: stats collector

5224 ? Ss 0:00 _ postgres: logical replication launcher

At this point node1 is a fully working instance and node2 is prepared to become the replica.

Preparing the primary

Four changes are required on the primary:

  • Set the bind addresses in postgresql.conf
  • Create a dedicated replication user
  • Permit remote access in pg_hba.conf
  • Restart the server

Binding and authentication

The main configuration file usually lives at /var/lib/pgsql/13/data/postgresql.conf, but PostgreSQL can report its own location if you are unsure:

1

2

3

4

5

6

7

8

9

10

[root@node1 ~]# su - postgres

[postgres@node1 ~]$ psql postgres

psql (13.2)

Type 'help' for help.

postgres=# SHOW config_file;

config_file

----------------------------------------

/var/lib/pgsql/13/data/postgresql.conf

(1 row)

Set the following parameter:

listen_addresses = '*'

By default PostgreSQL binds to localhost only, which blocks remote connections regardless of what pg_hba.conf says. listen_addresses controls the bind addresses of the service and is a prerequisite for any remote access.

Next, create the user that will stream the transaction log:

1

2

postgres=# CREATE USER repuser REPLICATION;

CREATE ROLE

A password may be set as well. The essential point is that the user carries the REPLICATION flag, which removes the need to stream the transaction log as a superuser.

Access control is handled in pg_hba.conf, which decides who may connect from which address. Add the line:

host replication repuser 10.0.3.201/32 trust

This allows repuser from the replica (10.0.3.201) to log in and stream WAL from the primary (10.0.3.200).

Because listen_addresses changed, the primary must be restarted. A change confined to pg_hba.conf needs only a reload:

1

2

3

[postgres@node1 ~]$ exit

logout

[root@node1 ~]# systemctl restart postgresql-13

Creating the replica

The replica process starts from a stopped service and an empty data directory. Stop the service first:

1

[root@node2 ~]# systemctl stop postgresql-13

Then clear the data directory:

1

2

3

4

5

6

[root@node2 ~]# cd /var/lib/pgsql/13/data/

[root@node2 data]# ls

PG_VERSION global pg_dynshmem pg_logical pg_replslot pg_stat pg_tblspc pg_xact postmaster.opts

base log pg_hba.conf pg_multixact pg_serial pg_stat_tmp pg_twophase postgresql.auto.conf

current_logfiles pg_commit_ts pg_ident.conf pg_notify pg_snapshots pg_subtrans pg_wal postgresql.conf

[root@node2 data]# rm -rf *

Skipping initdb during installation makes this step unnecessary, but it is required when converting an existing server into a replica.

1

2

3

4

5

[root@node2 data]# su postgres

bash-4.4$ pwd

/var/lib/pgsql/13/data

bash-4.4$ pg_basebackup -h 10.0.3.200 -U repuser --checkpoint=fast

      -D /var/lib/pgsql/13/data/ -R --slot=some_name -C

pg_basebackup connects to the primary as repuser and copies all data files. Instructing PostgreSQL to checkpoint promptly lets the copy start without delay. The -D flag names the destination directory on the replica, and -R configures replication automatically.

A replication slot was also created. Without one, the primary may recycle WAL the replica has not yet consumed, which causes the replica to fail; the slot guarantees that recycling happens only after the replica has consumed the WAL.

Inspect the result:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

bash-4.4$ ls -l

total 196

-rw-------. 1 postgres postgres 3 Feb 12 09:12 PG_VERSION

-rw-------. 1 postgres postgres 224 Feb 12 09:12 backup_label

-rw-------. 1 postgres postgres 135413 Feb 12 09:12 backup_manifest

drwx------. 5 postgres postgres 41 Feb 12 09:12 base

-rw-------. 1 postgres postgres 30 Feb 12 09:12 current_logfiles

drwx------. 2 postgres postgres 4096 Feb 12 09:12 global

drwx------. 2 postgres postgres 32 Feb 12 09:12 log

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_commit_ts

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_dynshmem

-rw-------. 1 postgres postgres 4598 Feb 12 09:12 pg_hba.conf

-rw-------. 1 postgres postgres 1636 Feb 12 09:12 pg_ident.conf

drwx------. 4 postgres postgres 68 Feb 12 09:12 pg_logical

drwx------. 4 postgres postgres 36 Feb 12 09:12 pg_multixact

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_notify

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_replslot

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_serial

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_snapshots

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_stat

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_stat_tmp

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_subtrans

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_tblspc

drwx------. 2 postgres postgres 6 Feb 12 09:12 pg_twophase

drwx------. 3 postgres postgres 60 Feb 12 09:12 pg_wal

drwx------. 2 postgres postgres 18 Feb 12 09:12 pg_xact

-rw-------. 1 postgres postgres 335 Feb 12 09:12 postgresql.auto.conf

-rw-------. 1 postgres postgres 28014 Feb 12 09:12 postgresql.conf

-rw-------. 1 postgres postgres 0 Feb 12 09:12 standby.signal

Alongside the copied data, pg_basebackup created the standby.signal file, which marks the instance as a replica, and adjusted postgresql.auto.conf with the settings the replica needs to connect back to the primary:

1

2

3

4

5

6

7

bash-4.4$ cat postgresql.auto.conf

# Do not edit this file manually!

# It will be overwritten by the ALTER SYSTEM command.

primary_conninfo = 'user=repuser passfile=''/var/lib/pgsql/.pgpass'' channel_binding=prefer

host=10.0.3.200 port=5432 sslmode=prefer sslcompression=0 ssl_min_protocol_version=TLSv1.2

gssencmode=prefer krbsrvname=postgres target_session_attrs=any'

primary_slot_name = 'some_name'

Starting and verifying the replica

Start the replica with systemctl:

1

2

3

4

5

6

7

8

9

10

11

12

bash-4.4$ exit

exit

[root@node2 data]# systemctl start postgresql-13

[root@node2 data]# ps axf | grep post

36394 pts/1 S+ 0:00 _ grep --color=auto post

36384 ? Ss 0:00 /usr/pgsql-13/bin/postmaster -D /var/lib/pgsql/13/data/

36386 ? Ss 0:00 _ postgres: logger

36387 ? Ss 0:00 _ postgres: startup recovering 000000010000000000000007

36388 ? Ss 0:00 _ postgres: checkpointer

36389 ? Ss 0:00 _ postgres: background writer

36390 ? Ss 0:00 _ postgres: stats collector

36391 ? Ss 0:00 _ postgres: walreceiver streaming 0/7000148

Confirm the processes are running and, in particular, that walreceiver is present — it fetches WAL from the primary, and its absence means the setup has failed. Also confirm the service is enabled.

Monitoring matters once the setup is live; a dedicated tool can handle this professionally. Checking the primary:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

[root@node1 ~]# su - postgres

[postgres@node1 ~]$ psql postgres

psql (13.2)

Type 'help' for help.

postgres=# x

Expanded display is on.

postgres=# SELECT * FROM pg_stat_replication ;

-[ RECORD 1 ]----+------------------------------

pid              | 6102

usesysid         | 16385

usename          | repuser

application_name | walreceiver

client_addr      | 10.0.3.201

client_hostname  |

client_port      | 34002

backend_start    | 2021-02-12 09:27:59.53724-05

backend_xmin     |

state            | streaming

sent_lsn         | 0/7000148

write_lsn        | 0/7000148

flush_lsn        | 0/7000148

replay_lsn       | 0/7000148

write_lag        |

flush_lag        |

replay_lag       |

sync_priority    | 0

sync_state       | async

reply_time       | 2021-02-12 09:29:49.783076-05

A row in pg_stat_replication shows that WAL is flowing from the primary to a secondary. Checking the replica:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

[root@node2 data]# su - postgres

[postgres@node2 ~]$ psql postgres

psql (13.2)

Type 'help' for help.

postgres=# x

Expanded display is on.

postgres=# SELECT * FROM pg_stat_wal_receiver;

-[ RECORD 1 ]---------+--------------------------------------------

pid                   | 36391

status                | streaming

receive_start_lsn     | 0/7000000

receive_start_tli     | 1

written_lsn           | 0/7000148

flushed_lsn           | 0/7000148

received_tli          | 1

last_msg_send_time    | 2021-02-12 09:29:59.683418-05

last_msg_receipt_time | 2021-02-12 09:29:59.674194-05

latest_end_lsn        | 0/7000148

latest_end_time       | 2021-02-12 09:27:59.556631-05

slot_name             | some_name

sender_host           | 10.0.3.200

sender_port           | 5432

conninfo              | user=repuser

                        passfile=/var/lib/pgsql/.pgpass

                        channel_binding=prefer

                        dbname=replication

                        host=10.0.3.200

                        port=5432

                        fallback_application_name=walreceiver

                        sslmode=prefer

                        sslcompression=0

                        ssl_min_protocol_version=TLSv1.2

                        gssencmode=prefer

                        krbsrvname=postgres

                        target_session_attrs=any

A row in pg_stat_wal_receiver confirms that the WAL receiver exists and that data is moving. Note that the primary's firewall must be disabled in this example — the replica connects on port 5432, and an active firewall blocks that port. A production setup would restrict access more precisely. For choosing between synchronous and asynchronous replication, see the documentation on the trade-offs.