skip to content
slashtec
Table of Contents

Dieser Guide dokumentiert ein Szenario der Migration eines Keycloak Servers inklusive des Realms. Ausgangspunkt ist eine existierende Installation, Ziel ist es zu einer aktuellen Version auf einem neuen Server mit Docker Compose zu gelangen. Eine besondere Situation ist hierbei, dass Zertifikate einer lokalen CA eingebunden werden müssen – sowohl SSL-Zertifikate für die interne abgesicherte Kommunikation als auch das Zertifikat der Domäne.


Voraussetzungen

  • Docker und Docker Compose (v2) installiert
  • TLS-Zertifikat und Private Key, ausgestellt von einer internen CA
  • Root-CA-Zertifikat (und ggf. Intermediate-Zertifikate) als PEM-Dateien
  • Optional: eine Realm-Export-Datei (JSON) für den automatischen Import

Verzeichnisstruktur

Das Projektverzeichnis sollte wie folgt aufgebaut sein. Mit dieser Struktur funktioniert das weiter unten beschriebene Betreiben mit der Docker Compose Datei. Zu beachten ist hierbei, dass dies die Struktur außerhalb der Container ist. In der Compose-Datei können dann die internen Pfade und Mounts zugeordnet werden.

keycloak/
├── docker-compose.yml
├── .env
├── keycloak-config/
│ └── my-realm.json
└── certs/
├── tls/
│ ├── tls.crt
│ └── tls.key
└── ca/
├── root-ca.pem
└── intermediate-ca.pem # optional

Die Trennung von certs/tls/ (Keycloaks eigene Server-Identität) und certs/ca/ (vertrauenswürdige CA-Zertifikate für den Truststore) ist bewusst gewählt, um die beiden Funktionen sauber voneinander abzugrenzen und etwas Klarheit in den Dschungel zu bringen. Besonders nützlich, da es beim ersten Setup leicht den Überblick zu verlieren.


Konfiguration

.env

# === Postgres ===
POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=ein-sicheres-passwort
# === Keycloak Admin ===
KC_BOOTSTRAP_ADMIN_USERNAME=admin
KC_BOOTSTRAP_ADMIN_PASSWORD=ein-anderes-sicheres-passwort
# === Hostname (muss zum TLS-Zertifikat passen!) ===
KC_HOSTNAME=keycloak.example.com

Der KC_HOSTNAME muss exakt dem Common Name oder einem Subject Alternative Name des TLS-Zertifikats entsprechen. Andernfalls schlägt die Zertifikatsvalidierung fehl.

docker-compose.yml

services:
postgres:
image: postgres:17
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
keycloak:
image: quay.io/keycloak/keycloak:26.5.4
command: start --import-realm
volumes:
# Realm-Import
- ./keycloak-config:/opt/keycloak/data/import:ro
# TLS-Zertifikat + Key fuer Keycloak selbst
- ./certs/tls:/opt/keycloak/conf/tls:ro
# CA-Zertifikate fuer den Truststore
- ./certs/ca:/opt/keycloak/conf/trustedca:ro
environment:
# --- Datenbank ---
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
KC_DB_USERNAME: ${POSTGRES_USER}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
# --- Admin Bootstrap ---
KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_BOOTSTRAP_ADMIN_USERNAME}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD}
# --- HTTPS ---
KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/conf/tls/tls.crt
KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/conf/tls/tls.key
KC_HTTPS_PORT: "8443"
# --- HTTP (fuer Health-Checks oder Reverse Proxy) ---
KC_HTTP_ENABLED: "true"
KC_HTTP_PORT: "8080"
# --- Truststore ---
KC_TRUSTSTORE_PATHS: /opt/keycloak/conf/trustedca
# --- Hostname ---
KC_HOSTNAME: ${KC_HOSTNAME}
KC_HOSTNAME_STRICT: "true"
# --- Proxy (falls hinter Reverse Proxy) ---
# KC_PROXY_HEADERS: xforwarded
# --- Logging (bei Problemen hochdrehen) ---
# KC_LOG_LEVEL: DEBUG
ports:
- "8080:8080"
- "8443:8443"
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:

Hinweise zur Compose-Datei:

  • Es wird start (Production Mode) verwendet, nicht start-dev. Nur im Production Mode greifen HTTPS- und Hostname-Einstellungen zuverlässig.
  • Die KC_DB_URL referenziert den Service-Namen postgres direkt, da diese zur Compose-Topologie gehört und nicht in die .env ausgelagert werden sollte.
  • Der PostgreSQL-Port ist bewusst nicht nach außen exponiert. Keycloak erreicht die Datenbank über das interne Docker-Netzwerk.
  • --import-realm liest beim Start alle JSON-Dateien aus /opt/keycloak/data/import. Die Strategie ist IGNORE_EXISTING – existierende Realms werden nicht überschrieben.

Dateiberechtigungen für Zertifikate

Der Keycloak-Container läuft als uid=1000 mit gid=0 (root). Das bedeutet: alle Dateien, die Keycloak lesen muss, brauchen entweder Owner root oder Gruppe root mit Leserecht. Dies war beim ersten Setup eine Fehlerquelle.

Ein häufiges Problem ist, dass der Private Key auf dem Host restriktive Berechtigungen hat (z.B. 640 root:andereradmin), die im Container nicht aufgelöst werden können.

