Veeam Kasten PostgresSQL logical backup and restore definitive guide
Link to the Veeam Kasten guide that this writing was Inspired by Logical PostgreSQL Backup
Useful video on the topic also from Veeam Kasten engineer on YouTube
- In this guide we will be installing PostgreSQL instance into
postgresql-k10-testnamespace which we will be backing up later. - Veeam Kasten is deployed into
kasten-ionamespace. - For backup repository, (Profiles / Locations) we already have set up Cohesity S3 appliance.
Enviroment
I was using VMware Tanzu distribution of Kubernetes, Veeam Kasten 8.0.13 and PostgreSQL version 17.5 (Chart version 18.1.0)
Create namespace where our PostgreSQL will live
- Create namespace
jjozelj@jjozelj:~$ kubectl create namespace postgresql-k10-test
- Label the namespace
jjozelj@jjozelj:~$ kubectl label namespace postgresql-k10-test pod-security.kubernetes.io/enforce=baseline pod-security.kubernetes.io/enforce-version=latest
Install PostgresSQL
Install PostgreSQL server from a Bitnami Helm chart, we will be backing up this instance with Veeam Kasten and Kanister.
1. Add bitnami helm repository (If not done already)
jjozelj@jjozelj:~$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories
2. Install PostgreSQL into the correct namespace
jjozelj@jjozelj:~$ helm install --namespace postgresql-k10-test postgres bitnami/postgresql --set global.security.allowInsecureImages=true --set volumePermissions.image.repository=bitnamilegacy/os-shell --set image.repository=bitnamilegacy/postgresql
level=WARN msg="unable to find exact version; falling back to closest available version" chart=postgresql requested="" selected=18.1.10
NAME: postgres
LAST DEPLOYED: Thu Nov 20 11:00:12 2025
NAMESPACE: postgresql-k10-test
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
CHART NAME: postgresql
CHART VERSION: 18.1.10
APP VERSION: 18.1.0
⚠ WARNING: Since August 28th, 2025, only a limited subset of images/charts are available for free.
Subscribe to Bitnami Secure Images to receive continued support and security updates.
More info at https://bitnami.com and https://github.com/bitnami/containers/issues/83267
** Please be patient while the chart is being deployed **
PostgreSQL can be accessed via port 5432 on the following DNS names from within your cluster:
postgres-postgresql.postgresql-k10-test.svc.cluster.local - Read/Write connection
To get the password for "postgres" run:
export POSTGRES_PASSWORD=$(kubectl get secret --namespace postgresql-k10-test postgres-postgresql -o jsonpath="{.data.postgres-password}" | base64 -d)
To connect to your database run the following command:
kubectl run postgres-postgresql-client --rm --tty -i --restart='Never' --namespace postgresql-k10-test --image registry-1.docker.io/bitnamilegacy/postgresql:latest --env="PGPASSWORD=$POSTGRES_PASSWORD" \
--command -- psql --host postgres-postgresql -U postgres -d postgres -p 5432
> NOTE: If you access the container using bash, make sure that you execute "/opt/bitnami/scripts/postgresql/entrypoint.sh /bin/bash" in order to avoid the error "psql: local user with ID 1001} does not exist"
To connect to your database from outside the cluster execute the following commands:
kubectl port-forward --namespace postgresql-k10-test svc/postgres-postgresql 5432:5432 &
PGPASSWORD="$POSTGRES_PASSWORD" psql --host 127.0.0.1 -U postgres -d postgres -p 5432
.
.
.
3. Get the password from the secret
Make sure that the name of the namespace is correct. We will use that secret to login to the PostgreSQL and check the version and later to see if the restore finished successfully.
jjozelj@jjozelj:~$ kubectl get secret --namespace postgresql-k10-test postgres-postgresql -o jsonpath="{.data.postgres-password}" | base64 -d
dPnUZX2nZY
jjozelj@jjozelj:~$
4. Exec into the pod
jjozelj@jjozelj:~$ kubectl exec -it postgres-postgresql-0 -n postgresql-k10-test -- sh
5. Log in to the PostgreSQL, list the databases and get the version
$ psql -U postgres
Password for user postgres:
psql (17.5)
Type "help" for help.
postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+-------------+-------------+--------+-----------+-----------------------
postgres | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
template0 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(3 rows)
postgres=# SELECT version();
version
---------------------------------------------------------------------------------------------------
PostgreSQL 17.5 on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14+deb12u1) 12.2.0, 64-bit
(1 row)
postgres=#
6. Lets also create new database
For testing the restore later we can create a test database.
postgres=# CREATE DATABASE mydb;
CREATE DATABASE
postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+-------------+-------------+--------+-----------+-----------------------
mydb | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
postgres | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
template0 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(4 rows)
postgres=#
Lets fill the newly created database with some sample table and content.
- Connect to the database
\c mydb
- Create a table
CREATE TABLE planets (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
diameter_km INTEGER,
has_life BOOLEAN
);
- Insert a sample data
INSERT INTO planets (name, diameter_km, has_life)
VALUES
('Mercury', 4879, FALSE),
('Venus', 12104, FALSE),
('Earth', 12742, TRUE),
('Mars', 6779, FALSE);
- Make sure it was created
mydb=# SELECT * FROM planets;
id | name | diameter_km | has_life
----+---------+-------------+----------
1 | Mercury | 4879 | f
2 | Venus | 12104 | f
3 | Earth | 12742 | t
4 | Mars | 6779 | f
(4 rows)
mydb=#
Create and apply the Blueprint
Veeam Kasten is using Kanister in the background to create logical backups of databases.
To successfully create a logical backup with Veeam Kasten, we need to use Kanisters workflow.
When it comes to Kastens implementation of Kanisters features it comes down to these things:
- Blueprints – Tells the Kanister how to do backup, restore, and deletion of restore points etc..
- ActionSets – Are created automatically by Kasten when the backup job is started
- Profiles – Can be set trough Kasten UI, and are repositories (where kasten/kanister is saving backups and restore points)
Blueprint Bindings – Can be set trough Kasten UI and tells the Kanister which blueprint (set of instructions on how to execute actions) is used for which application
The main parts of the blueprint are:
Actions- Which are
backup– Implements logical backuprestore– Implements restore of the logical backupdelete– Implements the deletion of the logical restore points from the repository
- Which are
Phases- Each
actionincludes a phase which executes afuncKanister functions . phasesalso defineargswhere the majority of the “action” is happening when thephasesare executed
- Each
1. Create postgres-logical-blueprint yaml, with the following contents
Blueprint was copied from Kanister GitHub repository kanister/examples/postgresql/v10.16.2/postgres-blueprint.yaml
What was changed from the original Blueprint v10.16.2 was:
- export PGPASSWORD “”postgresql-password”” to “postgres-password” in
backupandrestoreactions - | gunzip -c -f | sed ‘s/LOCALE/LC_COLLATE/’ | which is used in PostgreSQL older than 15, so if you want to restore older versions you can leave it like this, but for newer versions you have to include only | gunzip -c -f |
The guide from Veeam Kasten is using the V2 version, which I could not get to run properly, and it also has an interesting comment in GitHub README.md
NOTE: v2 Blueprints are experimental and are not supported with standalone Kanister.
postgres-logical-blueprint.yaml
kind: Blueprint
apiVersion: cr.kanister.io/v1alpha1
metadata:
name: postgres-logical-blueprint
namespace: kasten-io
actions:
backup:
kind: StatefulSet
outputArtifacts:
cloudObject:
keyValue:
backupLocation: "{{ .Phases.pgDump.Output.backupLocation }}"
phases:
- func: KubeTask
name: pgDump
objects:
pgSecret:
kind: Secret
name: '{{ index .Object.metadata.labels "app.kubernetes.io/instance"
}}-postgresql'
namespace: "{{ .StatefulSet.Namespace }}"
args:
command:
- bash
- -o
- errexit
- -o
- pipefail
- -c
- >
export PGHOST='{{ index .Object.metadata.labels
"app.kubernetes.io/instance" }}-postgresql.{{
.StatefulSet.Namespace }}.svc.cluster.local'
export PGUSER='postgres'
export PGPASSWORD='{{ index .Phases.pgDump.Secrets.pgSecret.Data
"postgres-password" | toString }}'
BACKUP_LOCATION=pg_backups/{{ .StatefulSet.Namespace }}/{{
.StatefulSet.Name }}/{{ toDate
"2006-01-02T15:04:05.999999999Z07:00" .Time | date
"2006-01-02T15:04:05Z07:00" }}/backup.sql.gz
pg_dumpall --clean -U $PGUSER | gzip -c | kando location push
--profile '{{ toJson .Profile }}' --path "${BACKUP_LOCATION}" -
kando output backupLocation "${BACKUP_LOCATION}"
image: ghcr.io/kanisterio/postgres-kanister-tools:0.116.0
namespace: "{{ .StatefulSet.Namespace }}"
delete:
inputArtifactNames:
- cloudObject
phases:
- func: KubeTask
name: deleteDump
args:
command:
- bash
- -o
- errexit
- -o
- pipefail
- -c
- >
kando location delete --profile '{{ toJson .Profile }}' --path '{{
.ArtifactsIn.cloudObject.KeyValue.backupLocation }}'
image: ghcr.io/kanisterio/postgres-kanister-tools:0.116.0
namespace: "{{ .Namespace.Name }}"
restore:
kind: StatefulSet
inputArtifactNames:
- cloudObject
phases:
- func: KubeTask
name: pgRestore
objects:
pgSecret:
kind: Secret
name: '{{ index .Object.metadata.labels "app.kubernetes.io/instance"
}}-postgresql'
namespace: "{{ .StatefulSet.Namespace }}"
args:
command:
- bash
- -o
- errexit
- -o
- pipefail
- -c
- >
export PGHOST='{{ index .Object.metadata.labels
"app.kubernetes.io/instance" }}-postgresql.{{
.StatefulSet.Namespace }}.svc.cluster.local'
export PGUSER='postgres'
export PGPASSWORD='{{ index
.Phases.pgRestore.Secrets.pgSecret.Data "postgres-password" |
toString }}'
BACKUP_LOCATION={{
.ArtifactsIn.cloudObject.KeyValue.backupLocation }}
kando location pull --profile '{{ toJson .Profile }}' --path
"${BACKUP_LOCATION}" - | gunzip -c -f | psql -q -U "${PGUSER}"
image: ghcr.io/kanisterio/postgres-kanister-tools:0.116.0
namespace: "{{ .StatefulSet.Namespace }}"
2. Make sure the password to access the postgreSQL is defined corectly
In backup and restore actions we have defined
export PGPASSWORD='{{ index .Phases.pgDump.Secrets.pgSecret.Data "postgres-password" | toString }}'
We can double check this with “getting” the secret as a yaml
jjozelj@jjozelj:~$ kubectl get secret postgres-postgresql -n postgresql-k10-test -o yaml
apiVersion: v1
data:
postgres-password: WHUxdkt3TVRrcA==
kind: Secret
metadata:
annotations:
.
.
.
3. Apply the blueprint file
We can apply it trough CLI or trough Kasten UI under Blueprints > Blueprints > Create New Blueprint
Or trough CLI with the following command:
jjozelj@jjozelj:~$ kubectl --namespace kasten-io apply -f postgres-logical-blueprint.yaml
4. Create Blueprint Binding
Kasten and Kanister need Blueprint Binding, so that when the backup job knows which resources to back up. In this case we are backing up StatefulSet with annotations:
release-namespace: postgresql-k10-testrelease-name: postgresBlueprint=postgres-logical-blueprint
You can create Blueprint Binding in two ways:
- One is trough the kubectl CLI
- The other is trough Kasten UI under
Blueprints > Bindings > Create New Binding
Create Blueprint Binding trough kubectl CLI
jjozelj@jjozelj:~$ kubectl --namespace postgresql-k10-test annotate statefulset/postgres-postgresql kanister.kasten.io/blueprint=postgres-logical-blueprint
statefulset.apps/postgres-postgresql annotated
Here we can see that it added annotations to the statefulset
jjozelj@jjozelj:~$ kubectl describe statefulset postgres-postgresql -n postgresql-k10-test
Name: postgres-postgresql
Namespace: postgresql-k10-test
CreationTimestamp: Tue, 18 Nov 2025 15:00:50 +0000
Selector: app.kubernetes.io/component=primary,app.kubernetes.io/instance=postgres,app.kubernetes.io/name=postgresql
Labels: app.kubernetes.io/component=primary
app.kubernetes.io/instance=postgres
app.kubernetes.io/managed-by=Helm
app.kubernetes.io/name=postgresql
app.kubernetes.io/version=18.1.0
helm.sh/chart=postgresql-18.1.10
Annotations: kanister.kasten.io/blueprint: postgres-logical-blueprint
meta.helm.sh/release-name: postgres
meta.helm.sh/release-namespace: postgresql-k10-test
Replicas: 1 desired | 1 total
Create Backup Policy
Backup policy is created in Veeam Kasten UI:
Navigate to Policies > Policies > Create New Policy


