Run Keycloak on general01¶
This playbook runs Keycloak and PostgreSQL with Docker Compose on general01 and creates the mcp realm. TLS is terminated at the gateway, so Keycloak itself only speaks HTTP.
Use it to set up (or rebuild) the identity provider before Secure MCP Servers with OAuth.
| Item | Details |
|---|---|
| Stack | Keycloak 26.8.0 PostgreSQL 18 Docker Compose |
| Target | general01 (192.168.10.9, Debian 12) |
| Run from | Fedora 44 KVM host |
| Env | lab |
Lab-only configuration
Keycloak runs in start-dev mode, with the bootstrap admin account and the plain-HTTP listener on 8080/8443 published on all of general01’s interfaces. That is acceptable on the isolated net-lab NAT network. Do not reuse it as is outside the lab.
The configuration lives in the repo under addons/keycloak/; the compose file below is embedded from there, so edit the repo, not this page.
Prerequisites¶
- general01 reachable from the host:
ssh cloud@192.168.10.9 hostname - Docker Engine with the Compose plugin on general01:
docker --version && docker compose version -
jqon the host for the checks:sudo dnf install -y jq - Dynamic DNS done: External-DNS publishes
keycloak.lab.riupie.comfrom the gateway route - The route to Keycloak is in place. It is defined in
gitops-fluxcdunderinfrastructure/overlays/development/configs/keycloak/, not in this repo: aServiceandEndpointSlicepointing at192.168.10.9:8080, and anHTTPRouteforkeycloak.lab.riupie.comon thehttpslistener ofgateway-ai. TLS is a Let’s Encrypt certificate that cert-manager issues with DNS-01 through Cloudflare (ClusterIssuerletsencrypt-production).
Steps¶
1. Prepare the directory on general01¶
On the host:
2. Copy the compose file¶
On the host:
scp addons/keycloak/docker-compose.yaml addons/keycloak/.env.example cloud@192.168.10.9:/tmp/
ssh cloud@192.168.10.9 'sudo install -m 0644 /tmp/docker-compose.yaml /opt/keycloak/docker-compose.yaml \
&& { sudo test -e /opt/keycloak/.env || sudo install -m 0600 /tmp/.env.example /opt/keycloak/.env; } \
&& rm /tmp/docker-compose.yaml /tmp/.env.example'
An existing /opt/keycloak/.env is never overwritten, so this is safe on a rebuild.
services:
keycloak:
container_name: keycloak_app
image: quay.io/keycloak/keycloak:26.8.0
restart: always
ports:
- "8443:8443"
- "8080:8080"
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_BOOTSTRAP_ADMIN_USERNAME}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD}
KC_HOSTNAME: ${KC_HOSTNAME}
KC_DB_URL: jdbc:postgresql://keycloak_postgres:5432/keycloak
KC_DB: postgres
KC_DB_USERNAME: ${POSTGRES_USER}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
KC_HTTP_ENABLED: "true" # TLS is terminated at the gateway
KC_PROXY_HEADERS: xforwarded
depends_on:
- keycloak_postgres
networks:
- keycloak-network
command:
- start-dev # lab only: dev mode, no production hardening
keycloak_postgres:
container_name: keycloak_postgres
image: postgres:18
restart: always
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/
networks:
- keycloak-network
networks:
keycloak-network:
name: keycloak-network
driver: bridge
volumes:
postgres_data:
3. Fill in the credentials¶
On general01. The compose file reads every secret from /opt/keycloak/.env. Generate the two passwords there instead of typing them, so they never enter your shell history:
cd /opt/keycloak
sudo sed -i \
-e "s|^KC_BOOTSTRAP_ADMIN_PASSWORD=.*|KC_BOOTSTRAP_ADMIN_PASSWORD=$(openssl rand -hex 16)|" \
-e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -hex 16)|" .env
sudo cat .env # note the admin password in your password manager
First install only
On a rebuild that keeps the postgres_data volume, skip this step: PostgreSQL ignores a new POSTGRES_PASSWORD once the volume exists, and Keycloak then fails to log in.
KC_HOSTNAME must be the full public URL (https://keycloak.lab.riupie.com). It decides the iss claim that the gateway policy compares in the OAuth playbook.
4. Start Keycloak¶
On general01:
cd /opt/keycloak
docker compose up -d
docker compose ps
docker compose logs keycloak | grep -E 'Keycloak .* started|Listening'
Expected: both containers Up, and a line such as Keycloak 26.8.0 on JVM (...) started in 25.4s. The first start imports the schema into PostgreSQL and takes about a minute.
5. Check the endpoint locally¶
On general01:
curl -s http://127.0.0.1:8080/realms/master/.well-known/openid-configuration | grep -o '"issuer":"[^"]*"'
Expected: "issuer":"https://keycloak.lab.riupie.com/realms/master". The public hostname appears even though you called the HTTP port because of KC_HOSTNAME.
6. Create the mcp realm¶
Admin console, https://keycloak.lab.riupie.com/admin (log in with the bootstrap admin from .env):
- Realm selector (top left) → Manage realms → Create realm → Realm name
mcp, Enabled ON → Create. - Switch back to the
masterrealm → Users → Add user, then Credentials → Set password (Temporary OFF) and Role mapping → Assign role →admin. Log in as that user and delete the bootstrap admin; Keycloak intends the bootstrap account to be temporary.
Continue with the groups, scopes and client registration policies in Secure MCP Servers with OAuth.
Verify¶
On the host:
curl -s https://keycloak.lab.riupie.com/realms/mcp/.well-known/openid-configuration \
| jq '{issuer, jwks_uri, registration_endpoint}'
Expected: issuer is exactly https://keycloak.lab.riupie.com/realms/mcp, and registration_endpoint ends in /realms/mcp/clients-registrations/openid-connect. The certificate is the public Let’s Encrypt one, so curl needs no extra CA.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
https://keycloak.lab.riupie.com returns 404 route not found or times out, while http://192.168.10.9:8080 works |
The gateway route or DNS record is missing | Check the HTTPRoute in gitops-fluxcd and resolvectl query keycloak.lab.riupie.com (Dynamic DNS step 8) |
iss or issuer shows http://…:8080 |
KC_HOSTNAME is a bare hostname, or the gateway does not send X-Forwarded-* |
Set the full https:// URL in .env, then docker compose up -d. Keep KC_PROXY_HEADERS=xforwarded |
keycloak_app exits with password authentication failed |
POSTGRES_PASSWORD changed after the database volume was created |
Restore the old password in .env. To start from an empty database instead, run docker compose down -v (-v removes the postgres_data volume, and with it the mcp realm, users and clients; plain down keeps the volume), then docker compose up -d |
Browser login loops or shows Invalid parameter: redirect_uri |
The client’s redirect URI does not match the URL you open | Open the console through https://keycloak.lab.riupie.com, not the node IP |
Rollback¶
On general01:
This stops Keycloak; the realm, users and registered clients stay in the postgres_data volume. DCR clients and MCP logins stop working until it is started again.