Continuing with the series of articles on Bareos , we’ll look at how to perform PostgreSQL backups on FreeBSD and Linux.
The article is composed of the following sections:
- Introduction
- FreeBSD/Linux Scenario
- PostgreSQL Installation
- RunScript
- Bpipe
- Postgres Python plugin
- Postgres Python plugin PITR
- Postgres Python plugin PITR Timelines
- Incremental Backup Standby Server
- Debug
Introduction:
Bareos allows us to perform PostgreSQL backups in three different ways:
- RunScript (logical backup):
pg_dumpall
dumps the database and copies the dump, requiring double the storage capacity. - Bpipe (logical backup):
pg_dumpall
with the backup streamed to Bareos, and the restoration can either be to a file or directly back into the database. - Postgres Python plugin (physical backup): Physical backup of the
DATA_DIR/pg_wal
files, allowing for PITR (Point in Time Recovery).
FreeBSD/Linux Scenario:
If we are installing PostgreSQL on FreeBSD, it requires access to sysvipc
. In my case, it’s set up within a jail using
Bastille
.
If the jail does not have access to sysvipc
, we will encounter the following error:
running bootstrap script ... 2024-11-01 07:11:16.834 CEST [23722] FATAL: could not create shared memory segment: Function not implemented
We create the jail and perform the basic system configuration using a custom Bastille template .
bastille create -T BareosClient 14.1-RELEASE 192.168.69.31 nfe0
bastille template BareosClient datadyne.alfaexploit.com/bastille-basicconfiguration
We allow access to sysvipc
per jail. It’s also possible to allow it globally on the host for all jails, but it’s preferable to be more granular:
bastille config BareosClient set sysvmsg=new
bastille config BareosClient set sysvsem=new
bastille config BareosClient set sysvshm=new
bastille stop BareosClient
bastille start BareosClient
NOTE: We should avoid using allow.sysvipc
as it is considered deprecated and grants more permissions than strictly necessary.
On Linux, we will set it up within an LXD container, which does not require access to any particular resource.
lxc launch ubuntu:noble/amd64 sys-bareos-client
PostgreSQL Installation:
We proceed with the installation of the package. It’s preferable to install version 16 of PostgreSQL, as version 17 is only partially compatible with the Bareos Python plugin, allowing only Full
backups.
pkg install postgresql16-server
We enable the service:
sysrc postgresql_enable=yes
We initialize the database:
service postgresql initdb
We start the service:
service postgresql start
apt install postgresql postgresql-contrib
We create a database and a table:
su postgres -c 'psql -c "CREATE DATABASE testkr0m;"'
su postgres -c 'psql -d testkr0m -c "CREATE TABLE employees (id SERIAL PRIMARY KEY, first_name VARCHAR(50), last_name VARCHAR(50), age INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);"'
We insert data and check the results:
su postgres -c "psql -d testkr0m -c \"INSERT INTO employees (first_name, last_name, age) VALUES ('John', 'Doe', 30);\""
su postgres -c "psql -d testkr0m -c \"INSERT INTO employees (first_name, last_name, age) VALUES ('Jane', 'Smith', 25);\""
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-02 10:33:12.5319
2 | Jane | Smith | 25 | 2024-11-02 10:33:12.555151
(2 rows)
RunScript:
The simplest way to perform a backup is with RunScript . You simply need to configure a dump command to run when the job executes, copy the generated file, and delete the dump.
On the Director
We create a FileSet that will back up /etc
, /root
, /home
, and the PostgreSQL configuration files as well as the dump:
vi /usr/local/etc/bareos/bareos-dir.d/fileset/sys-postgres.conf
FileSet {
Name = "sys-postgres"
Include {
Options {
Signature = XXH128
Compression = LZ4
}
# System files:
File = /etc/
File = /root/
File = /home/
# Database config files:
File = /var/db/postgres/data16/postgresql.conf
File = /var/db/postgres/data16/pg_hba.conf
File = /var/db/postgres/data16/pg_ident.conf
# Database dump file:
File = "/var/tmp/postgresql_dump.sql"
}
}
vi /etc/bareos/bareos-dir.d/fileset/sys-postgres.conf
FileSet {
Name = "sys-postgres"
Include {
Options {
Signature = XXH128
Compression = LZ4
}
# System files:
# Linux: PostgreSQL config files postgresql.conf, pg_hba.conf, and pg_ident.conf under /etc
File = /etc/
File = /root/
File = /home/
# Database dump file:
File = "/var/tmp/postgresql_dump.sql"
}
}
We modify the FileSet and add RunScript-Before
and RunScript-After
to the job.
NOTE: When Bareos runs the dump script on FreeBSD, it seems unable to locate the path to the pg_dumpall
binary, so we must specify the full path. On Linux, this is not an issue.
vi /usr/local/etc/bareos/bareos-dir.d/job/CLIENT_NAME-job.conf
Job {
Name = "CLIENT_NAME-job"
Client = "CLIENT_NAME-fd"
JobDefs = "DefaultJob"
Level = Full
FileSet="sys-postgres"
# This creates a dump of our database in the local filesystem on the client
RunScript {
FailJobOnError = Yes
RunsOnClient = Yes
RunsWhen = Before
# FreeBSD absolute path
Command = "su postgres -c '/usr/local/bin/pg_dumpall > /var/tmp/postgresql_dump.sql'"
}
# This deletes the dump in our local filesystem on the client
RunScript {
RunsOnSuccess = Yes
RunsOnClient = Yes
RunsWhen = After
Command = "bash -c 'rm /var/tmp/postgresql_dump.sql'"
}
}
vi /etc/bareos/bareos-dir.d/job/CLIENT_NAME-job.conf
Job {
Name = "CLIENT_NAME-job"
Client = "CLIENT_NAME-fd"
JobDefs = "DefaultJob"
Level = Full
FileSet="sys-postgres"
# This creates a dump of our database in the local filesystem on the client
RunScript {
FailJobOnError = Yes
RunsOnClient = Yes
RunsWhen = Before
# Linux
Command = "su postgres -c 'pg_dumpall > /var/tmp/postgresql_dump.sql'"
}
# This deletes the dump in our local filesystem on the client
RunScript {
RunsOnSuccess = Yes
RunsOnClient = Yes
RunsWhen = After
Command = "bash -c 'rm /var/tmp/postgresql_dump.sql'"
}
}
We restart the Director:
service bareos-dir restart
systemctl restart bareos-dir
We proceed to run the backup from the web interface by specifying the FileSet: Jobs -> Run
It should complete without issues:
We delete the database from the PostgreSQL CLI:
su postgres -c 'psql -c "DROP DATABASE testkr0m;"'
su postgres -c 'psql -c "\l"'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
postgres | postgres | UTF8 | libc | C | C.UTF-8 | | |
template0 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(3 rows)
We proceed with the restore: Restore
It is very important to disable the “Merge all client filesets” option, as otherwise the file tree on the right side will be a view of all combined backups for this client. We only want the files from the specified backup.
The restore job should complete successfully:
And on the server, we should have the backup file:
ls -la /tmp/bareos-restores/var/tmp/postgresql_dump.sql
-rw-r----- 1 postgres wheel 4234 Nov 2 10:34 /tmp/bareos-restores/var/tmp/postgresql_dump.sql
We load the dump into the database:
su postgres -c psql < /tmp/bareos-restores/var/tmp/postgresql_dump.sql
We check the restored data:
su postgres -c 'psql -c "\l"'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
postgres | postgres | UTF8 | libc | C | C.UTF-8 | | |
template0 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
testkr0m | postgres | UTF8 | libc | C | C.UTF-8 | | |
(4 rows)
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-02 10:33:12.5319
2 | Jane | Smith | 25 | 2024-11-02 10:33:12.555151
(2 rows)
Bpipe:
With Bpipe , we can dump the database and stream the dump directly to Bareos, so there is no need to store the backup file locally.
This plugin is installed by default with the file daemon on both FreeBSD and Linux:
file /usr/local/lib/bareos/plugins/bpipe-fd.so
/usr/local/lib/bareos/plugins/bpipe-fd.so: ELF 64-bit LSB shared object, x86-64, version 1 (FreeBSD), dynamically linked, for FreeBSD 14.0 (1400097), stripped
file /usr/lib/bareos/plugins/bpipe-fd.so
/usr/lib/bareos/plugins/bpipe-fd.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=4e5dbae63a927968fe8626446740457a252ad1ee, stripped
On the client:
For Bpipe to work, we need to reconfigure the plugin-related section in the file daemon:
vi /usr/local/etc/bareos/bareos-fd.d/client/myself.conf
Client {
Name = CLIENT_NAME-fd
Maximum Concurrent Jobs = 20
# remove comment from "Plugin Directory" to load plugins from specified directory.
# if "Plugin Names" is defined, only the specified plugins will be loaded,
# otherwise all file daemon plugins (*-fd.so) from the "Plugin Directory".
#
# FreeBSD:
Plugin Directory = "/usr/local/lib/bareos/plugins"
Plugin Names = "bpipe"
}
Restart the file daemon:
service bareos-fd restart
vi /etc/bareos/bareos-fd.d/client/myself.conf
Client {
Name = CLIENT_NAME-fd
Maximum Concurrent Jobs = 20
# remove comment from "Plugin Directory" to load plugins from specified directory.
# if "Plugin Names" is defined, only the specified plugins will be loaded,
# otherwise all file daemon plugins (*-fd.so) from the "Plugin Directory".
#
# Linux:
Plugin Directory = "/usr/lib/bareos/plugins"
Plugin Names = "bpipe"
}
Restart the file daemon:
systemctl restart bareos-fd
Although the backup is streamed, a “virtual” directory is required so Bareos can write/read the backup.
mkdir /POSTGRESQL
On the Director:
Bpipe requires three parameters:
- file: Path to a file that will be used temporarily as a pipe. When restoring, we must specify this file.
- reader: Command to execute to obtain the output that Bareos will read and save in the backup.
- writer: Command to which the restore output will be piped.
We create a FileSet with the Bpipe configuration.
vi /usr/local/etc/bareos/bareos-dir.d/fileset/sys-postgresBpipeDB.conf
FileSet {
Name = "sys-postgresBpipeDB"
Include {
Options {
Signature = XXH128
Compression = LZ4
}
# System files:
File = /etc/
File = /root/
File = /home/
# Database config files:
File = /var/db/postgres/data16/postgresql.conf
File = /var/db/postgres/data16/pg_hba.conf
File = /var/db/postgres/data16/pg_ident.conf
# Bpipe database dump
# FreeBSD absolute path
Plugin = "bpipe:file=/POSTGRESQL/dump.sql:reader=su postgres -c '/usr/local/bin/pg_dumpall --clean':writer=su postgres -c /usr/local/bin/psql"
}
}
vi /etc/bareos/bareos-dir.d/fileset/sys-postgresBpipeDB.conf
FileSet {
Name = "sys-postgresBpipeDB"
Include {
Options {
Signature = XXH128
Compression = LZ4
}
# System files:
# Linux: PostgreSQL config files postgresql.conf, pg_hba.conf and pg_ident.conf under /etc
File = /etc/
File = /root/
File = /home/
# Bpipe database dump
# Linux
Plugin = "bpipe:file=/POSTGRESQL/dump.sql:reader=su postgres -c 'pg_dumpall --clean':writer=su postgres -c psql"
}
}
NOTE: We back up with --clean
. This option adds SQL instructions in the dump to delete databases, roles, and other objects before recreating them.
We update the FileSet in the job, and there’s no need for any RunScript-Before
or RunScript-After
:
vi /usr/local/etc/bareos/bareos-dir.d/job/CLIENT_NAME-job.conf
vi /etc/bareos/bareos-dir.d/job/CLIENT_NAME-job.conf
Job {
Name = "CLIENT_NAME-job"
Client = "CLIENT_NAME-fd"
JobDefs = "DefaultJob"
Level = Full
FileSet="sys-postgresBpipeDB"
}
Restart the Director:
service bareos-dir restart
systemctl restart bareos-dir
We proceed to perform the backup from the web interface by selecting the FileSet: Jobs -> Run
It should complete without issues:
We delete the database from the PostgreSQL CLI:
su postgres -c 'psql -c "DROP DATABASE testkr0m;"'
su postgres -c 'psql -c "\l"'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
postgres | postgres | UTF8 | libc | C | C.UTF-8 | | |
template0 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(3 rows)
We proceed with the restore: Restore
It’s important to disable the “Merge all client filesets” option because, otherwise, the right-side file tree will be a combined view of all the client’s backups. We only want the files from the indicated backup. We select the virtual file /POSTGRESQL/dump.sql
in the right-hand tree.
The restore job should complete successfully:
If we check PostgreSQL, the database should be restored:
su postgres -c 'psql -c "\l"'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
postgres | postgres | UTF8 | libc | C | C.UTF-8 | | |
template0 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C.UTF-8 | | | postgres=CTc/postgres+
| | | | | | | | =c/postgres
testkr0m | postgres | UTF8 | libc | C | C.UTF-8 | | |
(4 rows)
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age |
created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-02 10:33:12.5319
2 | Jane | Smith | 25 | 2024-11-02 10:33:12.555151
(2 rows)
It may also be useful to pipe the restore output to a file instead of directly to the PostgreSQL CLI to save the backup to a file without touching the DB:
writer=sh -c 'cat >/var/tmp/postgres.sql'
On the Director:
We create a new FileSet with another Bpipe configuration.
vi /usr/local/etc/bareos/bareos-dir.d/fileset/sys-postgresBpipeFile.conf
FileSet {
Name = "sys-postgresBpipeFile"
Include {
Options {
Signature = XXH128
Compression = LZ4
}
# System files:
File = /etc/
File = /root/
File = /home/
# Database config files:
File = /var/db/postgres/data16/postgresql.conf
File = /var/db/postgres/data16/pg_hba.conf
File = /var/db/postgres/data16/pg_ident.conf
# Bpipe database dump
# FreeBSD absolute path
Plugin = "bpipe:file=/POSTGRESQL/dump.sql:reader=su postgres -c '/usr/local/bin/pg_dumpall --clean':writer=sh -c 'cat >/var/tmp/postgres.sql'"
}
}
vi /etc/bareos/bareos-dir.d/fileset/sys-postgresBpipeFile.conf
FileSet {
Name = "sys-postgresBpipeFile"
Include {
Options {
Signature = XXH128
Compression = LZ4
}
# System files:
# Linux: PostgreSQL config files postgresql.conf, pg_hba.conf and pg_ident.conf under /etc
File = /etc/
File = /root/
File = /home/
# Bpipe database dump
# Linux
Plugin = "bpipe:file=/POSTGRESQL/dump.sql:reader=su postgres -c 'pg_dumpall --clean':writer=sh -c 'cat >/var/tmp/postgres.sql'"
}
}
We change the FileSet:
vi /usr/local/etc/bareos/bareos-dir.d/job/CLIENT_NAME-job.conf
vi /etc/bareos/bareos-dir.d/job/CLIENT_NAME-job.conf
Job {
Name = "CLIENT_NAME-job"
Client = "CLIENT_NAME-fd"
JobDefs = "DefaultJob"
Level = Full
FileSet="sys-postgresBpipeFile"
}
Restart the Director:
service bareos-dir restart
systemctl restart bareos-dir
We proceed to perform the backup from the web interface by selecting the FileSet, and we need to perform a new backup since the restore uses the parameters from the time of the backup: Jobs -> Run
It should complete without issues:
We delete the database from the PostgreSQL CLI:
su postgres -c 'psql -c "DROP DATABASE testkr0m;"'
su postgres -c 'psql -c "\l"'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
postgres | postgres | UTF8 | libc | C | C.UTF-8 | | |
template0 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C.UTF-8 | | | postgres=CTc/postgres+
| | | | | | | | =c/postgres
(3 rows)
We proceed with the restore: Restore
It’s important to disable the “Merge all client filesets” option because, otherwise, the right-side file tree will be a combined view of all the client’s backups. We only want the files from the indicated backup. We select the virtual file /POSTGRESQL/dump.sql
in the right-hand tree.
The restore job should complete successfully:
But this time, it didn’t restore the backup in the DB. Instead, it generated the file in the directory specified in the sys-postgresBpipeFile FileSet config:
ls -la /var/tmp/postgres.sql
-rw-r----- 1 root wheel 6644 Nov 2 11:04 /var/tmp/postgres.sql
We load the file into the DB:
su postgres -c psql < /var/tmp/postgres.sql
We check the restored data:
su postgres -c 'psql -c "\l"'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
postgres | postgres | UTF8 | libc | C | C.UTF-8 | | |
template0 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C.UTF-8 | | | postgres=CTc/postgres+
| | | | | | | | =c/postgres
testkr0m | postgres | UTF8 | libc | C | C.UTF-8 | | |
(4 rows)
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-02 10:33:12.5319
2 | Jane | Smith | 25 | 2024-11-02 10:33:12.555151
(2 rows)
Be careful, as when restoring, the Bpipe parameters are applied from the time the backup was taken, not the current FileSet parameters (even if the director is restarted) when performing the restore. This information was saved in the catalog when the backup is made.
The FileSet parameters of a backup can be checked from the bconsole in the director:
bconsole
list jobs
+-------+------------------+-----------------+---------------------+----------+------+-------+----------+------------+-----------+
| jobid | name | client | starttime | duration | type | level | jobfiles | jobbytes | jobstatus |
+-------+------------------+-----------------+---------------------+----------+------+-------+----------+------------+-----------+
| 1 | BareosClient-job | BareosClient-fd | 2024-11-02 09:52:24 | 00:00:01 | B | F | 8 | 38,963,186 | T |
| 2 | RestoreFiles | BareosClient-fd | 2024-11-02 09:54:03 | 00:00:02 | R | F | 1 | 4,040,720 | T |
| 3 | BareosClient-job | BareosClient-fd | 2024-11-02 10:23:02 | 00:00:01 | B | F | 797 | 3,239,901 | T |
| 4 | RestoreFiles | BareosClient-fd | 2024-11-02 10:28:12 | 00:00:02 | R | F | 1 | 4,235 | T |
| 5 | BareosClient-job | BareosClient-fd | 2024-11-02 10:34:26 | 00:00:01 | B | F | 797 | 3,239,899 | T |
| 6 | RestoreFiles | BareosClient-fd | 2024-11-02 10:37:20 | 00:00:03 | R | F | 1 | 4,234 | T |
| 7 | BareosClient-job | BareosClient-fd | 2024-11-02 10:47:31 | 00:00:02 | B | F | 794 | 3,221,993 | T |
| 8 | RestoreFiles | BareosClient-fd | 2024-11-02 10:51:33 | 00:00:05 | R | F | 1 | 6,644 | T |
| 9 | BareosClient-job | BareosClient-fd | 2024-11-02 10:59:59 | 00:00:02 | B | F | 794 | 3,221,993 | T |
| 10 | RestoreFiles | BareosClient-fd | 2024-11-02 11:04:28 | 00:00:03 | R | F | 1 | 6,644 | T |
+-------+------------------+-----------------+---------------------+----------+------+-------+----------+------------+-----------+
sqlquery
SELECT Job.JobId, Job.Name, FileSet.FileSet, FileSet.FileSetId FROM Job, FileSet WHERE Job.FileSetId = FileSet.FileSetId AND Job.JobId = XX;
SELECT Job.JobId, Job.Name, FileSet.FileSet, FileSet.FileSetId FROM Job, FileSet WHERE Job.FileSetId = FileSet.FileSetId AND Job.JobId = 9;
+-------+------------------+-----------------------+-----------+
| jobid | name | fileset | filesetid |
+-------+------------------+-----------------------+-----------+
| 9 | BareosClient-job | sys-postgresBpipeFile | 4 |
+-------+------------------+-----------------------+-----------+
Enter SQL query: ENTER
End query mode.
list filesets
| 4 | sys-postgresBpipeFile | L9Ng76h0Mx/Og0gHH8/ZfD | 2024-11-02 10:59:57 | FileSet {
Name = "sys-postgresBpipeFile"
Include {
Options {
Signature = "XXH128"
Compression = "LZ4"
HardLinks = No
AclSupport = Yes
XattrSupport = Yes
}
File = "/etc/"
File = "/root/"
File = "/home/"
Plugin = "bpipe:file=/POSTGRESQL/dump.sql:reader=su postgres -c '/usr/local/bin/pg_dumpall --clean':writer=sh -c 'cat >/var/tmp/postgres.sql'"
}
}
|
If at any time we modify the command indicated in the FileSet, we can overwrite it from the web interface. In this case, I want to restore the dump with a different name, postgres2.sql. However, if we paste the parameters as they are, it will not parse the arguments correctly:
bpipe:file=/POSTGRESQL/dump.sql:reader=su postgres -c 'pg_dumpall --clean':writer=sh -c 'cat >/var/tmp/postgres2.sql'
We will get an error:
Invalid keyword: postgres
However, if we prepare a script, it won’t complain.
On the client:
vi dumpToFile.sh
#!/usr/local/bin/bash
cat >/var/tmp/postgres2.sql
#!/bin/bash
cat >/var/tmp/postgres2.sql
chmod 700 dumpToFile.sh
We can also prepare the script for direct output to the DB:
vi dumpToDB.sh
#!/usr/local/bin/bash
su postgres -c /usr/local/bin/psql
#!/bin/bash
su postgres -c /usr/local/bin/psql
chmod 700 dumpToDB.sh
And in the web interface, we only pass the writer since we’re restoring:
bpipe:file=/POSTGRESQL/dump.sql:writer=/root/dumpToFile.sh
bpipe:file=/POSTGRESQL/dump.sql:writer=/root/dumpToDB.sh
We can see the restored file:
ls -la /var/tmp/postgres2.sql
-rw-r----- 1 root wheel 0 Nov 2 11:26 /var/tmp/postgres2.sql
Bpipe could also be used to back up remote PostgreSQL databases from a server with access to that database:
FileSet {
Name = "postgresqlBpipeRemote"
Include {
Plugin = "bpipe:file=/POSTGRESQL/dump.sql:reader=pg_dumpall -h <hostname> -U <username> -W <password>:writer=psql -h <hostname> -U <username> -W <password>"
Options {
Signature = XXH128
Compression = LZ4
}
}
}
PostgreSQL Python plugin:
The PostgreSQL Python plugin allows for physical backups, providing faster dump and restore processes. However, it requires more storage on the Bareos server and does not support restoring only parts of the database.
The only drawback is that, for now (2024/10/29), it does not support PostgreSQL 17, even though they are working
on it
. In my tests, Full
backups worked without issues, but Incremental
backups failed due to changes in the management of WAL
files. The error shown with version 17 is as follows:
Fatal error: python3-fd-mod: Timeout waiting 60 sec. for wal file 000000120000000000000047 to be archived
Fortunately, the plugin works perfectly with PostgreSQL 16, which is the default version installed on Ubuntu 24.04, and it is also available in binary form on FreeBSD without issues.
Another point to note is that this plugin does not support Differential
backups, apparently because
PostgreSQL itself does not support them
:
Differential backups are not supported! Only Full and Incremental
Additionally, if a major version update occurs, previous backups won’t be compatible with the updated version. This requires resetting the backup history.
The main advantages of this plugin are:
- Faster backup speeds due to being a physical backup.
- It does not need double disk space on the PostgreSQL server.
- Point-In-Time-Restore (PITR), allowing restoration to a specific date and time.
- If using
Incremental
backups, large databases can be backed up since only the new WAL files are copied.
Before starting, we should understand some concepts:
- On every PostgreSQL server, queries are always written to a
WAL
file before they are executed in the DB (whether or not replication or backups are configured); this file is written to theDATA_DIR/pg_wal
directory. - PostgreSQL generates
WAL
files as a result of pending operations in the database. If there is an abrupt shutdown, the operations are read and applied. - These files can be used to perform
Incremental
backups, meaning a base backup is created, followed by applyingWAL
files, allowing large databases to be backed up by only copyingWAL
files instead of the entire database. However, keep in mind that the longer the interval betweenFull
backups, the moreWAL
files will need to be applied during restoration. Incremental
backups are only useful for large databases where only a small portion is modified. For small databases or large databases with many changes, PostgreSQL recommendsFull
backups .- To store the
WAL(DATA_DIR/pg_wal)
files, thearchive_command
must be configured, which simply copies theDATA_DIR/pg_wal
file to another directory before it’s reused, thus maintaining a historical log of operations in the DB. - The
DATA_DIR
is restored on its own, and archivedWAL
files are restored separately, the latter using therestore_command
configured in PostgreSQL. - If we want to restore up to a specific PITR, we need to indicate it with the parameter:
recovery_target_time
= ‘yyyy-mm-dd hh:mm:ss.sss’
These images clarify the difference between DATA_DIR/pg_wal
and archived WALs
.
DATA_DIR/pg_wal | Archived WALs |
---|---|
The backup process will vary depending on the backup type:
Full backup | Incremental Backup |
---|---|
Meanwhile, the
restoration process
is as follows:
You can also see its operation in this video.
For the plugin to work, WAL
archiving must be enabled on the PostgreSQL servers.
mkdir -p /var/lib/pgsql/wal_archive
chown postgres:postgres /var/lib/pgsql/wal_archive
vi /var/db/postgres/data16/postgresql.conf
vi /etc/postgresql/16/main/postgresql.conf
wal_level = replica
archive_mode = on
# If file doesn't exist in destination, copy it:
# Archive commands and libraries should generally be designed to refuse to overwrite any pre-existing archive file.
archive_command = 'test ! -f /var/lib/pgsql/wal_archive/%f && cp %p /var/lib/pgsql/wal_archive/%f'
The archive_command has several parameters:
- %p: The path of the file to archive, which is relative to PostgreSQL’s
DATA_DIR
. - %f: Only the name of the file to archive.
NOTE: If we want to use the %
character in the command, we must escape it by using %%
.
Another important point is that the archive command should only return 0 when it has completed successfully. If it returns 0, PostgreSQL will assume it was archived correctly, and the WAL(DATA_DIR/pg_wal)
file will be deleted or reused. If the command does not return 0, PostgreSQL will assume it was not archived correctly and will keep retrying until successful. This is risky because WAL(DATA_DIR/pg_wal)
files will never be recycled, consuming large amounts of space on the server and potentially disabling PostgreSQL.
If the archiving speed is insufficient and we’re doing Incremental
backups, the margin for data loss in a disaster is greater since only the archived WAL
files are being copied. This archiving delay also means that the WAL(DATA_DIR/pg_wal)
directory will grow.
In such cases, the archive command can be replaced with an
archive_library
written in C
, providing much higher performance than a shell script.
If we want to stop archiving temporarily, we can set archive_command
to empty ‘’, causing WAL
files that should be archived to accumulate in DATA_DIR/pg_wal
until archive_command
is reassigned.
Client:
Install the plugin:
pkg install bareos.com-filedaemon-python-plugins-common bareos.com-filedaemon-python3-plugin py311-dateutils py311-pg8000
The plugin has no package for FreeBSD, but we can download it from the GitHub repository:
wget https://raw.githubusercontent.com/bareos/bareos/602dbc78286fdd823ddf856fe31191c03269be66/core/src/plugins/filed/python/postgresql/bareos-fd-postgresql.py
cp bareos-fd-postgresql.py /usr/local/lib/bareos/plugins/
NOTE: The other file installed on Linux, bareos-fd-postgres.py
, appears to be an older version included for compatibility, which will be removed in Bareos 2.4, so there’s no need to install it on FreeBSD.
apt install bareos-filedaemon-postgresql-python-plugin python3-pg8000
Specify the plugin to use in the FileDaemon configuration:
vi /usr/local/etc/bareos/bareos-fd.d/client/myself.conf
Client {
Name = CLIENT_NAME-fd
Maximum Concurrent Jobs = 20
# remove comment from "Plugin Directory" to load plugins from specified directory.
# if "Plugin Names" is defined, only the specified plugins will be loaded,
# otherwise all filedaemon plugins (*-fd.so) from the "Plugin Directory".
#
# FreeBSD:
Plugin Directory = "/usr/local/lib/bareos/plugins"
Plugin Names = "python3"
}
vi /etc/bareos/bareos-fd.d/client/myself.conf
Client {
Name = CLIENT_NAME-fd
Maximum Concurrent Jobs = 20
# remove comment from "Plugin Directory" to load plugins from specified directory.
# if "Plugin Names" is defined, only the specified plugins will be loaded,
# otherwise all filedaemon plugins (*-fd.so) from the "Plugin Directory".
#
# Linux:
Plugin Directory = /usr/lib/bareos/plugins
Plugin Names = "python3"
}
Restart the FileDaemon:
service bareos-fd restart
systemctl restart bareos-fd
Director:
Create a FileSet named sys-postgresPlugin to back up the /etc
, /root
, /home
directories, the PostgreSQL configuration files, and the database using the PostgreSQL plugin.
vi /usr/local/etc/bareos/bareos-dir.d/fileset/sys-postgresPlugin.conf
FileSet {
Name = "sys-postgresPlugin"
Include {
Options {
Signature = XXH128
Compression = LZ4
}
# System files:
File = /etc/
File = /root/
File = /home/
# Database config files:
File = /var/db/postgres/data16/postgresql.conf
File = /var/db/postgres/data16/pg_hba.conf
File = /var/db/postgres/data16/pg_ident.conf
# PostgreSQL plugin
# FreeBSD: grep unix_socket_directories /var/db/postgres/data16/postgresql.conf
# /tmp
Plugin = "python"
":module_name=bareos-fd-postgresql"
":db_host=/tmp"
":db_user=postgres"
":wal_archive_dir=/var/lib/pgsql/wal_archive/"
":ignore_subdirs=pg_replslot,pg_dynshmem,pg_notify,pg_serial,pg_snapshots,pg_stat_tmp,pg_subtrans,pgsql_tmp"
":switch_wal=true"
":start_fast=true"
":stop_wait_wal_archive=true"
}
}
vi /etc/bareos/bareos-dir.d/fileset/sys-postgresPlugin.conf
FileSet {
Name = "sys-postgresPlugin"
Include {
Options {
Signature = XXH128
Compression = LZ4
}
# System files:
# Linux: PostgreSQL config files postgresql.conf, pg_hba.conf and pg_ident.conf under /etc
File = /etc/
File = /root/
File = /home/
# PostgreSQL plugin
# Linux: grep unix_socket_directories /etc/postgresql/16/main/postgresql.conf
# /var/run/postgresql
Plugin = "python"
":module_name=bareos-fd-postgresql"
":db_host=/run/postgresql/"
":db_user=postgres"
":wal_archive_dir=/var/lib/pgsql/wal_archive/"
":ignore_subdirs=pg_replslot,pg_dynshmem,pg_notify,pg_serial,pg_snapshots,pg_stat_tmp,pg_subtrans,pgsql_tmp"
":switch_wal=true"
":start_fast=true"
":stop_wait_wal_archive=true"
}
}
You can find the list of supported plugin parameters on the official Bareos website .
The ignore_subdirs
parameter can be determined from the
PostgreSQL documentation
.
The start_fast
parameter can be useful, especially while testing:
By default, the backup will start after a checkpoint, which can take some time.
If start_fast_start is true, pg_backup_start will be executed as quickly as possible.
This enforces an immediate checkpoint, which can cause a spike in I/O operations and slow any concurrently executing queries. Default: False
Another option, if the server has low activity, is to configure archive_timeout
to force a WAL
segment change every X period. However, note that these forced WAL
files will occupy the same space as regular WAL
files, so setting a very low value could easily exhaust disk space on the PostgreSQL server.
You can read the recommendation from the official PostgreSQL documentation :
archive_timeout settings of a minute or so are usually reasonable.
It’s important to note that when Bareos performs a backup, it copies both the DATA_DIR
and the archived WALs
. Once the backup is completed, these archived WALs
are no longer necessary, but Bareos does not delete them automatically.
While the PostgreSQL plugin backups only the required files from the WAL archive directory, old files are not removed automatically.
The idea is to create a RunScript-Before
to list existing WALs before the backup and a RunScript-After
to delete the WALs from the list generated by the RunScript-Before
.
Change the FileSet to sys-postgresPlugin
and set up RunScript-Before
and RunScript-After
scripts:
vi /usr/local/etc/bareos/bareos-dir.d/job/CLIENT_NAME-job.conf
vi /etc/bareos/bareos-dir.d/job/CLIENT_NAME-job.conf
Job {
Name = "CLIENT_NAME-job"
Client = "CLIENT_NAME-fd"
JobDefs = "DefaultJob"
Level = Full
FileSet="sys-postgresPlugin"
RunScript {
FailJobOnError = Yes
RunsOnClient = Yes
RunsWhen = Before
Command = "/root/listWALs.sh"
}
RunScript {
RunsOnSuccess = Yes
RunsOnClient = Yes
RunsWhen = After
Command = "/root/clearWALs.sh"
}
}
Restart the Director:
service bareos-dir restart
systemctl restart bareos-dir
Client:
The script to list WALs is as follows:
vi /root/listWALs.sh
#!/usr/local/bin/bash
ls -l /var/lib/pgsql/wal_archive > /tmp/WALLlist
#!/bin/bash
ls -l /var/lib/pgsql/wal_archive > /tmp/WALLlist
The script to clear the WALs is as follows:
vi /root/clearWALs.sh
#!/usr/local/bin/bash
for WALL in `cat /tmp/WALLlist | awk '{print $9}'`; do
rm -f /var/lib/pgsql/wal_archive/$WALL
done;
rm /tmp/WALLlist
#!/bin/bash
for WALL in `cat /tmp/WALLlist | awk '{print $9}'`; do
rm -f /var/lib/pgsql/wal_archive/$WALL
done;
rm /tmp/WALLlist
Set the correct permissions:
chmod 700 listWALs.sh
chmod 700 clearWALs.sh
PostgreSQL allows defining access rules for databases. These are defined in the pg_hba.conf
configuration file:
TYPE DATABASE USER ADDRESS METHOD
local/host DB_NAME USERNAME ADDRESS trust/peer/scram-sha-256...
- Type:
- local: Access for local system users.
- host: Access for remote system users.
- Database:
- DB_NAME: The name of the database the user can access.
- User:
- USERNAME: Username.
- Address:
- ADDRESS: IP address or hostname from where the user will connect if not local.
- Method:
- trust: Access for any local user without authentication (no password required). The only requirement is that the user must connect with an existing PostgreSQL
ROLE
, default is postgres. - peer: Access for any local user without authentication (no password required). However, the system username must match the name of the
ROLE
with which they connect. - scram-sha-256: Access with authentication.
- trust: Access for any local user without authentication (no password required). The only requirement is that the user must connect with an existing PostgreSQL
By default, PostgreSQL generates the ROLE
postgres. Here is an example of a local user with the trust
method, specifying an existing ROLE
:
local all all trust
BareosClient # ~> id
uid=0(root) gid=0(wheel) groups=0(wheel),5(operator)
BareosClient # ~> psql -U postgres
psql (16.4)
Type "help" for help.
postgres=#
Now an example with the peer
method, also with an existing ROLE
:
local all all peer
root@sys-bareos-client:~# id
uid=0(root) gid=0(root) groups=0(root)
root@sys-bareos-client:~# psql -U postgres
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: Peer authentication failed for user "postgres"
If we switch to the postgres user, it defaults to connecting with the ROLE
postgres, so the username and ROLE
match, allowing access to the DB:
root@sys-bareos-client:~# su postgres -c psql
psql (16.4)
Type "help" for help.
postgres=#
In FreeBSD, it defaults to allowing access for any local user using trust
, provided they connect with an existing ROLE
. Since we configured the FileSet in the director with the plugin db_user=postgres
, it will use the root user running Bareos and the ROLE
postgres specified in the plugin.
In Linux, however, only local users are allowed access using peer
, which requires the system username to match the ROLE
, but root does not match postgres.
To make it work, we need to allow root to connect with the ROLE
postgres by modifying the PostgreSQL configuration file as follows:
vi /etc/postgresql/16/main/pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
#local all postgres peer
local all postgres trust
Restart PostgreSQL:
systemctl restart postgresql
We can verify access from the root shell as Bareos will do:
psql -U postgres -h /tmp
psql -U postgres -h /var/run/postgresql
Client:
Set the command to execute for restoring the DB from WAL
files.
vi /var/db/postgres/data16/postgresql.conf
vi /etc/postgresql/16/main/postgresql.conf
restore_command = 'cp /var/lib/pgsql/wal_archive/%f %p'
Restart PostgreSQL:
service postgresql restart
systemctl restart postgresql
Perform a backup from the web interface, specifying the FileSet: Jobs -> Run
It should complete without issues:
Delete the database from the PostgreSQL CLI:
su postgres -c 'psql -c "DROP DATABASE testkr0m;"'
su postgres -c 'psql -c "\l"'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
postgres | postgres | UTF8 | libc | C | C.UTF-8 | | |
template0 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C.UTF-8 | | | postgres=CTc/postgres+
| | | | | | | | =c/postgres
(3 rows)
Proceed with restoration; it’s very important to disable the “Merge all client filesets” option in the Bareos restoration, as otherwise, the file tree on the right will show a combined view of all this client’s backups, and we only want files from the specified backup.
service postgresql stop
rm -rf /var/db/postgres/*
rm -rf /var/lib/pgsql/wal_archive/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/db/postgres
- /var/lib/pgsql/wal_archive/LAST_WAL_FILE+LAST_WAL_FILE.history
Follow the PostgreSQL restoration recommendations :
rm /var/db/postgres/data16/postmaster.pid
rm /var/db/postgres/data16/postmaster.opts
rm /var/db/postgres/data16/pg_internal.init 2>/dev/null
service postgresql start
systemctl stop postgresql
rm -rf /var/lib/postgresql/*
rm -rf /var/lib/pgsql/wal_archive/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/lib/postgresql
- /var/lib/pgsql/wal_archive/LAST_WAL_FILE+LAST_WAL_FILE.history
Follow the PostgreSQL restoration recommendations :
rm /var/lib/postgresql/16/main/postmaster.pid
rm /var/lib/postgresql/16/main/postmaster.opts
rm /var/lib/postgresql/16/main/pg_internal.init 2>/dev/null
systemctl start postgresql
The following table shows the restoration options:
FreeBSD | Linux |
---|---|
When starting PostgreSQL, errors may appear on both FreeBSD and Linux, but these can be ignored:
cp: /var/lib/pgsql/wal_archive/00000002.history: No such file or directory
cp: /var/lib/pgsql/wal_archive/000000010000000000000006: No such file or directory
The official PostgreSQL documentation also mentions this:
A normal recovery will end with a “file not found” message, the exact text of the error message depending upon your choice of restore_command.
You may also see an error message at the start of recovery for a file named something like 00000001.history.
This is also normal and does not indicate a problem in simple recovery situations
Verify the restored data:
su postgres -c 'psql -c "\l"'
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
postgres | postgres | UTF8 | libc | C | C.UTF-8 | | |
template0 | postgres | UTF8 | libc | C | C.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C.UTF-8 | | | postgres=CTc/postgres+
| | | | | | | | =c/postgres
testkr0m | postgres | UTF8 | libc | C | C.UTF-8 | | |
(4 rows)
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+
------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-02 10:33:12.5319
2 | Jane | Smith | 25 | 2024-11-02 10:33:12.555151
(2 rows)
PostgreSQL Python plugin PITR:
First, it’s important to know that PITR is not compatible with “Non-transactional instructions”:
BEGIN, COMMIT, ROLLBACK, CREATE DATABASE, DROP DATABASE, ALTER SYSTEM, CREATE TABLESPACE, DROP TABLESPACE, TRUNCATE TABLE...
If any of these instructions are executed, we’ll need to restore a “normal” backup and won’t be able to use PITR.
To restore a backup using PITR, we need to slightly modify the process. However, first, we must understand how PostgreSQL stores data in DATA_DIR
and how it archives WALs
.
As previously mentioned, operations in PostgreSQL are first executed in the DATA_DIR/pg_wal
file, then in the database, and finally, the DATA_DIR/pg_wal
is archived.
In the timeline of the diagram below, the archived WALs
represent the oldest data, while the DATA_DIR/pg_wal
files are the most recent.
In Temp1
, a backup is created using Bareos, copying DATA_DIR
and the archived WAL files
. The archived WALs
that were backed up are then deleted via RunScript-After
.
These backed-up and deleted WALs
represent operations that the DB performed from Start
to Temp1
-(DATA_DIR/pg_wal)
. The process from Temp1
to Temp2
follows the same pattern.
Now, in Temp2
, someone accidentally deletes critical data, and we want to restore it. The backup was taken at Temp1
, but there is data from Temp1
to Temp2
that we don’t want to lose, and we know that this data is in the archived WALs
+DATA_DIR/pg_wal
files generated from Temp1
to Temp2
.
The restoration steps are:
- Stop the PostgreSQL server.
- Copy the
WAL
files fromDATA_DIR
. - Clear the
DATA_DIR
. - DO NOT delete the
/var/lib/pgsql/wal_archive
files. - Restore the
DATA_DIR
backup taken at Temp1. - Restore the backup of
WAL
files from theDATA_DIR
copied in the second step. - Specify a PITR just before Temp2.
- Set the database to R/W mode.
service postgresql stop
rm -rf /root/pg_wal 2>/dev/null
cp -r /var/db/postgres/data16/pg_wal /root/
rm -rf /var/db/postgres/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/db/postgres
rm -rf /var/db/postgres/data16/pg_wal/*
cp -r /root/pg_wal/* /var/db/postgres/data16/pg_wal
chown -R postgres:postgres /var/db/postgres/data16/pg_wal/*
Specify the restoration time, carefully considering time zones:
vi /var/db/postgres/data16/postgresql.conf
recovery_target_time = 'yyyy-mm-dd hh:mm:ss.sss'
Start the service:
service postgresql start
Check that the restoration was successful. If it was, comment out recovery_target_time
and enable writes on the DB.
vi /var/db/postgres/data16/postgresql.conf
# recovery_target_time = 'yyyy-mm-dd hh:mm:ss.sss'
Enable writes on the DB:
psql -U postgres -c "SELECT pg_wal_replay_resume()"
systemctl stop postgresql
rm -rf /root/pg_wal 2>/dev/null
cp -r /var/lib/postgresql/16/main/pg_wal /root/
rm -rf /var/lib/postgresql/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/lib/postgresql
rm -rf /var/lib/postgresql/16/main/pg_wal/*
cp -r /root/pg_wal/* /var/lib/postgresql/16/main/pg_wal
chown -R postgres:postgres /var/lib/postgresql/16/main/pg_wal/*
Specify the restoration time, carefully considering time zones:
vi /etc/postgresql/16/main/postgresql.conf
recovery_target_time = 'yyyy-mm-dd hh:mm:ss.sss'
Start the service:
systemctl start postgresql
Check that the restoration was successful. If it was, comment out recovery_target_time
and enable writes on the DB.
vi /etc/postgresql/16/main/postgresql.conf
# recovery_target_time = 'yyyy-mm-dd hh:mm:ss.sss'
Enable writes on the DB:
psql -U postgres -c "SELECT pg_wal_replay_resume()"
Here’s a practical example:
- Sat Nov 2 12:23:04 CET 2024: A backup is taken.
- Sat Nov 2 12:36:01 CET 2024: Important data is inserted.
su postgres -c "psql -d testkr0m -c \"INSERT INTO employees (first_name, last_name, age) VALUES ('Joe', 'Macmillan', 40);\""
- Sat Nov 2 12:40:01 CET 2024: All data in the table is accidentally deleted.
su postgres -c "psql -d testkr0m -c \"DELETE FROM employees;\""
Follow the restoration steps, specifying this PITR:
recovery_target_time = '2024-11-02 12:39:00.000'
Verify the restored data:
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-02 10:33:12.5319
2 | Jane | Smith | 25 | 2024-11-02 10:33:12.555151
3 | Joe | Macmillan | 40 | 2024-11-02 12:36:01.579415
(3 rows)
PostgreSQL Python plugin PITR Timelines:
When performing PITR restores, a
Timeline
concept is introduced. This represents an alternate timeline after a PITR restore. PostgreSQL allows jumping between Timelines
as long as the WAL
files for each one are available. You only need to specify the restore date and the Timeline
that contains that point.
Let’s go through an example to clarify this concept, imagining the following scenario where we have these records:
su postgres -c "psql -d testkr0m -c \"INSERT INTO employees (first_name, last_name, age) VALUES ('John', 'Doe', 30);\""
su postgres -c "psql -d testkr0m -c \"INSERT INTO employees (first_name, last_name, age) VALUES ('Jane', 'Smith', 25);\""
We take a backup at Temp1: Mon Nov 4 09:03:00 CET 2024
Continuing with INSERTs
: Mon Nov 4 09:05:41 CET 2024
su postgres -c "psql -d testkr0m -c \"INSERT INTO employees (first_name, last_name, age) VALUES ('Joe', 'Macmillan', 40);\""
More INSERTs
: Mon Nov 4 09:07:43 CET 2024
su postgres -c "psql -d testkr0m -c \"INSERT INTO employees (first_name, last_name, age) VALUES ('Cameron', 'Howe', 23);\""
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-04 09:02:34.703267
2 | Jane | Smith | 25 | 2024-11-04 09:02:34.722347
3 | Joe | Macmillan | 40 | 2024-11-04 09:05:41.786131
4 | Cameron | Howe | 23 | 2024-11-04 09:07:43.140263
(4 rows)
Until someone accidentally deletes all the table data at Temp2: Mon Nov 4 09:11:45 CET 2024
su postgres -c "psql -d testkr0m -c \"DELETE FROM employees;\""
We restore to PITR point: 09:05:42-TL1, creating Timeline2
:
Stop the service:
service postgresql stop
Backup the DATA_DIR/pg_wal
from Timeline1
:
rm -rf /root/pg_wal_TL1 2>/dev/null
cp -r /var/db/postgres/data16/pg_wal /root/pg_wal_TL1
Clear the DATA_DIR
directory:
rm -rf /var/db/postgres/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/db/postgres
Follow the PostgreSQL restoration instructions :
rm /var/db/postgres/data16/postmaster.pid
rm /var/db/postgres/data16/postmaster.opts
rm /var/db/postgres/data16/pg_internal.init 2>/dev/null
Restore the files from DATA_DIR/pg_wal
from Timeline1
:
rm -rf /var/db/postgres/data16/pg_wal/*
cp -r /root/pg_wal_TL1/* /var/db/postgres/data16/pg_wal
chown -R postgres:postgres /var/db/postgres/data16/pg_wal/*
Specify the point to restore:
vi /var/db/postgres/data16/postgresql.conf
recovery_target_time = '2024-11-04 09:05:42.000'
NOTE: When only one Timeline
exists, it’s enough to specify just the date and time.
Restart the service:
service postgresql start
Verify successful restoration, then comment out recovery_target_time and enable DB writes.
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-04 09:02:34.703267
2 | Jane | Smith | 25 | 2024-11-04 09:02:34.722347
3 | Joe | Macmillan | 40 | 2024-11-04 09:05:41.786131
(3 rows)
vi /var/db/postgres/data16/postgresql.conf
#recovery_target_time = '2024-11-04 09:05:42.000'
psql -U postgres -c "SELECT pg_wal_replay_resume()"
Check the current Timeline
:
psql -U postgres -c "SELECT timeline_id FROM pg_control_checkpoint()"
timeline_id
-------------
2
(1 row)
Stop the service:
systemctl stop postgresql
Backup the DATA_DIR/pg_wal
from Timeline1
:
rm -rf /root/pg_wal_TL1 2>/dev/null
cp -r /var/lib/postgresql/16/main/pg_wal /root/pg_wal_TL1
Clear the DATA_DIR
directory:
rm -rf /var/lib/postgresql/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/lib/postgresql
Follow the PostgreSQL restoration instructions :
rm /var/lib/postgresql/16/main/postmaster.pid
rm /var/lib/postgresql/16/main/postmaster.opts
rm /var/lib/postgresql/16/main/pg_internal.init 2>/dev/null
Restore the files from DATA_DIR/pg_wal
from Timeline1
:
rm -rf /var/lib/postgresql/16/main/pg_wal/*
cp -r /root/pg_wal_TL1/* /var/lib/postgresql/16/main/pg_wal
chown -R postgres:postgres /var/lib/postgresql/16/main/pg_wal/*
Specify the point to restore:
vi /etc/postgresql/16/main/postgresql.conf
recovery_target_time = '2024-11-04 09:05:42.000'
NOTE: When only one Timeline
exists, it’s enough to specify just the date and time.
Restart the service:
systemctl start postgresql
Verify successful restoration, then comment out recovery_target_time and enable DB writes.
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-04 09:02:34.703267
2 | Jane | Smith | 25 | 2024-11-04 09:02:34.722347
3 | Joe | Macmillan | 40 | 2024-11-04 09:05:41.786131
(3 rows)
vi /etc/postgresql/16/main/postgresql.conf
#recovery_target_time = '2024-11-04 09:05:42.000'
psql -U postgres -c "SELECT pg_wal_replay_resume()"
Check the current Timeline
:
psql -U postgres -c "SELECT timeline_id FROM pg_control_checkpoint()"
timeline_id
-------------
2
(1 row)
We continue making INSERTs
: Mon Nov 4 09:15:52 CET 2024
su postgres -c "psql -d testkr0m -c \"INSERT INTO employees (first_name, last_name, age) VALUES ('Gordon', 'Clark', 39);\""
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-04 09:02:34.703267
2 | Jane | Smith | 25 | 2024-11-04 09:02:34.722347
3 | Joe | Macmillan | 40 | 2024-11-04 09:05:41.786131
36 | Gordon | Clark | 39 | 2024-11-04 09:15:51.910027
(4 rows)
But we realize that Cameron is missing, so we decide to return to PITR2: 09:07:44-TL1 from where Timeline3
originates.
Stop the service:
service postgresql stop
Backup DATA_DIR/pg_wal
from Timeline2
:
rm -rf /root/pg_wal_TL2 2>/dev/null
cp -r /var/db/postgres/data16/pg_wal /root/pg_wal_TL2
Clean up the DATA_DIR
directory:
rm -rf /var/db/postgres/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/db/postgres
Follow the PostgreSQL restoration guidelines :
rm /var/db/postgres/data16/postmaster.pid
rm /var/db/postgres/data16/postmaster.opts
rm /var/db/postgres/data16/pg_internal.init 2>/dev/null
Restore the DATA_DIR/pg_wal
files from Timeline1
:
rm -rf /var/db/postgres/data16/pg_wal/*
cp -r /root/pg_wal_TL1/* /var/db/postgres/data16/pg_wal
chown -R postgres:postgres /var/db/postgres/data16/pg_wal/*
Specify the point to restore to:
vi /var/db/postgres/data16/postgresql.conf
recovery_target_timeline = '1'
recovery_target_time = '2024-11-04 09:07:44.000'
Start the service:
service postgresql start
Verify restoration, then comment out recovery_target_time
and enable writes on the DB.
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-04 09:02:34.703267
2 | Jane | Smith | 25 | 2024-11-04 09:02:34.722347
3 | Joe | Macmillan | 40 | 2024-11-04 09:05:41.786131
4 | Cameron | Howe | 23 | 2024-11-04 09:07:43.140263
(4 rows)
vi /var/db/postgres/data16/postgresql.conf
#recovery_target_timeline = '1'
#recovery_target_time = '2024-11-04 09:07:44.000'
psql -U postgres -c "SELECT pg_wal_replay_resume()"
Check the current Timeline
:
psql -U postgres -c "SELECT timeline_id FROM pg_control_checkpoint()"
timeline_id
-------------
3
(1 row)
Stop the service:
systemctl stop postgresql
Backup DATA_DIR/pg_wal
from Timeline2
:
rm -rf /root/pg_wal_TL2 2>/dev/null
cp -r /var/lib/postgresql/16/main/pg_wal /root/pg_wal_TL2
Clean up the DATA_DIR
directory:
rm -rf /var/lib/postgresql/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/lib/postgresql
Follow the PostgreSQL restoration guidelines :
rm /var/lib/postgresql/16/main/postmaster.pid
rm /var/lib/postgresql/16/main/postmaster.opts
rm /var/lib/postgresql/16/main/pg_internal.init 2>/dev/null
Restore the DATA_DIR/pg_wal
files from Timeline1
:
rm -rf /var/lib/postgresql/16/main/pg_wal/*
cp -r /root/pg_wal_TL1/* /var/lib/postgresql/16/main/pg_wal
chown -R postgres:postgres /var/lib/postgresql/16/main/pg_wal/*
Specify the point to restore to:
vi /etc/postgresql/16/main/postgresql.conf
recovery_target_timeline = '1'
recovery_target_time = '2024-11-04 09:07:44.000'
Start the service:
systemctl start postgresql
Verify restoration, then comment out recovery_target_time
and enable writes on the DB.
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-04 09:02:34.703267
2 | Jane | Smith | 25 | 2024-11-04 09:02:34.722347
3 | Joe | Macmillan | 40 | 2024-11-04 09:05:41.786131
4 | Cameron | Howe | 23 | 2024-11-04 09:07:43.140263
(4 rows)
vi /etc/postgresql/16/main/postgresql.conf
#recovery_target_timeline = '1'
#recovery_target_time = '2024-11-04 09:07:44.000'
psql -U postgres -c "SELECT pg_wal_replay_resume()"
Check the current Timeline
:
psql -U postgres -c "SELECT timeline_id FROM pg_control_checkpoint()"
timeline_id
-------------
3
(1 row)
Now we see that we prefer to continue with Timeline2
, PITR3: Final-TL2.
Stop the service:
service postgresql stop
Backup DATA_DIR/pg_wal
from Timeline3
:
rm -rf /root/pg_wal_TL3 2>/dev/null
cp -r /var/db/postgres/data16/pg_wal /root/pg_wal_TL3
Clean up the DATA_DIR
directory:
rm -rf /var/db/postgres/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/db/postgres
Follow the PostgreSQL restoration guidelines :
rm /var/db/postgres/data16/postmaster.pid
rm /var/db/postgres/data16/postmaster.opts
rm /var/db/postgres/data16/pg_internal.init 2>/dev/null
Restore the DATA_DIR/pg_wal
files from Timeline2
:
rm -rf /var/db/postgres/data16/pg_wal/*
cp -r /root/pg_wal_TL2/* /var/db/postgres/data16/pg_wal
chown -R postgres:postgres /var/db/postgres/data16/pg_wal/*
If we want to go to the end of a Timeline
, as in this case, there’s no need to specify a date, just the Timeline
. Otherwise, errors like these might occur:
2024-11-04 08:46:11.187 CET [50052] LOG: last completed transaction was at log time 2024-11-04 09:15:51.910801+01
2024-11-04 08:46:11.187 CET [50052] FATAL: recovery ended before configured recovery target was reached
vi /var/db/postgres/data16/postgresql.conf
recovery_target_timeline = '2'
Restart the service:
service postgresql start
Verify restoration, then comment out recovery_target_time
.
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-04 09:02:34.703267
2 | Jane | Smith | 25 | 2024-11-04 09:02:34.722347
3 | Joe | Macmillan | 40 | 2024-11-04 09:05:41.786131
36 | Gordon | Clark | 39 | 2024-11-04 09:15:51.910027
(4 rows)
vi /var/db/postgres/data16/postgresql.conf
#recovery_target_timeline = '2'
NOTE: There’s no need to run “SELECT pg_wal_replay_resume()” when restoring to the end of a Timeline
.
Stop the service:
systemctl stop postgresql
Backup DATA_DIR/pg_wal
from Timeline3
:
rm -rf /root/pg_wal_TL3 2>/dev/null
cp -r /var/lib/postgresql/16/main/pg_wal /root/pg_wal_TL3
Clean up the DATA_DIR
directory:
rm -rf /var/lib/postgresql/*
Restore from Bareos: Restore
- Merge all client filesets: No
- Restore location on client: /
- /var/lib/postgresql
Follow the PostgreSQL restoration guidelines :
rm /var/lib/postgresql/16/main/postmaster.pid
rm /var/lib/postgresql/16/main/postmaster.opts
rm /var/lib/postgresql/16/main/pg_internal.init 2>/dev/null
Restore the DATA_DIR/pg_wal
files from Timeline2
:
rm -rf /var/lib/postgresql/16/main/pg_wal/*
cp -r /root/pg_wal_TL2/* /var/lib/postgresql/16/main/pg_wal
chown -R postgres:postgres /var/lib/postgresql/16/main/pg_wal/*
If we want to go to the end of a Timeline
, as in this case, there’s no need to specify a date, just the Timeline
. Otherwise, errors like these might occur:
2024-11-04 08:46:11.187 CET [50052] LOG: last completed transaction was at log time 2024-11-04 09:15:51.910801+01
2024-11-04 08:46:11.187 CET [50052] FATAL: recovery ended before configured recovery target was reached
vi /etc/postgresql/16/main/postgresql.conf
recovery_target_timeline = '2'
Restart the service:
systemctl start postgresql
Verify restoration, then comment out recovery_target_time
.
su postgres -c 'psql -d testkr0m -c "SELECT * FROM employees;"'
id | first_name | last_name | age | created_at
----+------------+-----------+-----+----------------------------
1 | John | Doe | 30 | 2024-11-04 09:02:34.703267
2 | Jane | Smith | 25 | 2024-11-04 09:02:34.722347
3 | Joe | Macmillan | 40 | 2024-11-04 09:05:41.786131
36 | Gordon | Clark | 39 | 2024-11-04 09:15:51.910027
(4 rows)
vi /etc/postgresql/16/main/postgresql.conf
#recovery_target_timeline = '2'
NOTE: There’s no need to run “SELECT pg_wal_replay_resume()” when restoring to the end of a Timeline
.
Incremental Backup on Standby Server:
In the
PostgreSQL documentation
, we can read that for Incremental
backups to work, the current backup must be taken after a checkpoint following the previous backup. This always happens on a primary
since each backup triggers a checkpoint.
However, on a standby
with low activity, a new checkpoint might not have been generated since the last backup, causing the Incremental
backup to fail.
An incremental backup is only possible if replay would begin from a later checkpoint than for the previous backup upon which it depends.
If you take the incremental backup on the primary, this condition is always satisfied, because each backup triggers a new checkpoint.
On a standby, replay begins from the most recent restartpoint. Therefore, an incremental backup of a standby server can fail if there has been
very little activity since the previous backup, since no new restartpoint might have been created.
Bareos will display errors like:
BareosClient2-fd JobId 80: Warning: python3-fd-mod: Could not get current LSN, last LSN was: 0/8000498: {'S': 'ERROR', 'V': 'ERROR', 'C': '55000',
'M': 'recovery is in progress', 'H': 'WAL control functions cannot be executed during recovery.', 'F': 'xlogfuncs.c', 'L': '287', 'R':
'pg_current_wal_lsn'}
This issue only affects backup modes that use WAL
file archiving:
- Postgres Python plugin.
- Postgres Python plugin PITR.
- Postgres Python plugin PITR Timelines.
We have two options:
- Only do
Full
backups on either theprimary
orstandby
. - Use two Bareos
schedules
: one forFull
andIncremental
backups and one only forFull
. Depending on whether the server isprimary
orstandby
, configure the job to use oneschedule
or the other. However, this requires reconfiguring the backup system if a role change occurs.
Debug:
For Bareos debugging, see the Bareos article, Debug section .
Configuration File:
Sometimes, changes are not reflected in the database simply because we are not modifying the correct file. Use SHOW config_file
to view the active configuration file.
su postgres -c 'psql -d testkr0m -c "SHOW config_file;"'
config_file
-----------------------------------------
/var/db/postgres/data16/postgresql.conf
(1 row)
Enable Logs:
mkdir /var/log/postgresql
chown postgres:postgres /var/log/postgresql
vi /var/db/postgres/data16/postgresql.conf
vi /etc/postgresql/16/main/postgresql.conf
log_destination = 'stderr'
logging_collector = on
log_directory = '/var/log/postgresql'
log_filename = 'postgresql.log'
log_file_mode = 0600
log_rotation_age = 1d
log_rotation_size = 10MB
Restart PostgreSQL:
service postgresql restart
systemctl restart postgresql
tail -f /var/log/postgresql/postgresql.log
PITR:
Be aware that PITR restoration does not work if “Non-transactional instructions” are executed.
BEGIN, COMMIT, ROLLBACK, CREATE DATABASE, DROP DATABASE, ALTER SYSTEM, CREATE TABLESPACE, DROP TABLESPACE, TRUNCATE TABLE...
For DROP DATABASE
, the following error will appear:
psql -U postgres -d testkr0m -c "SELECT * FROM employees"
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: cannot connect to invalid database "testkr0m"
HINT: Use DROP DATABASE to drop invalid databases.
With DROP/TRUNCATE TABLE
, the query simply never completes:
psql -U postgres -d testkr0m -c "SELECT * FROM employees"
It never completes
Log Analysis:
When performing PITR restorations, certain log information may be helpful.
- When the backup was performed:
grep 'database system was interrupted' /var/log/postgresql/postgresql*.log|tail -n 1|grep 'database system was interrupted'
- PITR restoration point:
grep 'starting point-in-time recovery to ' /var/log/postgresql/postgresql*.log|tail -n 1|grep 'starting point-in-time recovery to '
- WAL reading stopping point:
grep 'recovery stopping before commit of transaction' /var/log/postgresql/postgresql*.log|tail -n 1|grep 'recovery stopping before commit of transaction'
Dump WALs:
Obtaining instructions from WAL
files can provide clues about why certain database commands are executed.
/usr/local/bin/pg_waldump /var/lib/pgsql/wal_archive/000000010000000000000002
/usr/lib/postgresql/16/bin/pg_waldump /var/lib/pgsql/wal_archive/000000030000000000000007
Finding DELETEs in WAL Files:
for WAL_FILE in /root/pg_wal/*; do echo "WAL: $WAL_FILE" && /usr/local/bin/pg_waldump $WAL_FILE 2>/dev/null|grep DELETE; done
for WAL_FILE in /var/db/postgres/data16/pg_wal/*; do echo "WAL: $WAL_FILE" && /usr/local/bin/pg_waldump $WAL_FILE 2>/dev/null|grep DELETE; done
for WAL_FILE in /var/lib/pgsql/wal_archive/*; do echo "WAL: $WAL_FILE" && /usr/local/bin/pg_waldump $WAL_FILE 2>/dev/null|grep DELETE; done
for WAL_FILE in /root/pg_wal/*; do echo "WAL: $WAL_FILE" && /usr/lib/postgresql/16/bin/pg_waldump $WAL_FILE 2>/dev/null|grep DELETE; done
for WAL_FILE in /var/lib/postgresql/16/main/pg_wal/*; do echo "WAL: $WAL_FILE" && /usr/lib/postgresql/16/bin/pg_waldump $WAL_FILE 2>/dev/null|grep DELETE; done
for WAL_FILE in /var/lib/pgsql/wal_archive/*; do echo "WAL: $WAL_FILE" && /usr/lib/postgresql/16/bin/pg_waldump $WAL_FILE 2>/dev/null|grep DELETE; done
Grep the “WAL names with DELETES” from the previous step in PostgreSQL logs to see if they were executed by mistake.
Missing WAL Files:
We can verify that no WAL
files are missing during restoration. If there are no gaps, all necessary files are likely present.
ls -la /root/pg_wal*
ls -la /var/lib/postgresql/16/main/pg_wal
ls -la /var/lib/postgresql/16/main/pg_wal/archive_status
ls -la /var/lib/pgsql/wal_archive/
ls -la /root/pg_wal*
ls -la /var/db/postgres/data16/pg_wal/
ls -la /var/db/postgres/data16/pg_wal/archive_status
ls -la /var/lib/pgsql/wal_archive/
WAL Integrity:
for f in /var/lib/pgsql/wal_archive/[0-9A-F][0-9A-F]*[^.backup][^.history]; do
/usr/local/bin/pg_waldump $f > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "$f OK"
else
echo "$f CORRUPTED"
fi
done
for f in /var/lib/pgsql/wal_archive/[0-9A-F][0-9A-F]*[^.backup][^.history]; do
/usr/lib/postgresql/16/bin/pg_waldump $f > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "$f OK"
else
echo "$f CORRUPTED"
fi
done
Timelines:
If we want to go to the end of a Timeline
, as in this case, there’s no need to specify a date, just the Timeline
. Otherwise, errors like these might occur:
2024-11-04 08:46:11.187 CET [50052] LOG: last completed transaction was at log time 2024-11-04 09:15:51.910801+01
2024-11-04 08:46:11.187 CET [50052] FATAL: recovery ended before configured recovery target was reached
We can check the current Timeline
:
psql -U postgres -c "SELECT timeline_id FROM pg_control_checkpoint()"
We can also view the divergence points in the Timelines
:
ls -la /var/db/postgres/data16/pg_wal/*.history
cat /var/db/postgres/data16/pg_wal/XX.history
ls -la /var/lib/pgsql/wal_archive/*.history
cat /var/lib/pgsql/wal_archive/XX.history
ls -la /var/lib/postgresql/16/main/pg_wal/*.history
cat /var/lib/postgresql/16/main/pg_wal/XX.history
ls -la /var/lib/pgsql/wal_archive/*.history
cat /var/lib/pgsql/wal_archive/XX.history
Server Out of Space:
The first step is to delete archived WAL
files, as they are not strictly necessary for the database to function. If PITR restoration to a specific point in those files is required, it won’t be possible, but the database will remain operational. Restoration to the latest Bareos backup (either Full
or Incremental
) is still possible.
service postgresql stop
rm -f /var/lib/pgsql/wal_archive/*
service postgresql start
systemctl stop postgresql
rm -f /var/lib/pgsql/wal_archive/*
systemctl start postgresql
Another option to address space issues is to
compress archived WAL
files
:
archive_command = 'gzip < %p > /mnt/server/archivedir/%f.gz'
restore_command = 'gunzip < /mnt/server/archivedir/%f.gz > %p'
WAL Paths:
Physical backups cannot be restored on a server with
different paths
than those configured on the server where the backup was taken:
CREATE TABLESPACE commands are WAL-logged with the literal absolute path, and will therefore be replayed as tablespace creations with the same absolute path.
Community:
https://discord.com/channels/710918545906597938/710918545906597941