Keycloak (OIDC)
This guide walks through running DiscoPanel with Keycloak as an OIDC identity provider using a ready-made Docker Compose stack. The included realm config pre-creates a client, roles, groups, protocol mappers, and a default admin user so everything works out of the box.
Prerequisites
Section titled “Prerequisites”- Docker and Docker Compose
Docker Compose
Section titled “Docker Compose”Clone the repo and navigate to the oidc/keycloak/ directory, then start the stack:
cd oidc/keycloakdocker compose up -dKeycloak takes 30–60 seconds to start. DiscoPanel waits for it via a health check.
# DiscoPanel + Keycloak (OIDC)## This is a complete docker-compose with OIDC authentication pre-configured using Keycloak.## Keycloak takes ~30-60 seconds to start. DiscoPanel waits for it.## Keycloak Admin UI (See KC_BOOTSTRAP_ADMIN_USERNAME/KC_BOOTSTRAP_ADMIN_PASSWORD below): http://localhost:8180/admin
services: discopanel: build: context: ../../ dockerfile: docker/Dockerfile.discopanel #image: nickheyer/discopanel:dev container_name: discopanel restart: unless-stopped network_mode: host volumes: - /var/run/docker.sock:/var/run/docker.sock - /tmp/discopanel:/app/data environment: - DISCOPANEL_DATA_DIR=/app/data - DISCOPANEL_HOST_DATA_PATH=/tmp/discopanel - TZ=UTC
# ------------------------------------ AUTH CONFIG STARTS HERE FOR DISCOPANEL + KEYCLOAK ------------------------------------ - DISCOPANEL_AUTH_LOCAL_ENABLED=true - DISCOPANEL_AUTH_OIDC_ENABLED=true
# ONLY CHANGE THIS IF YOU ARE HOSTING OIDC ON ANOTHER MACHINE OR WITH A DIFFERENT "realm" NAME - DISCOPANEL_AUTH_OIDC_ISSUER_URI=http://localhost:8180/realms/discopanel
# ONLY CHANGE THIS IF YOUR CLIENT NAME IS DIFFERENT - DISCOPANEL_AUTH_OIDC_CLIENT_ID=discopanel
# YOU SHOULD CHANGE THIS HERE AS WELL AS IN oidc/keycloak/config/realm.json (inside the clients object) - DISCOPANEL_AUTH_OIDC_CLIENT_SECRET=discopanel-dev-secret
# YOU SHOULD CHANGE "localhost:8080" TO WHATEVER YOUR PUBLIC DOMAIN IS FOR DISCOPANEL (ie: https://mypanel.com/api/v1/auth/oidc/callback) - DISCOPANEL_AUTH_OIDC_REDIRECT_URL=http://localhost:8080/api/v1/auth/oidc/callback
- DISCOPANEL_AUTH_OIDC_ROLE_CLAIM=groups
depends_on: keycloak: condition: service_healthy
keycloak: image: quay.io/keycloak/keycloak:26.1 container_name: keycloak command: start-dev --import-realm volumes: # THIS IS AN EXAMPLE REALM CONFIG, MODIFY AS NEEDED (WHICH MAY REQUIRE MODIFYING DISCOPANEL CONFIG TO MATCH, SEE ABOVE) - ./config/realm.json:/opt/keycloak/data/import/realm.json:ro environment: # KEYCLOAK ADMIN LOGIN CREDENTIALS - CHANGE THESE!!!!! - KC_BOOTSTRAP_ADMIN_USERNAME=admin - KC_BOOTSTRAP_ADMIN_PASSWORD=admin
# KEYCLOAK DATABASE CONFIG, DONT CHANGE THESE UNLESS YOU KNOW WHAT YOU ARE DOING OR USING EXISTING DATABASE - KC_DB=postgres - KC_DB_URL_HOST=keycloak-db - KC_DB_URL_DATABASE=keycloak - KC_DB_USERNAME=keycloak - KC_DB_PASSWORD=keycloak
# MISC KEYCLOAK CONFIGS - KC_HOSTNAME_STRICT=false - KC_HTTP_ENABLED=true - KC_HEALTH_ENABLED=true - KC_PROXY_HEADERS=xforwarded ports: - "8180:8080" healthcheck: test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3; timeout 2 cat <&3 | grep -q '200 OK'"] interval: 20s timeout: 5s retries: 12 start_period: 30s depends_on: keycloak-db: condition: service_healthy
keycloak-db: image: postgres:17-alpine container_name: keycloak-db environment: - POSTGRES_DB=keycloak - POSTGRES_USER=keycloak - POSTGRES_PASSWORD=keycloak volumes: - keycloak-db-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U keycloak"] interval: 10s timeout: 3s retries: 5
volumes: keycloak-db-data:Key environment variables
Section titled “Key environment variables”| Variable | Purpose |
|---|---|
DISCOPANEL_AUTH_OIDC_ENABLED | Enables OIDC authentication |
DISCOPANEL_AUTH_OIDC_ISSUER_URI | Keycloak realm URL — change if hosting on another machine or using a different realm name |
DISCOPANEL_AUTH_OIDC_CLIENT_ID | Must match the clientId in the realm config |
DISCOPANEL_AUTH_OIDC_CLIENT_SECRET | Must match the secret in the realm config — change this for production |
DISCOPANEL_AUTH_OIDC_REDIRECT_URL | The callback URL — update localhost:8080 to your public domain |
DISCOPANEL_AUTH_OIDC_ROLE_CLAIM | Set to groups to read Keycloak group membership as DiscoPanel roles |
Realm configuration
Section titled “Realm configuration”The included realm JSON is imported automatically on first start via --import-realm. It configures:
- Client (
discopanel): confidential client with standard (authorization code) flow - Roles:
adminanduserrealm roles - Groups:
adminandusergroups mapped to their respective roles — new users are added to theusergroup by default - Protocol mappers: a
groupsmapper that puts group membership into thegroupsclaim on all tokens, and arealm-rolesmapper for therolesclaim - Default user:
admin/adminwith bothadminandusergroups
{ "realm": "discopanel", "enabled": true, "registrationAllowed": false, "resetPasswordAllowed": true, "loginWithEmailAllowed": true, "duplicateEmailsAllowed": false, "sslRequired": "none", "roles": { "realm": [ { "name": "admin", "description": "DiscoPanel administrator" }, { "name": "user", "description": "DiscoPanel user" } ] }, "groups": [ { "name": "admin", "realmRoles": ["admin"] }, { "name": "user", "realmRoles": ["user"] } ], "defaultGroups": ["user"], "clients": [ { "clientId": "discopanel", "enabled": true, "protocol": "openid-connect", "publicClient": false, "secret": "discopanel-dev-secret", "standardFlowEnabled": true, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": false, "redirectUris": [ "http://localhost:8080/*", "http://localhost:5173/*" ], "webOrigins": [ "http://localhost:8080", "http://localhost:5173" ], "defaultClientScopes": [ "openid", "profile", "email", "roles" ], "protocolMappers": [ { "name": "groups", "protocol": "openid-connect", "protocolMapper": "oidc-group-membership-mapper", "config": { "full.path": "false", "introspection.token.claim": "true", "userinfo.token.claim": "true", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "groups", "jsonType.label": "String" } }, { "name": "realm-roles", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-realm-role-mapper", "config": { "introspection.token.claim": "true", "userinfo.token.claim": "true", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "roles", "jsonType.label": "String", "multivalued": "true" } } ] } ], "users": [ { "username": "admin", "enabled": true, "emailVerified": true, "firstName": "Disco", "lastName": "Admin", "credentials": [ { "type": "password", "value": "admin", "temporary": false } ], "groups": ["admin", "user"] } ]}Default credentials
Section titled “Default credentials”| Service | URL | Username | Password |
|---|---|---|---|
| DiscoPanel | http://localhost:8080 | — | Log in via OIDC |
| Keycloak Admin Console | http://localhost:8180/admin | admin | admin |
| Keycloak OIDC user | — | admin | admin |
Production notes
Section titled “Production notes”- Change all secrets:
DISCOPANEL_AUTH_OIDC_CLIENT_SECRET,KC_BOOTSTRAP_ADMIN_PASSWORD, the clientsecretin realm.json, and the Postgres password - Enable TLS: put Keycloak behind a reverse proxy with a real certificate and update
DISCOPANEL_AUTH_OIDC_ISSUER_URItohttps:// - Update redirect URIs: change
localhostentries in both the compose file and realm.json to your actual domain - Disable local auth (optional): set
DISCOPANEL_AUTH_LOCAL_ENABLED=falseif you want OIDC-only login