In this part, we will install and configure Barman to take continuous backups from our PostgreSQL 18 Patroni Cluster using streaming replication.
This setup allows:
Continuous WAL streaming (near real-time)
Point-In-Time Recovery (PITR)
Full filesystem-level base backups
ZFS-based optimized backup storage
Proper failover handling via HAProxy VIP
No archive_command complexity (simpler and more stable for Patroni)
Important Notes
-
PostgreSQL version: 18
-
Cluster uses SSL/TLS encryption
-
Barman uses:
-
Streaming replication (pull) for WALs (WAL is pulled from replication slot to barman server)
-
pg_basebackup for full backups
-
-
ZFS storage used for backup retention & performance
- We do NOT use archive_command
1) Install Barman and PostgreSQL 18 Client (on Barman Server):
sudo su
apt update
apt install -y barman
barman --version
Install PostgreSQL 18 Client
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
gpg --dearmor -o /usr/share/keyrings/postgresql.gpg
echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list
apt update
apt install -y postgresql-client-18
2) Prepare ZFS Backup Storage:
I added another disk to barman server. The following command scans iscsi devices. So you will not have to reboot the server to see the new disk.
for host in /sys/class/scsi_host/*; do
echo "- - -" > $host/scan
done
lsblk
Install ZFS utilities
apt install -y zfsutils-linux
Create ZFS Pool
zpool create -f barman-pool /dev/sdb
Create dataset
zfs create barman-pool/backups
zfs set mountpoint=/var/lib/barman barman-pool/backups
chown -R barman:barman /var/lib/barman
chmod 700 /var/lib/barman
Optimize dataset for PostgreSQL workloads
zfs set compression=lz4 barman-pool/backups
zfs set dedup=on barman-pool/backups
zfs set atime=off barman-pool/backups
zfs set recordsize=128K barman-pool/backups
zfs set primarycache=metadata barman-pool/backups
Check:
df -h | grep barman
zfs get compression,dedup barman-pool/backups
3) Patroni Configuration Updates (on ALL PostgreSQL nodes):
nano /etc/patroni/config.yml
Add WAL settings
parameters:
max_connections: 100
shared_buffers: 256MB
wal_level: replica
archive_mode: off
archive_command: 'true'
max_wal_senders: 5
wal_keep_size: 4GB
Add pg_hba rules required for Barman (TLS)
pg_hba:
- hostssl replication replicator 127.0.0.1/32 md5 ## Patroni/Postgres replication connection to itself
- hostssl replication barman 192.168.204.11/32 md5 #WAL streaming connection (haproxy01 IP)
- hostssl all barman 192.168.204.11/32 md5 #base backup connection (haproxy01 IP)
- hostssl replication barman 192.168.204.12/32 md5 #WAL streaming connection (haproxy02 IP)
- hostssl all barman 192.168.204.12/32 md5 #base backup connection (haproxy02 IP)
##Replication between nodes
- hostssl replication replicator 192.168.204.16/32 md5
- hostssl replication replicator 192.168.204.17/32 md5
- hostssl replication replicator 192.168.204.18/32 md5
- hostssl all all 127.0.0.1/32 md5 ##Localhost
- hostssl all all 0.0.0.0/0 md5 ##All other clients such as PGAdmin

Restart Patroni on each node
systemctl restart patroni
journalctl -u patroni -f
Verify Cluster
patronictl -c /etc/patroni/config.yml list

4) Create Barman PostgreSQL Role (on Leader: postgres01)
psql -U postgres -h 127.0.0.1 -c "CREATE ROLE barman WITH REPLICATION LOGIN PASSWORD 'myrepPASS123';"
psql -U postgres -h 127.0.0.1 -c "ALTER ROLE barman WITH SUPERUSER;"
Verify
psql -U postgres -h 127.0.0.1 -c "\du"

5) Connection Test from Barman Server:
Here note that, we do not connect postgres nodes's IP addresses directl, we use haproxy VIP instead. This will ensure that even if the leader node is changed, WAL streaming will not fail and continue to the leader.
psql "host=192.168.204.10 port=5432 user=barman dbname=postgres sslmode=require"
#then run any postgres command such as
\du

6) Create Barman Configuration File (On Barman server):
nano /etc/barman.d/postgres.conf
Barman replication slot should be created on the current Patroni leader as the best practice.
Replication slots do NOT work on replicas, and creating a slot on a replica will cause Patroni to fail WAL tracking. This subject is being discussed in the official barman documentation as the following statement:
"When using WAL streaming, it is recommended to always stream from the primary node. This is to ensure that all WALs are received by Barman, even in the event of a failover."
On the orher hand, base backup can be taken from the leader or a standby node. For small or mid-sized environments, it is preferred to take base backup from the leader node for the sake of simplicity of configuration and troubleshooting.
Unlike standalone PostgreSQL, a Patroni cluster dynamically manages replication nodes.
Replica nodes may be rebuilt, resynced, or reassigned at any time.
Therefore:
WAL streaming must always use the current leader
Replication slot must exist on the leader
Replica nodes must never be used for streaming_conninfo
[postgres]
description = "PostgreSQL 18 Patroni Cluster Backups"
# Base backup ALWAYS via leader (VIP)
conninfo = host=192.168.204.10 port=5432 user=barman password=myrepPASS123 dbname=postgres
backup_method = postgres
# Continuous WAL streaming via replication slot
streaming_conninfo = host=192.168.204.10 port=5432 user=barman password=myrepPASS123 dbname=postgres
#enables pg_receivewal-based streaming
streaming_archiver = on
archiver = off
slot_name = barman_slot
#this creates barman slot automatically on postgres leader node
create_slot = auto
immediate_checkpoint = true
path_prefix = /usr/lib/postgresql/18/bin/
retention_policy = RECOVERY WINDOW OF 30 DAYS
wal_retention_policy = main
Permission:
chmod 644 /etc/barman.d/postgres.conf
7) Check Barman Status
su - barman
barman receive-wal --create-slot postgres
barman check postgres
#check barman logs
tail -n 50 -f /var/log/barman/barman.log

First Full Backup:
Time to take the first full backup now.
barman backup postgres
barman list-backup postgres
barman check postgres
Barman took the first backup by using pg_basebackup. For consistency, WALs must be recieved that might be created during the backup process.
WAL size is 0B. That means WAL has not been received yet.

Let's run these commands to check if WAL is received.
ls -lh /var/lib/barman/postgres/wals/
OR
barman show-backup postgres <backup-id>
#In my case
barman show-backup postgres 20251202T113138

#to check backup details
barman show-backup postgres 20251202T113138
#to delete backup
barman delete postgres 20251202T113138
#for PITR restore
barman recover postgres 20251202T113138 /tmp/recover
Scheduled Jobs (Barman Cron Tasks):
crontab -e -u barman
# 1) Daily full backup at 02:00
0 2 * * * /usr/bin/barman backup postgres >> /var/log/barman/backup.log 2>&1
# 2) Barman maintenance tasks every 5 minutes
# - archives new WAL files
# - finalizes backups waiting for WALs
# - applies retention policies
# - cleans up expired/failed backups
*/5 * * * * /usr/bin/barman cron >> /var/log/barman/cron.log 2>&1
# (Optional) 3) WAL archive consistency check every hour
#0 * * * * /usr/bin/barman check-wal-archive postgres >> /var/log/barman/check-wal.log 2>&1
Restore Test:
PITR (Point In Time Recovery) lets you restore your PostgreSQL database to a specific moment in the past(second, transaction, or WAL location (LSN)) .
First I want to take a new base backup for this test. This process will hang.
barman backup postgres --wait
Then On Postgres leader node, I will switch WAL file. So barman can recieve the latest WAL segment and the backup command above can be completed.
psql "host=127.0.0.1 sslmode=require user=postgres dbname=postgres" \
-c "SELECT pg_switch_wal();"
On postgres leader node, run these command to create a test table and then drop. Just make sure after creating the table, wait a few minutes before dropping.
psql "host=127.0.0.1 sslmode=require user=postgres dbname=postgres"
#Just for reference time
SELECT now();
#Create test table
CREATE TABLE pitr_final(x int);
INSERT INTO pitr_final SELECT generate_series(1, 50000);
#close wallsegment after INSERT
SELECT pg_switch_wal();
#time before DROP
SELECT now();
#Drop the table
DROP TABLE pitr_final;
#Close WAL that includes DROP
SELECT pg_switch_wal();
#time after DROP
SELECT now();

