Production Deployment with Helm
The ClickID Helm chart deploys the full stack — Keycloak, the SP Portal, and PostgreSQL — to a Kubernetes cluster. The chart is located at infra/helm/clickid/ in the repository.
Prerequisites
Before deploying, ensure you have:
| Requirement | Notes |
|---|---|
| Kubernetes cluster | K3s is recommended for single-node; any K8s 1.26+ works |
nginx-ingress-controller | Installed in the cluster (ingress-nginx chart) |
cert-manager | For automatic TLS certificate issuance (Let's Encrypt) |
helm CLI | Helm 3.12+ |
kubectl | Configured to reach your cluster |
| Container registry | For hosting your custom images (ghcr.io recommended) |
| DNS records | auth.clickid.eu and portal.clickid.eu pointing to your ingress IP |
Step 1: Build and push images
The Helm chart references container images that you must build from the repository and push to your registry.
# Clone the repository
git clone https://github.com/clickid/clickid
cd clickid
# Set your registry prefix
REGISTRY=ghcr.io/your-org/clickid
# Build and push the custom Keycloak image
docker build -t ${REGISTRY}/keycloak:latest ./keycloak
docker push ${REGISTRY}/keycloak:latest
# Build and push the SP Portal image
docker build -t ${REGISTRY}/sp-portal:latest ./sp-portal
docker push ${REGISTRY}/sp-portal:latest
Update the image references in your Helm values to point to your registry.
Step 2: Update Helm dependencies
The chart depends on the Bitnami PostgreSQL subchart. Fetch it before installing:
helm dependency update infra/helm/clickid
Step 3: Install the chart
Generate secrets for the install command. Do this once and store the values securely (e.g. in a password manager or Vault):
KEYCLOAK_ADMIN_PASSWORD=$(openssl rand -base64 20)
KEYCLOAK_DB_PASSWORD=$(openssl rand -base64 20)
SECTOR_ID_PEPPER=$(openssl rand -base64 32)
POSTGRESQL_PASSWORD=$(openssl rand -base64 20)
SP_PORTAL_AUTH_SECRET=$(openssl rand -base64 32)
Install the chart:
helm install clickid ./infra/helm/clickid \
--namespace clickid --create-namespace \
--set keycloak.hostname=auth.clickid.eu \
--set spPortal.hostname=portal.clickid.eu \
--set keycloak.admin.password=${KEYCLOAK_ADMIN_PASSWORD} \
--set keycloak.db.password=${KEYCLOAK_DB_PASSWORD} \
--set keycloak.sectorIdPepper=${SECTOR_ID_PEPPER} \
--set postgresql.auth.password=${POSTGRESQL_PASSWORD} \
--set spPortal.authSecret=${SP_PORTAL_AUTH_SECRET} \
--set spPortal.keycloak.clientSecret=your-portal-client-secret \
--set spPortal.keycloakAdmin.clientSecret=your-portal-admin-secret \
--set keycloak.ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod \
--set spPortal.ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod
For anything beyond a quick test, use a values.yaml file instead of many --set flags. Store secrets separately using --set-file or a secrets manager integration like External Secrets Operator.
Verifying the deployment
Check pod status
kubectl get pods -n clickid
Expected output (all pods Running or Completed):
NAME READY STATUS RESTARTS AGE
clickid-keycloak-6d9f7b4c8-xk2pj 1/1 Running 0 3m
clickid-sp-portal-7c8d9f6b5-mq3rn 1/1 Running 0 3m
clickid-postgresql-0 1/1 Running 0 3m
Check Keycloak logs
kubectl logs -n clickid deployment/clickid-keycloak --tail=50
Look for Keycloak 24.x.x started in ...ms. If Keycloak is crash-looping, check the logs for database connection errors or missing environment variables.
Check ingress
kubectl get ingress -n clickid
Both ingresses (clickid-keycloak and clickid-sp-portal) should have an ADDRESS assigned (the nginx ingress IP). TLS certificates may take 1–2 minutes to issue via Let's Encrypt.
Verify TLS
curl -I https://auth.clickid.eu/realms/clickid/protocol/saml/descriptor
Should return HTTP 200 with a valid TLS certificate.
Post-install
Realm and demo user import
The clickid and clickid-sandbox realms are imported automatically from the realm JSON files bundled in the Keycloak image on first startup. You do not need to manually configure Keycloak.
The demo user (resident@example.nl) is seeded in both realms by a Kubernetes Job that runs after Keycloak is healthy.
Access the Keycloak admin console
The Keycloak admin console is not exposed via ingress by default (security best practice). Use kubectl port-forward to access it:
kubectl port-forward -n clickid svc/clickid-keycloak 8080:8080
# Open http://localhost:8080/admin in your browser
Upgrading
To upgrade an existing deployment:
# Pull latest changes
git pull
# Rebuild and push images if changed
docker build -t ${REGISTRY}/keycloak:latest ./keycloak && docker push ${REGISTRY}/keycloak:latest
docker build -t ${REGISTRY}/sp-portal:latest ./sp-portal && docker push ${REGISTRY}/sp-portal:latest
# Upgrade the Helm release
helm upgrade clickid ./infra/helm/clickid \
--namespace clickid \
--reuse-values \
--set keycloak.image.tag=latest \
--set spPortal.image.tag=latest
If the realm JSON has changed between versions, the import only runs on first startup by default. To re-import realms (e.g. to apply new client or policy configuration), set keycloak.importRealms.overwrite=true during upgrade. This will overwrite realm configuration but not user data.
High availability
For production workloads requiring HA:
helm upgrade clickid ./infra/helm/clickid \
--namespace clickid \
--reuse-values \
--set keycloak.replicas=2
Keycloak clustering uses KUBE_PING (JGroups discovery via the Kubernetes API), which is pre-configured in the Helm chart. All pods in the clickid namespace are discovered automatically.
For HA with replicas > 1, ensure:
- All Keycloak pods share the same
SECTOR_ID_PEPPER(guaranteed by the chart's shared Secret) - PostgreSQL is also HA (consider the Bitnami PostgreSQL HA chart or an external managed database)
- Your ingress is configured to use sticky sessions or set
keycloak.proxy=reencryptif TLS terminates at the pod
# Check clustering status from a running pod
kubectl exec -n clickid deployment/clickid-keycloak -- \
/opt/keycloak/bin/kc.sh show-config | grep cluster