Skip to main content
Version: Release 26.1

Migrate from Bitnami to Operator-Managed PostgreSQL

This guide covers the manual steps required to migrate from the Bitnami chart-managed PostgreSQL installation (the default up to and including operator 25.1 releases) to the CloudNativePG (CNPG) operator-managed equivalent introduced in operator 25.3 releases.

The Bitnami PostgreSQL PVC and the CNPG Cluster PVC are incompatible on-disk formats. A manual dump-and-restore is required: dump the existing Bitnami data before upgrade, let the upgrade provision a fresh CNPG cluster, then restore the dump into it.

note

Before proceeding with any actions, ensure you have created a backup of your database.

Step 1. Retrieve credentials from the Bitnami PostgreSQL cluster.

POSTGRES_PASSWORD=$(kubectl get secret -n <NAMESPACE> dai-xlr-postgresql \
-o jsonpath="{.data.postgres-password}" | base64 -d)
echo "Postgres password: $POSTGRES_PASSWORD"

Step 2. Dump All Databases from the Bitnami Pod

Create a backup directory inside the running Bitnami pod and dump each database using the superuser account. The dumps use the custom format (-F c) which is compressed and supports selective restore.

note

The backup directory /bitnami/postgresql/backup lives on the same PVC as the data directory. Before creating it, verify there is sufficient free space: kubectl exec -n <NAMESPACE> dai-xlr-postgresql-0 -- df -h /bitnami/postgresql.

# Create the backup directory inside the pod
kubectl exec -n <NAMESPACE> dai-xlr-postgresql-0 -- mkdir -p /bitnami/postgresql/backup

# Dump xlr-db using the postgres superuser
kubectl exec -n <NAMESPACE> dai-xlr-postgresql-0 \
-- bash -c "PGPASSWORD='$POSTGRES_PASSWORD' pg_dump \
-U postgres -F c \
-f /bitnami/postgresql/backup/xlr-db.dump xlr-db"

# Dump xlr-report-db
kubectl exec -n <NAMESPACE> dai-xlr-postgresql-0 \
-- bash -c "PGPASSWORD='$POSTGRES_PASSWORD' pg_dump \
-U postgres -F c \
-f /bitnami/postgresql/backup/xlr-report-db.dump xlr-report-db"

# Verify the dump files exist and have non-zero size
kubectl exec -n <NAMESPACE> dai-xlr-postgresql-0 -- ls -lh /bitnami/postgresql/backup/

Step 3 — Copy Dumps to Local Machine

Copy the dumps out before the upgrade destroys the Bitnami pod and its PVC.

mkdir -p ./postgres-backup/release

kubectl cp -n <NAMESPACE> \
dai-xlr-postgresql-0:/bitnami/postgresql/backup/xlr-db.dump \
./postgres-backup/release/xlr-db.dump

kubectl cp -n <NAMESPACE> \
dai-xlr-postgresql-0:/bitnami/postgresql/backup/xlr-report-db.dump \
./postgres-backup/release/xlr-report-db.dump

ls -lh ./postgres-backup/release/

Step 4 — Scale Application to Zero Replicas

Scale the Release application pods to zero before running xl kube upgrade. This ensures no application writes occur during the migration and avoids connection errors while the PostgreSQL backend is being replaced.

# Record your current replicaCount before scaling down
REPLICA_COUNT=$(kubectl get digitalaireleases.xlr.digital.ai dai-xlr -n <NAMESPACE> \
-o jsonpath='{.spec.replicaCount}')
echo "Current replicaCount: $REPLICA_COUNT"

# Scale down to 0
kubectl patch -n <NAMESPACE> digitalaireleases.xlr.digital.ai dai-xlr \
--type=merge --patch '{"spec": {"replicaCount": 0}}'

# Wait until all Release pods are terminated
kubectl wait --for=delete pod -l app.kubernetes.io/instance=dai-xlr \
-n <NAMESPACE> --timeout=300s

Step 5 — Run xl kube upgrade

Run the upgrade wizard normally.

When prompted about PostgreSQL installation:

Answer: operator [CloudNativePG Operator (experimental/non-production)]

When prompted about PVC preservation:

Should we preserve persisted volume claims? If not all volume data will be lost: Yes

Answer: Y (Yes)

warning

This prompt is global answering No deletes all PVCs, not just PostgreSQL. Ensure you have backed up any other persistent data before answering No.

Preserve CR Keys to EXCLUDE

When the wizard asks Edit list of custom resource keys that will migrate to the new Release CR, the following keys must NOT be present in the preserve-keys list. If they are pre-populated, remove them before continuing:

  • spec.postgresql.install
  • spec.external.db (and all sub-keys: enabled, main.url, main.username, main.password, report.url, report.username, report.password)

