Upgrade Keycloak 23 → 26 PostgreSQL 16 → 17 via ArgoCD - gpillon/k4all GitHub Wiki
| Component | Before | After |
|---|---|---|
| Helm Chart | keycloak-18.7.1 |
keycloak-25.1.1 |
| Keycloak | bitnamilegacy/keycloak:23.0.7-debian-12-r0 |
bitnamilegacy/keycloak:26.3.2-debian-12-r2 |
| PostgreSQL | bitnami/postgresql:16.1.0-debian-11-r25 |
bitnamilegacy/postgresql:17.5.0-debian-12-r20 |
| Namespace | keycloak |
keycloak |
| ArgoCD App |
keycloak (namespace argocd) |
keycloak (namespace argocd) |
| Ingress | https://<REDACTED>/ |
https://<REDACTED>/ |
-
kubectlconfigured with the cluster kubeconfig (KUBECONFIG=/path/to/kubeconfig) - Admin access to the Kubernetes cluster
- ArgoCD Application
keycloakwith manual sync (no auto-sync)
PostgreSQL 16 → 17 is a major version upgrade. The binary data files on disk
(in the PVC data-keycloak-postgresql-0) are NOT compatible across major versions.
A dump/restore strategy is required to migrate the data.
Keycloak automatically handles database schema migrations via Liquibase.
The migration path executed was:
23.0.7 → 24.0.0 → 24.0.3 → 25.0.0 → 26.0.0 → 26.1.0 → 26.2.0 → 26.3.0
export KUBECONFIG=/path/to/kubeconfig
# pg_dump in custom format (compressed) from the running PostgreSQL 16 pod
kubectl exec -n keycloak keycloak-postgresql-0 -- \
env PGPASSWORD=<REDACTED> pg_dump -U postgres -d bitnami_keycloak -Fc \
> keycloak_backup.dump
# Verify the file is not empty (expected ~200KB)
ls -la keycloak_backup.dumpStop Keycloak first (it depends on PostgreSQL), then PostgreSQL.
# Stop Keycloak
kubectl scale statefulset keycloak -n keycloak --replicas=0
# Stop PostgreSQL
kubectl scale statefulset keycloak-postgresql -n keycloak --replicas=0
# Verify all pods are terminated
kubectl get pods -n keycloak
# Expected output: "No resources found in keycloak namespace."Note: ArgoCD will not overwrite these changes because auto-sync is disabled.
Delete the PVC containing PostgreSQL 16 binary data, which is incompatible with PostgreSQL 17.
kubectl delete pvc data-keycloak-postgresql-0 -n keycloakModify the ArgoCD Application configuration with the new parameters.
Key changes:
-
targetRevision:18.7.1→25.1.1 -
proxyHeaders:"xforwarded,forwarded"→xforwarded(Keycloak 26 does not accept multiple values) - Added
global.security.allowInsecureImages: true(required to usebitnamilegacyimages) - All passwords, ingress config, metrics and affinity settings are preserved
# Apply the patch (argocd-patch.yaml file with the new values)
kubectl patch application keycloak -n argocd --type merge --patch-file argocd-patch.yamlContents of argocd-patch.yaml:
spec:
source:
targetRevision: "25.1.1"
helm:
values: |-
image:
registry: docker.io
repository: bitnamilegacy/keycloak
global:
security:
allowInsecureImages: true
postgresql:
auth:
password: <REDACTED>
postgresPassword: <REDACTED>
proxyHeaders: xforwarded
auth:
adminPassword: <REDACTED>
adminUser: <REDACTED>
postgresql:
image:
registry: docker.io
repository: bitnamilegacy/postgresql
primary:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- <REDACTED>
ingress:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
enabled: true
hostname: <REDACTED>
ingressClassName: nginx
labels: {}
path: '{{ .Values.httpRelativePath }}'
pathType: ImplementationSpecific
servicePort: http
tls: true
metrics:
enabled: true
prometheusRule:
enabled: true
namespace: monitor
serviceMonitor:
enabled: true
namespace: monitor
production: true
proxy: edge
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- <REDACTED>The updated chart modifies immutable fields in the StatefulSet. It must be deleted so ArgoCD can recreate it.
kubectl delete statefulset keycloak -n keycloak --cascade=orphanFor this first sync, temporarily set replicaCount: 0 in the ArgoCD Application
values so that only PostgreSQL 17 is created.
# Add "replicaCount: 0" to the values and apply the patch, then:
kubectl patch application keycloak -n argocd --type merge \
-p '{"operation":{"initiatedBy":{"username":"admin"},"sync":{"syncStrategy":{"hook":{}},"syncOptions":["RespectIgnoreDifferences=true"]}}}'Wait for PostgreSQL to be ready:
kubectl get pods -n keycloak
# Expected: keycloak-postgresql-0 1/1 Running# Copy the dump into the pod (Windows workaround: use pipe with cat)
cat keycloak_backup.dump | kubectl exec -i -n keycloak keycloak-postgresql-0 -- \
sh -c 'cat > /tmp/keycloak_backup.dump'
# Verify the file was copied correctly
MSYS_NO_PATHCONV=1 kubectl exec -n keycloak keycloak-postgresql-0 -- \
ls -la /tmp/keycloak_backup.dump
# Run the restore
MSYS_NO_PATHCONV=1 kubectl exec -n keycloak keycloak-postgresql-0 -- \
env PGPASSWORD=<REDACTED> pg_restore -U postgres -d bitnami_keycloak \
--clean --if-exists /tmp/keycloak_backup.dump
# Verify the restored tables (92 tables expected)
MSYS_NO_PATHCONV=1 kubectl exec -n keycloak keycloak-postgresql-0 -- \
env PGPASSWORD=<REDACTED> psql -U postgres -d bitnami_keycloak -c "\dt"
# Verify the realms
MSYS_NO_PATHCONV=1 kubectl exec -n keycloak keycloak-postgresql-0 -- \
env PGPASSWORD=<REDACTED> psql -U postgres -d bitnami_keycloak \
-c "SELECT id, name FROM realm;"Windows (Git Bash) note: use
MSYS_NO_PATHCONV=1before kubectl commands that contain Linux paths (e.g./tmp/...) to prevent automatic path conversion by MSYS.
Remove replicaCount: 0 from the values and perform the final sync.
# Apply the patch without replicaCount: 0
kubectl patch application keycloak -n argocd --type merge --patch-file argocd-patch.yaml
# Trigger the final sync
kubectl patch application keycloak -n argocd --type merge \
-p '{"operation":{"initiatedBy":{"username":"admin"},"sync":{"syncStrategy":{"hook":{}},"syncOptions":["RespectIgnoreDifferences=true"]}}}'Monitor the Keycloak logs to confirm the Liquibase migration:
kubectl logs -f keycloak-0 -n keycloakExpected output during migration:
migrated realm master to 24.0.0
migrated realm master to 24.0.3
migrated realm master to 25.0.0
migrated realm master to 26.0.0
migrated realm master to 26.1.0
migrated realm master to 26.2.0
migrated realm master to 26.3.0
Keycloak 26.3.2 on JVM (powered by Quarkus 3.20.2) started in ~38s
Verify the final state:
# Pods
kubectl get pods -n keycloak
# Expected: keycloak-0 1/1 Running, keycloak-postgresql-0 1/1 Running
# ArgoCD
kubectl get application keycloak -n argocd -o jsonpath='{.status.sync.status}'
# Expected: Synced
kubectl get application keycloak -n argocd -o jsonpath='{.status.health.status}'
# Expected: Healthy
# Images
kubectl get statefulset keycloak -n keycloak -o jsonpath='{.spec.template.spec.containers[0].image}'
# Expected: docker.io/bitnamilegacy/keycloak:26.3.2-debian-12-r2
kubectl get statefulset keycloak-postgresql -n keycloak -o jsonpath='{.spec.template.spec.containers[0].image}'
# Expected: docker.io/bitnamilegacy/postgresql:17.5.0-debian-12-r20Problem: The images docker.io/bitnami/keycloak:26.x and docker.io/bitnami/postgresql:17.6.0-*
do not exist in the Docker Hub registry.
Solution: Use bitnamilegacy/keycloak for Keycloak and bitnamilegacy/postgresql for PostgreSQL.
Add global.security.allowInsecureImages: true in the Helm values to bypass
the Bitnami chart's security check on non-standard images.
Problem: Keycloak 26 does not accept proxyHeaders: "xforwarded,forwarded".
Solution: Use a single value: proxyHeaders: xforwarded (correct for nginx which
uses the X-Forwarded-* headers).
Problem: kubectl apply fails with "updates to statefulset spec for fields other
than 'replicas'... are forbidden".
Solution: Delete the old StatefulSet with --cascade=orphan and let ArgoCD
recreate it from the updated chart.
Problem: Paths are incorrectly translated by MSYS.
Solution: Use cat file | kubectl exec -i ... -- sh -c 'cat > /path' to copy
files into pods, and MSYS_NO_PATHCONV=1 for exec commands with Linux paths.
In case a rollback is needed:
- Scale down Keycloak and PostgreSQL to 0 replicas
- Delete the PVC
data-keycloak-postgresql-0 - Restore the ArgoCD Application to the previous version:
targetRevision: 18.7.1- Restore
image.repository: bitnamilegacy/keycloak(withoutallowInsecureImages) - Restore
proxyHeaders: "xforwarded,forwarded"
- Sync ArgoCD (PostgreSQL 16 will be recreated empty)
- Restore the backup
keycloak_backup.dumpinto the new PG16 pod - Sync/scale up Keycloak
IMPORTANT: Keep the
keycloak_backup.dumpfile until Keycloak 26 is confirmed to be working correctly in production.