Keycloak¶
dev.rightsize.modules.KeycloakContainer — a single-node Keycloak container in
start-dev mode (in-memory H2, no external database — fine for tests, never for
production).
Defaults¶
| Default image | quay.io/keycloak/keycloak:latest — this image's floating reference (see below) |
| Exposed ports | 8080 (HTTP / auth server), 9000 (management — health lives here, see below) |
| Command | start-dev |
| Env | KC_BOOTSTRAP_ADMIN_USERNAME=admin, KC_BOOTSTRAP_ADMIN_PASSWORD=admin, KC_HEALTH_ENABLED=true |
| Memory limit | withMemoryLimit(1024) — see below |
| Wait strategy | Wait.forHttp("/health").forPort(9000).withStartupTimeout(Duration.ofSeconds(180)) |
With no image given, this module tracks upstream's latest tag rather than a version
this library pins, so the version moves with Keycloak's own releases instead of this
library's release cycle. Every fact below — the admin bootstrap env names, the
management-port health path, the memory ladder — was verified against
quay.io/keycloak/keycloak:26.4 specifically; pass that image explicitly to pin it:
The compatibility check strips the quay.io registry host and expects the repository
keycloak/keycloak.
Helpers¶
| Member | Returns |
|---|---|
authServerUrl: String |
The auth server's base URI (HTTP port) — realm/OIDC endpoints live under this |
managementUrl: String |
The management interface's base URI (health/metrics — port 9000, a different port than authServerUrl) |
adminUsername / adminPassword: String |
The configured bootstrap admin credentials (default admin/admin) |
withAdminUsername(username: String): KeycloakContainer |
Overrides KC_BOOTSTRAP_ADMIN_USERNAME |
withAdminPassword(password: String): KeycloakContainer |
Overrides KC_BOOTSTRAP_ADMIN_PASSWORD |
Call the withX overrides before start().
Example¶
package dev.rightsize.modules
import dev.rightsize.modules.KeycloakContainer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
class KeycloakContainerTest {
private val http = HttpClient.newHttpClient()
@Test
fun `OIDC discovery document asserts the issuer`() {
val keycloak = KeycloakContainer()
keycloak.start()
try {
val resp = http.send(
HttpRequest.newBuilder(
URI("${keycloak.authServerUrl}/realms/master/.well-known/openid-configuration"),
).GET().build(),
HttpResponse.BodyHandlers.ofString(),
)
assertEquals(200, resp.statusCode(), "discovery document fetch failed: ${resp.body()}")
assertTrue(
resp.body().contains("\"issuer\":\"${keycloak.authServerUrl}/realms/master\""),
"unexpected issuer in discovery document: ${resp.body()}",
)
} finally {
keycloak.stop()
}
}
}
Backend notes¶
Admin bootstrap env names changed in 26.x — verified against the pinned tag.
Keycloak 26.x replaced the older KEYCLOAK_ADMIN/KEYCLOAK_ADMIN_PASSWORD pair with
KC_BOOTSTRAP_ADMIN_USERNAME/KC_BOOTSTRAP_ADMIN_PASSWORD. Verified directly against
quay.io/keycloak/keycloak:26.4: booting with only the new names produces
KC-SERVICES0077: Created temporary admin user with username admin in the log, and
the legacy names are not read by this image at all. If you're overriding the image to
an older tag, check which pair it actually reads before assuming these overrides
apply.
Health lives on the management port (9000), not the HTTP port (8080) — and the path
is /health, not /health/ready. Captured verbatim from a real boot:
With KC_HEALTH_ENABLED=true set, GET /health on port 9000 returns 200 OK
(literal body OK). The same path on 8080 404s, and the commonly assumed
/health/ready sub-path also 404s on this tag — this image's SmallRye Health root
aggregate is served bare at /health, not under a /ready sub-path. This module's
wait strategy is pinned to /health on 9000 accordingly.
Memory needed the same ladder as Spring Cloud Config's Paketo JVM. Booted under
microsandbox's default ~450 MB microVM RAM, the Quarkus JVM gets OOM-killed partway
through startup (captured: 'java' ... -XX:MaxRAMPercentage=70 ... Killed) — the same
Paketo/Quarkus-on-microVM story as
Spring Cloud Config. Retried with -m 1024M:
boots clean, /health reports 200 well within the startup timeout. This module
ships with withMemoryLimit(1024) for exactly this reason.
Compatibility checking¶
Passing an explicit image checks its repository — with any registry host stripped —
against the one this module understands (keycloak/keycloak) before any port,
wait-strategy, or backend work runs — a mismatched image fails fast with a typed
IncompatibleImageException naming both repositories, rather than degrading into a
bare wait-strategy timeout. To use a differently-named image on purpose (a private
mirror, a hardened rebuild), wrap it with the escape hatch:
KeycloakContainer(
DockerImageName.parse("mycorp/keycloak-hardened:26.4")
.asCompatibleSubstituteFor("keycloak/keycloak"))
See Core Concepts for DockerImageName itself.