Terminal window
# Korrekte Berechtigungen setzen
sudo chown root:root ./certs/tls/tls.key
sudo chmod 640 ./certs/tls/tls.key
sudo chmod 644 ./certs/tls/tls.crt
sudo chmod 755 ./certs/tls/
sudo chmod 644 ./certs/ca/*.pem

Entscheidend ist, dass die Gruppe root (GID 0) Leserechte auf den Key hat. Nur chown root:1000 zu setzen reicht nicht, weil die primäre Gruppe im Container GID 0 ist, nicht 1000.

Zur Überprüfung der tatsächlichen UID/GID im Container:

Terminal window
docker compose run --rm --entrypoint id keycloak
# Erwartete Ausgabe: uid=1000(keycloak) gid=0(root) groups=0(root)

Erster Start

Terminal window
cd /pfad/zum/keycloak-projekt
# Container starten
docker compose up -d
# Logs beobachten
docker compose logs -f keycloak

Beim ersten Start passiert Folgendes:

  1. PostgreSQL initialisiert die Datenbank
  2. Keycloak wartet auf den Postgres-Healthcheck
  3. Keycloak führt die Datenbank-Migration durch (Schemaerstellung)
  4. Der Realm-Import wird ausgeführt (Log: Realm 'xyz' imported)
  5. Der Bootstrap-Admin wird angelegt
  6. Keycloak startet und ist erreichbar

Das Web-Interface ist dann verfügbar unter https://<KC_HOSTNAME>:8443. Als erstes sollte ein neuer Admin-Account angelegt und der Bootstrap-Admin aus der .env deaktiviert werden.


Troubleshooting

AccessDeniedException auf tls.key

ERROR: Failed to load 'https-trust-store' or 'https-key-' material:
AccessDeniedException /opt/keycloak/conf/tls/tls.key

Die Dateiberechtigungen stimmen nicht. Siehe Abschnitt „Dateiberechtigungen für Zertifikate”. Die häufigste Ursache ist, dass die Dateigruppe nicht mit der Container-GID (0/root) übereinstimmt.

Webinterface nicht erreichbar

Prüfe zunächst den Hostnamen. Bei KC_HOSTNAME_STRICT: "true" antwortet Keycloak nur auf den exakten Hostnamen. Zum Testen kann temporär KC_HOSTNAME_STRICT: "false" gesetzt werden.

JGroups JOIN Timeouts

WARN: JOIN(...) sent to ... timed out (after 2000 ms)

Das sind Warnungen von Infinispan/JDBC_PING, die nach früheren Cluster-Membern suchen. Bei einem Single-Node-Setup sind diese Meldungen harmlos. Nach 10 Versuchen wird Keycloak zum Singleton und läuft normal weiter.

Debugging aktivieren

Temporär in der docker-compose.yml:

KC_LOG_LEVEL: DEBUG

Danach docker compose restart keycloak. Die Log-Ausgabe wird erheblich umfangreicher und zeigt die meisten Probleme im Detail.


Backup

Datenbank-Backup

Terminal window
docker compose exec postgres pg_dump \
-U ${POSTGRES_USER} ${POSTGRES_DB} \
> keycloak_backup_$(date +%Y%m%d).sql

Datenbank-Restore

Terminal window
docker compose exec -T postgres psql \
-U ${POSTGRES_USER} ${POSTGRES_DB} \
< keycloak_backup_YYYYMMDD.sql

Realm-Export über die Admin-CLI

Für einen aktuellen Realm-Export, der auch Secrets und Credentials enthält:

Terminal window
docker compose exec keycloak \
/opt/keycloak/bin/kc.sh export \
--dir /opt/keycloak/data/export \
--realm my-realm \
--users realm_file

Update auf eine neue Keycloak-Version

Die Datenbank-Migration erfolgt automatisch beim Start einer neuen Version. Keycloak erkennt Schemaänderungen und führt die nötigen Migrationsschritte durch.

Vorbereitung

  1. Upgrading Guide lesen: keycloak.org/docs/latest/upgrading – insbesondere die Breaking Changes für jede Zwischenversion prüfen
  2. Datenbank-Backup erstellen (siehe oben)

Durchführung

Terminal window
# 1. Image-Tag in docker-compose.yml anpassen
# z.B. image: quay.io/keycloak/keycloak:26.5.4
# 2. Neues Image herunterladen
docker compose pull
# 3. Container stoppen
docker compose down
# 4. Mit neuem Image starten (DB-Migration läuft automatisch)
docker compose up -d
# 5. Logs prüfen
docker compose logs -f keycloak

Die Reihenfolge ist wichtig: Erst die Compose-Datei ändern und das neue Image pullen, bevor die laufenden Container gestoppt werden. So wird die Downtime minimiert, weil der Image-Download bereits abgeschlossen ist, wenn die Container neu gestartet werden.

Rollback

Falls das Update fehlschlägt:

Terminal window
# Container stoppen
docker compose down
# Image-Tag in docker-compose.yml auf alte Version zurücksetzen
# Datenbank-Backup wiederherstellen (falls Schema bereits migriert wurde)
docker compose up -d postgres
docker compose exec -T postgres psql \
-U ${POSTGRES_USER} ${POSTGRES_DB} \
< keycloak_backup_YYYYMMDD.sql
# Keycloak mit alter Version starten
docker compose up -d

Hinweise zur Absicherung für den Produktivbetrieb

  • Den PostgreSQL-Port niemals ohne Firewall nach außen exponieren
  • Starke, zufällig generierte Passwörter in der .env verwenden
  • Die .env-Datei mit restriktiven Berechtigungen versehen (chmod 600)
  • Einen Reverse Proxy (z.B. nginx, Traefik) vor Keycloak setzen und KC_PROXY_HEADERS: xforwarded aktivieren
  • Regelmäßig Backups der PostgreSQL-Datenbank erstellen
  • Das Keycloak-Image nicht auf latest setzen, sondern immer einen konkreten Versionstag verwenden