|
| 1 | +# Keycloak Helm Chart |
| 2 | + |
| 3 | +[](https://github.com/KitStream/helms/actions/workflows/ci.yaml) |
| 4 | +[](https://github.com/KitStream/helms/releases) |
| 5 | +[](https://github.com/keycloak/keycloak) |
| 6 | + |
| 7 | +A Helm chart for deploying [Keycloak](https://www.keycloak.org) IAM using the upstream `quay.io/keycloak/keycloak` image on Kubernetes. |
| 8 | + |
| 9 | +## Overview |
| 10 | + |
| 11 | +This chart deploys Keycloak directly from the upstream container image with native `KC_*` environment variable configuration. It supports: |
| 12 | + |
| 13 | +- **Databases**: PostgreSQL, MySQL, MSSQL, or embedded H2 (dev mode) |
| 14 | +- **Clustering**: JDBC-PING (default) or Kubernetes DNS-PING via headless service |
| 15 | +- **Build optimization**: Optional init container for `kc.sh build` to speed up startup |
| 16 | +- **Observability**: Health endpoints on a dedicated management port, Prometheus metrics, and optional ServiceMonitor |
| 17 | + |
| 18 | +## Prerequisites |
| 19 | + |
| 20 | +- Kubernetes 1.24+ |
| 21 | +- Helm 3.x |
| 22 | +- An external database for production use (PostgreSQL recommended) |
| 23 | + |
| 24 | +## Installation |
| 25 | + |
| 26 | +### From OCI Registry (recommended) |
| 27 | + |
| 28 | +```bash |
| 29 | +helm install keycloak oci://ghcr.io/kitstream/helms/keycloak \ |
| 30 | + --version 26.5.0 \ |
| 31 | + -n keycloak --create-namespace \ |
| 32 | + -f my-values.yaml |
| 33 | +``` |
| 34 | + |
| 35 | +### From Source |
| 36 | + |
| 37 | +```bash |
| 38 | +helm install keycloak ./charts/keycloak \ |
| 39 | + -n keycloak --create-namespace \ |
| 40 | + -f my-values.yaml |
| 41 | +``` |
| 42 | + |
| 43 | +## Minimal Configuration Examples |
| 44 | + |
| 45 | +### Development (embedded H2) |
| 46 | + |
| 47 | +No external database required. **Not suitable for production.** |
| 48 | + |
| 49 | +```yaml |
| 50 | +database: |
| 51 | + type: dev |
| 52 | + |
| 53 | +admin: |
| 54 | + username: admin |
| 55 | + password: |
| 56 | + secretName: keycloak-admin-password |
| 57 | + secretKey: password |
| 58 | +``` |
| 59 | +
|
| 60 | +### PostgreSQL (production) |
| 61 | +
|
| 62 | +```yaml |
| 63 | +hostname: keycloak.example.com |
| 64 | +hostnameStrict: true |
| 65 | + |
| 66 | +database: |
| 67 | + type: postgresql |
| 68 | + host: postgres.database.svc.cluster.local |
| 69 | + port: 5432 |
| 70 | + user: keycloak |
| 71 | + name: keycloak |
| 72 | + password: |
| 73 | + secretName: keycloak-db-password |
| 74 | + secretKey: password |
| 75 | + |
| 76 | +admin: |
| 77 | + username: admin |
| 78 | + password: |
| 79 | + secretName: keycloak-admin-password |
| 80 | + secretKey: password |
| 81 | + |
| 82 | +ingress: |
| 83 | + enabled: true |
| 84 | + className: nginx |
| 85 | + hosts: |
| 86 | + - host: keycloak.example.com |
| 87 | + paths: |
| 88 | + - path: / |
| 89 | + pathType: Prefix |
| 90 | + tls: |
| 91 | + - secretName: keycloak-tls |
| 92 | + hosts: |
| 93 | + - keycloak.example.com |
| 94 | +``` |
| 95 | +
|
| 96 | +### Multi-Replica with Kubernetes DNS-PING |
| 97 | +
|
| 98 | +```yaml |
| 99 | +replicaCount: 3 |
| 100 | + |
| 101 | +hostname: keycloak.example.com |
| 102 | +hostnameStrict: true |
| 103 | + |
| 104 | +database: |
| 105 | + type: postgresql |
| 106 | + host: postgres.database.svc.cluster.local |
| 107 | + port: 5432 |
| 108 | + user: keycloak |
| 109 | + name: keycloak |
| 110 | + password: |
| 111 | + secretName: keycloak-db-password |
| 112 | + secretKey: password |
| 113 | + |
| 114 | +cache: |
| 115 | + stack: kubernetes |
| 116 | + |
| 117 | +admin: |
| 118 | + username: admin |
| 119 | + password: |
| 120 | + secretName: keycloak-admin-password |
| 121 | + secretKey: password |
| 122 | +``` |
| 123 | +
|
| 124 | +## Creating Secrets |
| 125 | +
|
| 126 | +### Admin Password |
| 127 | +
|
| 128 | +```bash |
| 129 | +kubectl create secret generic keycloak-admin-password \ |
| 130 | + --from-literal=password='YOUR_SECURE_PASSWORD' \ |
| 131 | + -n keycloak |
| 132 | +``` |
| 133 | + |
| 134 | +### Database Password |
| 135 | + |
| 136 | +```bash |
| 137 | +kubectl create secret generic keycloak-db-password \ |
| 138 | + --from-literal=password='YOUR_DB_PASSWORD' \ |
| 139 | + -n keycloak |
| 140 | +``` |
| 141 | + |
| 142 | +## Hostname Configuration |
| 143 | + |
| 144 | +Keycloak 26 requires explicit hostname configuration for production mode: |
| 145 | + |
| 146 | +- **`hostname`**: Set to your public Keycloak URL (e.g. `keycloak.example.com`) |
| 147 | +- **`hostnameStrict`**: Set to `true` when `hostname` is configured to prevent dynamic hostname resolution from request headers. Defaults to `false` so Keycloak can start without a hostname (dev/testing). |
| 148 | + |
| 149 | +If `hostnameStrict` is `true` and no `hostname` is set, Keycloak will refuse to start. |
| 150 | + |
| 151 | +## Clustering |
| 152 | + |
| 153 | +The chart supports two cache stacks for multi-replica deployments: |
| 154 | + |
| 155 | +| Stack | Description | Default | |
| 156 | +| ------------ | ------------------------------------------------------------------------ | ------- | |
| 157 | +| `jdbc-ping` | Uses the configured database for node discovery. No extra config needed. | Yes | |
| 158 | +| `kubernetes` | Uses DNS-PING via the headless service for JGroups discovery. | No | |
| 159 | + |
| 160 | +In dev mode (`database.type: dev`), clustering is disabled (`KC_CACHE=local`) since the embedded H2 database cannot be shared across replicas. |
| 161 | + |
| 162 | +## Build Optimization |
| 163 | + |
| 164 | +By default, Keycloak runs an auto-build at startup in production mode (`start`), which can take 2-5 minutes. To speed up startup, enable the build init container: |
| 165 | + |
| 166 | +```yaml |
| 167 | +build: |
| 168 | + enabled: true |
| 169 | +``` |
| 170 | +
|
| 171 | +This runs `kc.sh build` in an init container and passes `--optimized` to the main container, reducing startup time to seconds. |
| 172 | + |
| 173 | +## Values Reference |
| 174 | + |
| 175 | +### Global |
| 176 | + |
| 177 | +| Key | Type | Default | Description | |
| 178 | +| ---------------------------- | ------ | ------- | -------------------------------- | |
| 179 | +| `nameOverride` | string | `""` | Override the chart name | |
| 180 | +| `fullnameOverride` | string | `""` | Fully override the resource name | |
| 181 | +| `imagePullSecrets` | list | `[]` | Global image pull secrets | |
| 182 | +| `serviceAccount.create` | bool | `true` | Create a ServiceAccount | |
| 183 | +| `serviceAccount.annotations` | object | `{}` | ServiceAccount annotations | |
| 184 | +| `serviceAccount.name` | string | `""` | ServiceAccount name override | |
| 185 | + |
| 186 | +### Image |
| 187 | + |
| 188 | +| Key | Type | Default | Description | |
| 189 | +| ------------------ | ------ | ----------------------------- | --------------------------- | |
| 190 | +| `image.repository` | string | `"quay.io/keycloak/keycloak"` | Container image repository | |
| 191 | +| `image.tag` | string | `""` (appVersion) | Image tag | |
| 192 | +| `image.pullPolicy` | string | `"IfNotPresent"` | Image pull policy | |
| 193 | +| `replicaCount` | int | `1` | Number of Keycloak replicas | |
| 194 | + |
| 195 | +### Database |
| 196 | + |
| 197 | +| Key | Type | Default | Description | |
| 198 | +| ------------------------------ | ------ | ------------ | ------------------------------------------------------- | |
| 199 | +| `database.type` | string | `"dev"` | Database type (`postgresql`, `mysql`, `mssql`, `dev`) | |
| 200 | +| `database.host` | string | `""` | Database hostname (required for external databases) | |
| 201 | +| `database.port` | string | `""` | Database port (auto-defaults: 5432/3306/1433) | |
| 202 | +| `database.name` | string | `"keycloak"` | Database name | |
| 203 | +| `database.user` | string | `""` | Database username | |
| 204 | +| `database.password.secretName` | string | `""` | Secret containing the database password | |
| 205 | +| `database.password.secretKey` | string | `"password"` | Key in the Secret | |
| 206 | +| `database.poolMinSize` | string | `""` | Connection pool minimum size | |
| 207 | +| `database.poolInitialSize` | string | `""` | Connection pool initial size | |
| 208 | +| `database.poolMaxSize` | string | `""` | Connection pool maximum size | |
| 209 | +| `database.sslMode` | string | `""` | SSL mode for PostgreSQL (e.g. `verify-full`, `require`) | |
| 210 | + |
| 211 | +### Hostname & Proxy |
| 212 | + |
| 213 | +| Key | Type | Default | Description | |
| 214 | +| ---------------- | ------ | ------- | ------------------------------------------------------------------------ | |
| 215 | +| `hostname` | string | `""` | Public hostname (maps to `KC_HOSTNAME`) | |
| 216 | +| `hostnameAdmin` | string | `""` | Separate admin console hostname (maps to `KC_HOSTNAME_ADMIN`) | |
| 217 | +| `hostnameStrict` | bool | `false` | Disable dynamic hostname from request headers (set `true` with hostname) | |
| 218 | +| `proxyHeaders` | string | `""` | Proxy header mode: `xforwarded` or `forwarded` | |
| 219 | +| `httpEnabled` | bool | `true` | Enable HTTP listener (required for edge-terminated TLS) | |
| 220 | + |
| 221 | +### TLS |
| 222 | + |
| 223 | +| Key | Type | Default | Description | |
| 224 | +| ---------------- | ------ | ------- | ------------------------------------------- | |
| 225 | +| `tls.enabled` | bool | `false` | Enable TLS passthrough (mount cert and key) | |
| 226 | +| `tls.secretName` | string | `""` | Secret containing `tls.crt` and `tls.key` | |
| 227 | + |
| 228 | +### Clustering |
| 229 | + |
| 230 | +| Key | Type | Default | Description | |
| 231 | +| ------------- | ------ | ------------- | -------------------------------------------------- | |
| 232 | +| `cache.stack` | string | `"jdbc-ping"` | Cache stack: `jdbc-ping` (default) or `kubernetes` | |
| 233 | + |
| 234 | +### Admin Credentials |
| 235 | + |
| 236 | +| Key | Type | Default | Description | |
| 237 | +| --------------------------- | ------ | ------------ | ----------------------------------------- | |
| 238 | +| `admin.username` | string | `""` | Admin username (maps to `KEYCLOAK_ADMIN`) | |
| 239 | +| `admin.password.secretName` | string | `""` | Secret containing the admin password | |
| 240 | +| `admin.password.secretKey` | string | `"password"` | Key in the Secret | |
| 241 | + |
| 242 | +### Observability |
| 243 | + |
| 244 | +| Key | Type | Default | Description | |
| 245 | +| -------------------------------------- | ------ | -------- | ----------------------------------- | |
| 246 | +| `healthEnabled` | bool | `true` | Enable health endpoints | |
| 247 | +| `metrics.enabled` | bool | `true` | Enable Prometheus metrics | |
| 248 | +| `metrics.serviceMonitor.enabled` | bool | `false` | Create a ServiceMonitor resource | |
| 249 | +| `metrics.serviceMonitor.labels` | object | `{}` | Additional ServiceMonitor labels | |
| 250 | +| `metrics.serviceMonitor.interval` | string | `""` | Scrape interval | |
| 251 | +| `metrics.serviceMonitor.scrapeTimeout` | string | `""` | Scrape timeout | |
| 252 | +| `logLevel` | string | `"info"` | Log level (`debug`, `info`, `warn`) | |
| 253 | + |
| 254 | +### Build Optimization |
| 255 | + |
| 256 | +| Key | Type | Default | Description | |
| 257 | +| --------------- | ---- | ------- | ----------------------------------- | |
| 258 | +| `build.enabled` | bool | `false` | Run `kc.sh build` in init container | |
| 259 | + |
| 260 | +### Extra Configuration |
| 261 | + |
| 262 | +| Key | Type | Default | Description | |
| 263 | +| -------------------- | ------ | ------- | ------------------------------------------- | |
| 264 | +| `extraEnvVars` | list | `[]` | Additional environment variables | |
| 265 | +| `extraEnvVarsSecret` | string | `""` | Existing Secret to mount as env vars | |
| 266 | +| `extraVolumes` | list | `[]` | Additional volumes | |
| 267 | +| `extraVolumeMounts` | list | `[]` | Additional volume mounts | |
| 268 | +| `features` | string | `""` | Comma-separated Keycloak features to enable | |
| 269 | + |
| 270 | +### Persistent Storage |
| 271 | + |
| 272 | +| Key | Type | Default | Description | |
| 273 | +| -------------------------- | ------ | ------------------- | -------------------------------------- | |
| 274 | +| `persistence.enabled` | bool | `false` | Enable PVC for custom themes/providers | |
| 275 | +| `persistence.storageClass` | string | `""` | Storage class | |
| 276 | +| `persistence.accessModes` | list | `["ReadWriteOnce"]` | PVC access modes | |
| 277 | +| `persistence.size` | string | `"1Gi"` | Volume size | |
| 278 | +| `persistence.annotations` | object | `{}` | PVC annotations | |
| 279 | + |
| 280 | +### Services |
| 281 | + |
| 282 | +| Key | Type | Default | Description | |
| 283 | +| ----------------------------- | ------ | ------------- | ---------------------------------- | |
| 284 | +| `service.type` | string | `"ClusterIP"` | Service type | |
| 285 | +| `service.httpPort` | int | `8080` | HTTP port | |
| 286 | +| `service.managementPort` | int | `9000` | Management port (health + metrics) | |
| 287 | +| `service.annotations` | object | `{}` | Service annotations | |
| 288 | +| `headlessService.jgroupsPort` | int | `7800` | JGroups clustering port | |
| 289 | +| `headlessService.annotations` | object | `{}` | Headless service annotations | |
| 290 | + |
| 291 | +### Ingress |
| 292 | + |
| 293 | +| Key | Type | Default | Description | |
| 294 | +| --------------------- | ------ | ------- | ----------------------- | |
| 295 | +| `ingress.enabled` | bool | `false` | Create Ingress resource | |
| 296 | +| `ingress.className` | string | `""` | Ingress class name | |
| 297 | +| `ingress.annotations` | object | `{}` | Ingress annotations | |
| 298 | +| `ingress.hosts` | list | `[]` | Ingress host rules | |
| 299 | +| `ingress.tls` | list | `[]` | TLS configuration | |
| 300 | + |
| 301 | +### Probes |
| 302 | + |
| 303 | +| Key | Type | Default | Description | |
| 304 | +| ---------------- | ------ | --------------------------------------------- | --------------- | |
| 305 | +| `startupProbe` | object | HTTP GET `/health/started` on management:9000 | Startup probe | |
| 306 | +| `livenessProbe` | object | HTTP GET `/health/live` on management:9000 | Liveness probe | |
| 307 | +| `readinessProbe` | object | HTTP GET `/health/ready` on management:9000 | Readiness probe | |
| 308 | + |
| 309 | +### Pod Scheduling & Metadata |
| 310 | + |
| 311 | +| Key | Type | Default | Description | |
| 312 | +| -------------------- | ------ | ------- | -------------------------------- | |
| 313 | +| `resources` | object | `{}` | CPU/memory requests and limits | |
| 314 | +| `nodeSelector` | object | `{}` | Node selector labels | |
| 315 | +| `tolerations` | list | `[]` | Pod tolerations | |
| 316 | +| `affinity` | object | `{}` | Pod affinity rules | |
| 317 | +| `podAnnotations` | object | `{}` | Pod annotations | |
| 318 | +| `podLabels` | object | `{}` | Additional pod labels | |
| 319 | +| `podSecurityContext` | object | `{}` | Pod-level security context | |
| 320 | +| `securityContext` | object | `{}` | Container-level security context | |
| 321 | + |
| 322 | +## Architecture |
| 323 | + |
| 324 | +``` |
| 325 | +┌──────────────────────────────────────────────────────┐ |
| 326 | +│ Ingress Controller │ |
| 327 | +│ │ |
| 328 | +│ / ────────────────────────► Keycloak Pod │ |
| 329 | +│ ├─ :8080 HTTP │ |
| 330 | +│ ├─ :9000 Management │ |
| 331 | +│ │ (health + metrics) │ |
| 332 | +│ └─ :7800 JGroups │ |
| 333 | +│ (clustering) │ |
| 334 | +└──────────────────────────────────────────────────────┘ |
| 335 | + │ │ |
| 336 | + Headless Service Main Service |
| 337 | + (JGroups DNS-PING) (HTTP + Management) |
| 338 | +``` |
| 339 | +
|
| 340 | +## Upstream Source |
| 341 | +
|
| 342 | +This chart deploys the upstream [Keycloak](https://github.com/keycloak/keycloak) image from `quay.io/keycloak/keycloak`. See the `sources` field in `Chart.yaml` for details. |
| 343 | +
|
| 344 | +## License |
| 345 | +
|
| 346 | +Apache License 2.0 — see [LICENSE](../../LICENSE). |
0 commit comments