This post was originally published in 2021 and was updated in 2025.
Kubernetes adoption keeps climbing, and databases are often one of the last workloads teams try to move. The reasons are clear: PostgreSQL is critical, downtime isn’t an option, and migration can feel risky. But with the right approach, you can modernize without bringing your applications to a halt.
In this post, I’ll show you how to migrate a PostgreSQL database to Kubernetes with minimal downtime using Percona Operator for PostgreSQL. You’ll see how backups and Write Ahead Logs (WALs) flow through pgBackRest to keep the target cluster in sync until you’re ready to cut over.
This walkthrough focuses on the technical steps, but if you’re evaluating PostgreSQL on Kubernetes more broadly (automation, scaling, or day-2 operations), make sure to check out our in-depth resources at the end.
Goal
To perform the migration I’m going to use the following setup:
- PostgreSQL database deployed on-prem or in the cloud (the Source).
- Google Kubernetes Engine (GKE) cluster where Percona Operator deploys and manages a PostgreSQL cluster (the Target) and pgBackRest Pod.
- PostgreSQL backups and Write Ahead Logs are uploaded to an Object Storage bucket (GCS in my case).
- pgBackRest Pod reads the data from the bucket.
- pgBackRest Pod restores the data continuously to the PostgreSQL cluster in Kubernetes.
The data is continuously synchronized. In the end, I want to shut down PostgreSQL running on-prem and only keep the cluster in GKE.
Migration
Prerequisites
To replicate the setup, you will need the following:
- PostgreSQL (v 12 or 13) running somewhere
- pgBackRest installed
- Google Cloud Storage or any S3 bucket (examples here use GCS)
- Kubernetes cluster
Configure the source
I have Percona Distribution for PostgreSQL version 13 running on some Linux machines.
1. Configure pgBackrest
1 2 3 4 5 6 7 8 9 10 11 12 |
# cat /etc/pgbackrest.conf [global] log-level-console=info log-level-file=debug start-fast=y [db] pg1-path=/var/lib/postgresql/13/main repo1-type=gcs repo1-gcs-bucket=sp-test-1 repo1-gcs-key=/tmp/gcs.key repo1-path=/on-prem-pg |
-
pg1-path
should point to PostgreSQL data directory -
repo1-type
is set to GCS as we want our backups to go there -
The key is in
/tmp/gcs.key
file. The key can be obtained through Google Cloud UI. -
The backups are going to be stored in
on-prem-pg
folder insp-test-1
bucket
2. Edit postgresql.conf config to enable archival through pgBackrest
1 2 |
archive_mode = on archive_command = 'pgbackrest --stanza=db archive-push %p' |
A restart is required after changing the configuration.
3. Operator requires to have a postgresql.conf file in the data directory. It is enough to have an empty file:
1 |
touch /var/lib/postgresql/13/main/postgresql.conf |
4. Create primaryuser on the Source
1 |
# create user primaryuser with encrypted password '<PRIMARYUSER PASSWORD>' replication; |
Configure the target
1. Deploy Percona Operator for PostgreSQL on Kubernetes. Read more about it in the documentation here.
1 2 3 4 5 6 7 8 9 |
# create the namespace kubectl create namespace pgo # clone the git repository git clone -b v0.2.0 https://github.com/percona/percona-postgresql-operator/ cd percona-postgresql-operator # deploy the operator kubectl apply -f deploy/operator.yaml |
2. Edit main custom resource manifest – deploy/cr.yaml.
Keep the cluster name as cluster1. The cluster will run in Standby mode, syncing data from the GCS bucket.
Example spec.backup section:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
backup: ... repoPath: "/on-prem-pg" ... storages: my-s3: type: gcs endpointUrl: https://storage.googleapis.com region: us-central1-a uriStyle: path verifyTLS: false bucket: sp-test-1 storageTypes: [ "gcs" ] |
- Also, set spec.pgReplicas.hotStandby.size to 1 for at least one replica.
3. Operator should be able to authenticate with GCS.
To do that we need to create a secret object called <CLUSTERNAME>-backrest-repo-config with gcs-key in data. It should be the same key we used on the Source. See the example of this secret here.
1 |
kubectl apply -f gcs.yaml |
4. Create users by creating Secret objects: postgres and primaryuser (the one we created on the Source). See the examples of users Secrets here. The passwords should be the same as on the Source.
1 |
kubectl apply -f users.yaml |
5. Now let’s deploy our cluster on Kubernetes by applying the cr.yaml:
1 |
kubectl apply -f deploy/cr.yaml |
Verify and troubleshoot
If everything is configured correctly, you should see the following in the Primary Pod logs:
1 2 3 4 5 6 7 |
kubectl -n pgo logs -f --tail=20 cluster1-5dfb96f77d-7m2rs 2021-07-30 10:41:08,286 INFO: Reaped pid=548, exit status=0 2021-07-30 10:41:08,298 INFO: establishing a new patroni connection to the postgres cluster 2021-07-30 10:41:08,359 INFO: initialized a new cluster Fri Jul 30 10:41:09 UTC 2021 INFO: PGHA_INIT is 'true', waiting to initialize as primary Fri Jul 30 10:41:09 UTC 2021 INFO: Node cluster1-5dfb96f77d-7m2rs fully initialized for cluster cluster1 and is ready for use 2021-07-30 10:41:18,781 INFO: Lock owner: cluster1-5dfb96f77d-7m2rs; I am cluster1-5dfb96f77d-7m2rs 2021-07-30 10:41:18,810 INFO: no action. i am the standby leader with the lock 2021-07-30 10:41:28,781 INFO: Lock owner: cluster1-5dfb96f77d-7m2rs; I am cluster1-5dfb96f77d-7m2rs 2021-07-30 10:41:28,832 INFO: no action. i am the standby leader with the lock |
Make a change on the Source and confirm that it is synchronized to the Target cluster.
Common issues
Forgot to create postgresql.conf?
1 |
FileNotFoundError: [Errno 2] No such file or directory: '/pgdata/cluster1/postgresql.conf' -> '/pgdata/cluster1/postgresql.base.conf' |
Forgot to create primaryuser?
1 |
psycopg2.OperationalError: FATAL: password authentication failed for user "primaryuser" |
Wrong or missing object store credentials?
1 2 3 4 5 6 |
WARN: repo1: [CryptoError] unable to load info file '/on-prem-pg/backup/db/backup.info' or '/on-prem-pg/backup/db/backup.info.copy': CryptoError: raised from remote-0 protocol on 'cluster1-backrest-shared-repo': unable to read PEM: [218529960] wrong tag HINT: is or was the repo encrypted? CryptoError: raised from remote-0 protocol on 'cluster1-backrest-shared-repo': unable to read PEM: [218595386] nested asn1 error HINT: is or was the repo encrypted? HINT: backup.info cannot be opened and is required to perform a backup. HINT: has a stanza-create been performed? ERROR: [075]: no backup set found to restore Fri Jul 30 10:54:00 UTC 2021 ERROR: pgBackRest standby Creation: pgBackRest restore failed when creating standby |
Cutover
Once you’re confident everything is working, it’s time to complete the migration.
1. Stop the source PostgreSQL cluster to ensure no data is written
1 |
systemctl stop postgresql |
2. Promote the Target cluster to primary.
To do that remove spec.backup.repoPath, change spec.standby to false in deploy/cr.yaml, and apply the changes:
1 |
kubectl apply -f deploy/cr.yaml |
PostgreSQL will restart, and the logs should confirm promotion:
1 2 3 4 5 6 7 |
2021-07-30 11:16:20,020 INFO: updated leader lock during promote 2021-07-30 11:16:20,025 INFO: Changed archive_mode from on to True (restart might be required) 2021-07-30 11:16:20,025 INFO: Changed max_wal_senders from 10 to 6 (restart might be required) 2021-07-30 11:16:20,027 INFO: Reloading PostgreSQL configuration. server signaled 2021-07-30 11:16:21,037 INFO: Lock owner: cluster1-5dfb96f77d-n4c79; I am cluster1-5dfb96f77d-n4c79 2021-07-30 11:16:21,132 INFO: no action. i am the leader with the lock |
Wrapping up
Migrating PostgreSQL to Kubernetes doesn’t have to be overwhelming. With the Percona Operator for PostgreSQL and pgBackRest, you can keep downtime minimal while gaining the flexibility and consistency of a Kubernetes-native deployment.
If you’re considering running PostgreSQL on Kubernetes at scale, the migration itself is only the first step. Day-2 operations—like scaling, monitoring, and cost management—can quickly become the real challenge. That’s why we’ve put together a dedicated resource to show you how to simplify PostgreSQL in the cloud with automation, scalability, and zero lock-in.
Explore how Percona makes PostgreSQL on Kubernetes easier