On barman
barman list-backup postgres
barman show-backup postgres <backup-id>
We should select the base backup which is closest to the dropping table.
Table dropped at 11:22
20251204T100809 - Thu Dec 4 10:08:11 2025 Best option, closest to the table drop
20251204T095858 - Thu Dec 4 09:58:59 2025 Possible but not as close as 20251204T100809 to the dropping
20251204T081454 - Thu Dec 4 08:14:55 2025 Possible but not as close as 20251204T100809 to the dropping
20251202T113138 - Tue Dec 2 11:31:40 2025 Too old

On Barman, we will restore to 11:22:24
barman recover postgres 20251204T100809 \
/tmp/pitr_final \
--target-time "2025-12-04 11:22:24+00"
Copy the restored files to postgres (using a non root user)
cd /tmp
tar czf pitr_final.tar.gz pitr_final
scp pitr_final.tar.gz This email address is being protected from spambots. You need JavaScript enabled to view it. :/tmp/
On postgres01
sudo mv /tmp/pitr_final.tar.gz /var/lib/postgresql/
cd /var/lib/postgresql
sudo tar xzf pitr_final.tar.gz
sudo chown -R postgres:postgres /var/lib/postgresql/pitr_final
#Check
ls -lh /var/lib/postgresql/pitr_final
nano /var/lib/postgresql/pitr_final/pg_hba.conf
#Add these to the top
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
local all postgres trust
Create another instance on port5433
sudo mkdir -p /var/run/pg-restore
sudo chown postgres:postgres /var/run/pg-restore
sudo -u postgres /usr/lib/postgresql/18/bin/postgres \
-D /var/lib/postgresql/pitr_final \
-p 5433 \
-c unix_socket_directories=/var/run/pg-restore
Use another terminall on postgres01 and check
#If this query returns true
SELECT pg_is_in_recovery();
#then we need to run this to resume
SELECT pg_wal_replay_resume();
#check if table and dataexists
\d pitr_final
SELECT * FROM pitr_final LIMIT 10;
#now we can run the actually query which returns 5000 as output
SELECT COUNT(*) FROM pitr_final;
We can see the data. PITR is succesful. We can restore this on Patroni Cluster now.

