Skip to main content

Keycloak Database Migration

This guide covers migrating Keycloak's database from the in-cluster PostgreSQL (Crunchy PGO) to a cloud-managed PostgreSQL instance. Two target options are supported:

  • Shared database — Keycloak reuses the existing CodeMie PostgreSQL instance. No infrastructure changes required.
  • Dedicated database — Keycloak gets its own PostgreSQL instance provisioned by Terraform.
Plan Downtime

Keycloak will be unavailable during the migration.

What Changes

ComponentBefore (PGO)After (Cloud-Managed)
Database hostkeycloak-primary.security.svcCloud PostgreSQL endpoint
Database useradminkeycloak_admin
Kubernetes secretkeycloak-pguser-admin (auto-created by PGO)keycloak-postgresql (manually created)
Secret keyspassword, host, port, dbname, user, uri, jdbc-uripassword only
Helm values sectionpgo.enabled: truesharedDatabaseInstance.enabled: true/false
OperatorPostgres Operator in postgres-operator namespaceNot required

Prerequisites

  • kubectl access to the cluster (security and postgres-operator namespaces)
  • Updated codemie-helm-charts repository with the new Keycloak configuration
  • deployment_outputs.env with target database connection details
Dedicated database only
  1. Update the Terraform repository and set TF_VAR_keycloak_db_config='{"enabled":true}' in deployment.conf.
  2. Apply Terraform to provision a dedicated PostgreSQL instance.
  3. Copy deployment_outputs.env to codemie-helm-charts.

Step 1: Stop Keycloak

kubectl scale statefulset keycloakx -n security --replicas=0
kubectl get pods -n security -l app.kubernetes.io/name=keycloakx

Step 2: Dump the PGO Database

Launch a temporary pod and dump the database. The pod will be reused in Step 5 for restoring.

KC_PGO_PASSWORD=$(kubectl get secret keycloak-pguser-admin -n security \
-o jsonpath='{.data.password}' | base64 -d)
kubectl run pg-tmp \
--image=postgres:17-alpine \
--restart=Never \
-n security \
-- sleep 3600
kubectl wait pod/pg-tmp -n security \
--for=condition=Ready --timeout=60s
kubectl exec pg-tmp -n security -- sh -c \
"PGPASSWORD='${KC_PGO_PASSWORD}' pg_dump \
--host=keycloak-primary.security.svc \
--port=5432 \
--username=admin \
--dbname=keycloak \
--no-owner \
--no-privileges \
--format=plain \
--file=/tmp/keycloak-dump.sql"
# Save a local backup copy
kubectl cp security/pg-tmp:/tmp/keycloak-dump.sql ./keycloak-dump.sql

Step 3: Create Kubernetes Secrets

source deployment_outputs.env
# Password for the keycloak_admin database user
kubectl create secret generic keycloak-postgresql \
--from-literal=password="${KEYCLOAK_POSTGRES_DATABASE_PASSWORD}" \
--namespace security

Step 4: Prepare the Target Database

Edit keycloak-helm/values-<cloud_name>.yaml:

  database:
hostname: "<KEYCLOAK_POSTGRES_DATABASE_HOST value>"
# ...
sharedDatabaseInstance:
enabled: false
initImage: alpine/psql:18.3

Step 5: Restore the Dump

Restore from the dump file that is already inside pg-tmp (created in Step 2).

kubectl exec pg-tmp -n security -- sh -c \
"PGPASSWORD='${KEYCLOAK_POSTGRES_DATABASE_PASSWORD}' psql \
--host='${KEYCLOAK_POSTGRES_DATABASE_HOST}' \
--port=5432 \
--username=keycloak_admin \
--dbname=keycloak \
--file=/tmp/keycloak-dump.sql"

Clean up the temporary pod:

kubectl delete pod pg-tmp -n security

Step 6: Deploy Updated Keycloak

helm upgrade --install keycloak keycloak-helm/. \
-n security \
--values keycloak-helm/values-<cloud_name>.yaml \
--wait \
--timeout 900s \
--dependency-update

Step 7: Verify

kubectl get pods -n security -l app.kubernetes.io/name=keycloakx
kubectl logs -n security -l app.kubernetes.io/name=keycloakx --tail=20

Then verify in the browser:

  1. Open the Keycloak admin console
  2. Check that realms, clients, and users are present
  3. Test an SSO login flow

Step 8: Clean Up PGO Resources

The PGO PostgreSQL pods and secrets in the security namespace are removed automatically by helm upgrade (the postgres-cluster.yaml template no longer exists in the chart). Only the Postgres Operator itself remains in a separate namespace:

helm uninstall postgres-operator -n postgres-operator
kubectl delete namespace postgres-operator