Run the Backup Policy
Under Policies > Policies select the three dots on the right side of the policy name and select Run Once, and click Yes, Continue.
We can follow the process of the backup trough the Veeam Kasten UI on the Dashboard or trough the CLI with this commands:
kubectl get actionset -A -o wide --watch
Disaster strikes
1. Delete the namespace
Someone mistakenly deletes the whole namespace where our super important PostgreSQL StatefulSet lived.
kubectl delete namespace postgresql-k10-test
This will also delete Persistant Volume Claim, the Persistant Volume will also get deleted, as we have set persistentVolumeReclaimPolicy: Delete. But the PV can also be deleted manually.
We can check the status of objects with the following command:
kubectl get all -n postgresql-k10-test
2. Recreate the namespace
Follow the steps from the beginning of this guide at Create namespace where our PostgreSQL will live We can set the same name or we can change it to something else.
3. Restore the PostgreSQL
- Restore is done trough the Veeam Kasten UI select the
Restore Pointsthen select the three dots on the right side of the application namepostgresql-k10-testand restore point time you wish to restore it to. - Select
Restore
Restore process can be monitored trough the Veeam Kasten UI on the Dashboard or trough the CLI, with the following command:
kubectl get actionset -A -o wide --watch
4. verify that restore process
- Check if the resources are back in the appropriate namespace
jjozelj@jjozelj:~$ kubectl get all -n postgresql-k10-test
NAME READY STATUS RESTARTS AGE
pod/postgres-postgresql-0 1/1 Running 0 2m35s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/postgres-postgresql ClusterIP 10.124.148.176 <none> 5432/TCP 3m35s
service/postgres-postgresql-hl ClusterIP None <none> 5432/TCP 3m35s
NAME READY AGE
statefulset.apps/postgres-postgresql 1/1 2m35s
- Exec into the pod
jjozelj@jjozelj:~$ kubectl exec -it postgres-postgresql-0 -n postgresql-k10-test -- sh
- Log in to the PostgreSQL, list the databases and the content of
mydb
$ psql -U postgres
Password for user postgres:
psql (17.5)
Type "help" for help.
postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+-------------+-------------+--------+-----------+-----------------------
mydb | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
postgres | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
template0 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | postgres=CTc/postgres+
| | | | | | | | =c/postgres
(4 rows)
postgres=# \c mydb
You are now connected to database "mydb" as user "postgres".
mydb=# SELECT * FROM planets;
id | name | diameter_km | has_life
----+---------+-------------+----------
1 | Mercury | 4879 | f
2 | Venus | 12104 | f
3 | Earth | 12742 | t
4 | Mars | 6779 | f
(4 rows)
mydb=#