Patroni Cluster Restore:
On postgres01 we already have the restored files (/var/lib/postgresql/pitr_final)
data_dir: /var/lib/postgresql/data
Let's stop patroni on all nodes
systemctl stop patroni
Delete old data folder
#on postgres01
mv /var/lib/postgresql/data /var/lib/postgresql/data.old
mkdir /var/lib/postgresql/data
cp -a /var/lib/postgresql/pitr_final/* /var/lib/postgresql/data
cd /var/lib/postgresql/data
rm -f standby.signal
rm -f recovery.signal
chown -R postgres:postgres /var/lib/postgresql/data
chmod 700 /var/lib/postgresql/data
#delete cluster record on ETCD
patronictl -c /etc/patroni/config.yml remove postgresql-cluster
systemctl start patroni
#only on postgres02 and postgres03
rm -rf /var/lib/postgresql/data/*
mkdir -p /var/lib/postgresql/data
chown -R postgres:postgres /var/lib/postgresql/data
chmod 700 /var/lib/postgresql/data
systemctl start patroni
patronictl -c /etc/patroni/config.yml list
Cluster is up and running again.

I check the dropped table and verify table is recovered.

It is possible to backup multiple databases on the same barman server. The best practice is to have seperate config file for each server. The section at the top in the config file defines the server. Then we use this name in our barman commands like this:
barman check postgres
barman backup postgres
barman list-backup postgres