The wizard will uninstall the Bitnami PostgreSQL Helm chart and delete its PVC, install the CNPG operator, and apply the new postgresql-cluster template which provisions a fresh CNPG cluster on the new PostgreSQL version.

Step 6 — Verify the CR After Upgrade

Immediately after the upgrade wizard completes — and before scaling the application back up — confirm that external.db points at the new operator-managed services with enabled: true. If the value is wrong or missing, the application pods will fail to start.

kubectl get digitalaireleases.xlr.digital.ai dai-xlr -n <NAMESPACE> \
-o jsonpath='{.spec.external.db}'

Step 7 — Wait for the New CNPG Cluster to be Ready

After the upgrade wizard completes, verify the new CNPG cluster comes up fully before restoring data.

# Watch until cluster phase is "Cluster in healthy state"
kubectl get cluster -n <NAMESPACE> dai-xlr-postgres -w

# Or wait until the primary pod is running and ready
kubectl wait pod/dai-xlr-postgres-1 \
-n <NAMESPACE> \
--for=condition=Ready \
--timeout=300s

# Confirm the expected PostgreSQL image is in use
kubectl get pod dai-xlr-postgres-1 -n <NAMESPACE> \
-o jsonpath='{.spec.containers[0].image}'; echo

Step 8 — Copy Dumps into the New CNPG Cluster Pod

The CNPG pod mounts /tmp as a read-only filesystem. Use /run/tmp/restore instead.

kubectl exec -n <NAMESPACE> dai-xlr-postgres-1 -- mkdir -p /run/tmp/restore

kubectl cp -n <NAMESPACE> \
./postgres-backup/release/xlr-db.dump \
dai-xlr-postgres-1:/run/tmp/restore/xlr-db.dump

kubectl cp -n <NAMESPACE> \
./postgres-backup/release/xlr-report-db.dump \
dai-xlr-postgres-1:/run/tmp/restore/xlr-report-db.dump

kubectl exec -n <NAMESPACE> dai-xlr-postgres-1 -- ls -lh /run/tmp/restore/

Step 9 — Restore the Database Dumps

xl kube upgrade provisions a fresh CNPG cluster which runs postInitApplicationSQLRefs on startup. That executes init.sql automatically, creating the application users and setting them as database owners.

Because the users and ownership are already in place, simply restore as the database owner. pg_restore connects as xlr (or xlr-report), so every object it creates is owned by that user — no grants required.

First, retrieve the application user passwords from the Release secret:

# Get xlr user password
XLR_PASSWORD=$(kubectl get secret -n <NAMESPACE> dai-xlr-digitalai-release \
-o jsonpath="{.data.mainDatabasePassword}" | base64 -d)

# Get xlr-report user password
XLR_REPORT_PASSWORD=$(kubectl get secret -n <NAMESPACE> dai-xlr-digitalai-release \
-o jsonpath="{.data.reportDatabasePassword}" | base64 -d)

Then restore each database as its owner:

# Restore xlr-db as its database owner (xlr)
kubectl exec -n <NAMESPACE> dai-xlr-postgres-1 \
-- bash -c "PGPASSWORD='$XLR_PASSWORD' pg_restore \
-U xlr \
-h localhost \
-d xlr-db \
--no-owner \
--exit-on-error \
/run/tmp/restore/xlr-db.dump"

# Restore xlr-report-db as its database owner (xlr-report)
kubectl exec -n <NAMESPACE> dai-xlr-postgres-1 \
-- bash -c "PGPASSWORD='$XLR_REPORT_PASSWORD' pg_restore \
-U 'xlr-report' \
-h localhost \
-d xlr-report-db \
--no-owner \
--exit-on-error \
/run/tmp/restore/xlr-report-db.dump"

Verify Row Counts (Optional Sanity Check)

kubectl exec -n <NAMESPACE> dai-xlr-postgres-1 \
-- bash -c "PGPASSWORD='$XLR_PASSWORD' psql -U xlr -h localhost -d xlr-db \
-c 'SELECT count(*) FROM xl_version;'"

Step 10 — Scale Application Back to Original Replica Count

kubectl patch -n <NAMESPACE> digitalaireleases.xlr.digital.ai dai-xlr \
--type=merge --patch "{\"spec\": {\"replicaCount\": $REPLICA_COUNT}}"

# Monitor pods coming up
kubectl get pods -n <NAMESPACE> -l app.kubernetes.io/instance=dai-xlr -w

Wait for all application pods to reach Running and pass their readiness probes before treating the migration as complete.

Cleanup

Once the application is confirmed healthy, remove the dump files:

# Local cleanup
rm -rf ./postgres-backup/

# Pod cleanup
kubectl exec -n <NAMESPACE> dai-xlr-postgres-1 -- rm -rf /run/tmp/restore