Skip to main content
Version: Release 26.3

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.

These steps use the default namespace digitalai. If you installed Release in a custom namespace, replace digitalai with your namespace throughout these steps.

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 digitalai 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 digitalai dai-xlr-postgresql-0 -- df -h /bitnami/postgresql.

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

# Dump xlr-db using the postgres superuser
kubectl exec -n digitalai 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 digitalai 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 digitalai 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 digitalai \
dai-xlr-postgresql-0:/bitnami/postgresql/backup/xlr-db.dump \
./postgres-backup/release/xlr-db.dump

kubectl cp -n digitalai \
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 digitalai \
-o jsonpath='{.spec.replicaCount}')
echo "Current replicaCount: $REPLICA_COUNT"

# Scale down to 0
kubectl patch -n digitalai 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 digitalai --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 digitalai \
-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 digitalai dai-xlr-postgres -w

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

# Confirm the expected PostgreSQL image is in use
kubectl get pod dai-xlr-postgres-1 -n digitalai \
-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 digitalai dai-xlr-postgres-1 -- mkdir -p /run/tmp/restore

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

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

kubectl exec -n digitalai 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 digitalai dai-xlr-digitalai-release \
-o jsonpath="{.data.mainDatabasePassword}" | base64 -d)

# Get xlr-report user password
XLR_REPORT_PASSWORD=$(kubectl get secret -n digitalai 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 digitalai 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 digitalai 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 digitalai 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 digitalai digitalaireleases.xlr.digital.ai dai-xlr \
--type=merge --patch "{\"spec\": {\"replicaCount\": $REPLICA_COUNT}}"

# Monitor pods coming up
kubectl get pods -n digitalai -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 digitalai dai-xlr-postgres-1 -- rm -rf /run/tmp/restore