Skip to Content

Keycloak Docker Setup Guide: User Federation, Custom Themes, Fine-Grained Authorization, and High Availability

Go beyond basic Keycloak deployment — learn how to sync users from LDAP/Active Directory, build branded login themes, implement fine-grained resource authorization, and run Keycloak in a highly available cluster behind a load balancer.
Keycloak setup guide

Keycloak Docker Setup Guide: User Federation, Custom Themes, Fine-Grained Authorization, and High Availability

The first Keycloak guide covered the fundamentals — deployment, realms, clients, and basic OAuth2/OIDC flows. This guide goes deeper into the capabilities that make Keycloak genuinely enterprise-grade: federating users from your existing Active Directory or LDAP directory so people log in with their corporate credentials, building custom login themes that match your product's brand, implementing fine-grained resource-level authorization that controls access beyond simple roles, and running Keycloak in a highly available cluster that survives node failures without authentication outages.

If you haven't deployed Keycloak yet, start with our Keycloak Docker setup guide which covers basic deployment, realm configuration, client setup, and OIDC integration. This guide picks up from a running Keycloak instance.


Prerequisites

  • A running Keycloak instance with PostgreSQL — see our getting started guide
  • Keycloak 24+ — this guide uses the Quarkus-based distribution
  • Admin access to the Keycloak admin console and Docker host
  • For LDAP federation: access to an LDAP/Active Directory server and service account credentials
  • For clustering: at least two server instances and a load balancer (Nginx or HAProxy)

Confirm your Keycloak version and current status:

# Check Keycloak version:
docker exec keycloak /opt/keycloak/bin/kc.sh --version

# Check health endpoint:
curl https://auth.yourdomain.com/health/ready
# Should return: {"status": "UP"}

# Check active sessions and realm stats:
curl -X POST https://auth.yourdomain.com/realms/master/protocol/openid-connect/token \
  -d 'client_id=admin-cli&grant_type=password&username=admin&password=YOUR_PASS' | \
  jq -r .access_token | \
  xargs -I{} curl -s https://auth.yourdomain.com/admin/realms \
    -H 'Authorization: Bearer {}' | jq '[.[] | {realm: .realm, users: .usersCount}]'

LDAP and Active Directory User Federation

Most enterprise teams already have users in Active Directory or an LDAP directory. Keycloak's user federation lets people authenticate with their existing credentials without migrating accounts — Keycloak validates credentials against LDAP at login time and syncs attributes and group memberships.

Configuring the LDAP Federation Provider

In the Keycloak admin console for your realm, go to User Federation → Add provider → LDAP. The critical settings:

# LDAP Federation settings (configure in UI or via kcadm.sh):

# For Active Directory:
Vendor: Active Directory
Connection URL: ldap://ad.yourdomain.com:389
# Or LDAPS (secure):
Connection URL: ldaps://ad.yourdomain.com:636
Enable StartTLS: Off (use LDAPS URL instead)

Bind DN: cn=keycloak-svc,ou=service-accounts,dc=yourdomain,dc=com
Bind Credential: service-account-password

Users DN: ou=users,dc=yourdomain,dc=com
Username LDAP attribute: sAMAccountName
RDN LDAP attribute: cn
UUID LDAP attribute: objectGUID
User Object Classes: person, organizationalPerson, user

Search scope: Subtree
Read timeout: 10000ms
Periodic Full Sync: On
Full Sync Period: 86400 (24 hours)
Periodic Changed Users Sync: On
Changed Users Sync Period: 300 (5 minutes)

# For OpenLDAP:
Vendor: Other
Username LDAP attribute: uid
UUID LDAP attribute: entryUUID
User Object Classes: inetOrgPerson, organizationalPerson

# Test the connection before saving:
# Click "Test connection" → should show "LDAP connection successful"
# Click "Test authentication" → enter the Bind DN and password to verify

Configuring LDAP Group Synchronization

# After saving the LDAP provider, add a Group mapper:
# User Federation → [your LDAP] → Mappers → Add mapper

# Group mapper settings:
Name: ldap-groups
Mapper Type: group-ldap-mapper
LDAP Groups DN: ou=groups,dc=yourdomain,dc=com
Group Name LDAP Attribute: cn
Group Object Classes: group  # For AD; use groupOfNames for OpenLDAP
Membership LDAP Attribute: member
Membership Attribute Type: DN
Membership User LDAP Attribute: distinguishedName
Groups LDAP filter: (|(cn=app-*)(cn=admin-*))
# Filter to only sync relevant groups, not all of AD
Mode: READ_ONLY  # Groups managed in AD, not Keycloak
User Groups Retrieve Strategy: LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY
Drop non-existing groups during sync: On

# Trigger manual sync to verify:
# User Federation → [your LDAP] → Synchronize all users
# Then check: Users → search for an AD user → verify attributes populated

# Also verify groups synced:
curl -s "https://auth.yourdomain.com/admin/realms/myapp/groups" \
  -H 'Authorization: Bearer $ADMIN_TOKEN' | jq '[.[] | .name]'

Attribute Mappers for User Data

# Add attribute mappers to pull AD attributes into Keycloak user profiles
# User Federation → [LDAP] → Mappers → Add mapper

# Mapper 1: Department
Name: department
Mapper Type: user-attribute-ldap-mapper
User Model Attribute: department
LDAP Attribute: department
Read Only: true
Always Read Value From LDAP: true

# Mapper 2: Display Name
Name: fullName
Mapper Type: full-name-ldap-mapper
LDAP Full Name Attribute: displayName
Read Only: true

# Mapper 3: Email
Name: email
Mapper Type: user-attribute-ldap-mapper
User Model Attribute: email
LDAP Attribute: mail
Read Only: true

# Mapper 4: Phone
Name: phone
Mapper Type: user-attribute-ldap-mapper
User Model Attribute: phoneNumber
LDAP Attribute: telephoneNumber
Read Only: false  # Allow users to update in Keycloak

# Make department available in tokens:
# Client → [your client] → Client scopes → Add mapper
# Mapper Type: User Attribute
# User Attribute: department
# Token Claim Name: department
# Claim JSON Type: String
# Add to ID token: true
# Add to access token: true

Custom Login Themes

The default Keycloak login page screams "off-the-shelf." For any user-facing product, you want authentication to feel like part of your product — your logo, your colors, your typography. Keycloak's theming system supports full customization of every screen without patching Keycloak itself.

Theme Structure

# Create your custom theme directory:
mkdir -p themes/myapp/login

# Keycloak theme directory structure:
themes/
└── myapp/                    # Your theme name
    └── login/                # Login-related pages
        ├── theme.properties  # Tells Keycloak about your theme
        ├── resources/
        │   ├── css/
        │   │   └── login.css # Custom styles
        │   ├── img/
        │   │   └── logo.svg  # Your logo
        │   └── js/
        │       └── login.js  # Optional JS
        └── templates/
            └── login.ftl     # FreeMarker template for the login form

# theme.properties:
cat > themes/myapp/login/theme.properties << 'EOF'
parent=keycloak   # Inherit from base theme
import=common/keycloak

styles=css/login.css
scripts=js/login.js

# Override specific built-in styles:
styles-common=

logoUrl=/resources/myapp/login/img/logo.svg
EOF

# Mount your theme in Docker Compose:
# Add to Keycloak service volumes:
# - ./themes/myapp:/opt/keycloak/themes/myapp

# Apply the theme:
# Admin Console → Realm Settings → Themes → Login Theme → select "myapp"

Custom Login Template

# themes/myapp/login/templates/login.ftl
# FreeMarker template for the login form
# Copy from Keycloak source and modify:

cat > themes/myapp/login/templates/login.ftl << 'TEMPLATE'
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
    <#if section = "header">
        <#-- Custom header with your brand -->
        
${msg("loginAccountTitle")} <#elseif section = "form">
<#if realm.password>
#if>
#if> @layout.registrationLayout> TEMPLATE # Custom CSS that overrides Keycloak's default styling: cat > themes/myapp/login/resources/css/login.css << 'CSS' /* Your brand colors */ :root { --brand-primary: #6366f1; --brand-secondary: #4f46e5; --bg-color: #0f172a; --card-bg: #1e293b; --text-color: #e2e8f0; } body { background-color: var(--bg-color); font-family: 'Inter', sans-serif; } .login-pf-page { background: var(--bg-color); } .card-pf { background: var(--card-bg); border: 1px solid #334155; border-radius: 12px; padding: 2rem; } .btn-primary { background-color: var(--brand-primary); border-color: var(--brand-primary); border-radius: 6px; font-weight: 600; } .btn-primary:hover { background-color: var(--brand-secondary); border-color: var(--brand-secondary); } .brand-logo { max-width: 180px; margin-bottom: 1.5rem; } CSS

Fine-Grained Authorization with Keycloak Authorization Services

Role-based access control (RBAC) works for coarse-grained permissions — "admins can do X, users can do Y." But many applications need finer control: "users can edit their own resources, managers can edit resources owned by their team, but only for resources in their department." Keycloak's Authorization Services handles this without pushing the logic into your application code.

Enabling Authorization for a Client

# Enable Authorization Services on a client:
# Clients → [your client] → Settings
# Client authentication: On
# Authorization: On
# Save

# This adds an "Authorization" tab to the client
# The authorization model consists of:
# 1. Resources — what you're protecting (files, APIs, documents)
# 2. Scopes — what actions can be performed (read, write, delete)
# 3. Policies — who can do what (role-based, user-based, time-based, JS-based)
# 4. Permissions — tie resources+scopes to policies

# Create a Resource via API (kcadm.sh):
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh \
  create clients/CLIENT_ID/authz/resource-server/resource \
  --server https://auth.yourdomain.com \
  --realm myapp \
  --user admin \
  -s name=document \
  -s 'displayName="Document Resource"' \
  -s 'uris=["/api/documents/*"]' \
  -s 'scopes=[{"name":"read"},{"name":"write"},{"name":"delete"}]'

# Create a Role Policy — users with the 'document-admin' role can do everything:
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh \
  create clients/CLIENT_ID/authz/resource-server/policy/role \
  --server https://auth.yourdomain.com \
  --realm myapp \
  --user admin \
  -s name=document-admin-policy \
  -s 'roles=[{"id":"ROLE_ID","required":true}]'

# Create a Permission — bind the resource and policy:
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh \
  create clients/CLIENT_ID/authz/resource-server/permission/resource \
  --server https://auth.yourdomain.com \
  --realm myapp \
  --user admin \
  -s name=document-admin-permission \
  -s 'resources=["RESOURCE_ID"]' \
  -s 'policies=["POLICY_ID"]' \
  -s decisionStrategy=UNANIMOUS

JavaScript Authorization Policies for Complex Rules

# JavaScript policies let you write custom authorization logic
# Enable in Keycloak: Realm Settings → General → Decision Strategy
# Note: JS policies require --features=preview or explicit enablement in Keycloak 24+

# Example: users can only access documents in their department
# Create a JS policy:
# Clients → [client] → Authorization → Policies → Create policy → JavaScript

# Policy Name: same-department-policy
# Code:

var context = $evaluation.getContext();
var identity = context.getIdentity();
var permission = $evaluation.getPermission();

// Get the requesting user's department from their token
var userDepartment = identity.getAttributes().getValue('department');

// Get the document's department from the resource attributes
var resource = permission.getResource();
var resourceDepartment = resource.getAttribute('department');

if (userDepartment !== null && resourceDepartment !== null) {
    if (userDepartment.asString(0) === resourceDepartment.asString(0)) {
        $evaluation.grant();
    } else {
        $evaluation.deny();
    }
} else {
    // No department info — deny by default
    $evaluation.deny();
}

# Evaluating authorization from your backend:
curl -X POST https://auth.yourdomain.com/realms/myapp/protocol/openid-connect/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=urn:ietf:params:oauth:grant-type:uma-ticket' \
  -d 'client_id=my-backend-client' \
  -d 'client_secret=CLIENT_SECRET' \
  -d 'token=USER_ACCESS_TOKEN' \
  -d 'permission=document#read' \
  -d 'response_mode=decision' | jq .result
# Returns: true = access granted, false = denied

High Availability Clustering

A single Keycloak instance is a single point of failure for authentication. If it goes down, no one can log in. For production systems where authentication availability is non-negotiable, Keycloak must run in a cluster with at least two nodes and a shared session store.

Clustered Docker Compose Configuration

# docker-compose.yml for a 2-node Keycloak cluster
version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: keycloak_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - keycloak_net

  keycloak_node1:
    image: quay.io/keycloak/keycloak:24.0
    container_name: keycloak_node1
    restart: unless-stopped
    command: start
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
      KC_HOSTNAME: auth.yourdomain.com
      KC_HTTP_ENABLED: "true"
      KC_HOSTNAME_STRICT: "false"
      KC_PROXY: edge
      # Clustering configuration:
      KC_CACHE: ispn             # Infinispan distributed cache
      KC_CACHE_STACK: kubernetes # Or: tcp for Docker Compose discovery
      JAVA_OPTS_APPEND: >-
        -Djgroups.dns.query=keycloak_cluster
        -Djgroups.bind_addr=match-interface:eth0
      # Node-specific settings:
      KC_HEALTH_ENABLED: "true"
    ports:
      - "8080:8080"
    depends_on:
      - postgres
    networks:
      keycloak_net:
        aliases:
          - keycloak_cluster  # DNS alias for JGroups cluster discovery

  keycloak_node2:
    image: quay.io/keycloak/keycloak:24.0
    container_name: keycloak_node2
    restart: unless-stopped
    command: start
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
      KC_HOSTNAME: auth.yourdomain.com
      KC_HTTP_ENABLED: "true"
      KC_HOSTNAME_STRICT: "false"
      KC_PROXY: edge
      KC_CACHE: ispn
      KC_CACHE_STACK: kubernetes
      JAVA_OPTS_APPEND: >-
        -Djgroups.dns.query=keycloak_cluster
        -Djgroups.bind_addr=match-interface:eth0
      KC_HEALTH_ENABLED: "true"
    ports:
      - "8081:8080"
    depends_on:
      - postgres
    networks:
      keycloak_net:
        aliases:
          - keycloak_cluster

volumes:
  postgres_data:

networks:
  keycloak_net:

Load Balancer Configuration for the Cluster

# Nginx load balancer for Keycloak cluster
# Sticky sessions required — Keycloak sessions are node-local by default
# unless using distributed cache (Infinispan), in which case sticky sessions
# improve performance but aren't required for correctness

upstream keycloak_cluster {
    # ip_hash provides sticky sessions based on client IP
    ip_hash;

    server localhost:8080 max_fails=3 fail_timeout=30s;
    server localhost:8081 max_fails=3 fail_timeout=30s;

    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name auth.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/auth.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Health check endpoint — use for monitoring
    location /health/ready {
        proxy_pass http://keycloak_cluster;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        access_log off;  # Don't log health checks
    }

    location / {
        proxy_pass http://keycloak_cluster;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Large token responses and JWKS payloads
        proxy_buffer_size 256k;
        proxy_buffers 4 512k;
        proxy_busy_buffers_size 512k;

        proxy_read_timeout 90s;
    }
}

# Verify cluster formation:
docker logs keycloak_node1 2>&1 | grep -i 'cluster\|members\|jgroups'
# Look for: Keycloak 24.0 clustered
# And: ISPN000094: Received new cluster view ... [keycloak_node1, keycloak_node2]

sudo nginx -t && sudo systemctl reload nginx

Production Hardening and Operations

Keycloak Security Hardening Checklist

# Security settings to configure via admin console and kcadm:

# 1. Brute force protection:
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh update realms/myapp \
  --server https://auth.yourdomain.com --realm master --user admin \
  -s 'bruteForceProtected=true' \
  -s 'failureFactor=5' \
  -s 'waitIncrementSeconds=60' \
  -s 'maxFailureWaitSeconds=900' \
  -s 'maxDeltaTimeSeconds=3600' \
  -s 'quickLoginCheckMilliSeconds=1000'

# 2. Session timeouts:
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh update realms/myapp \
  --server https://auth.yourdomain.com --realm master --user admin \
  -s 'accessTokenLifespan=300' \
  -s 'ssoSessionIdleTimeout=1800' \
  -s 'ssoSessionMaxLifespan=36000' \
  -s 'offlineSessionIdleTimeout=2592000'

# 3. Password policy:
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh update realms/myapp \
  --server https://auth.yourdomain.com --realm master --user admin \
  -s 'passwordPolicy="length(12) and upperCase(1) and lowerCase(1) and digits(1) and specialChars(1) and notUsername and passwordHistory(5)"'

# 4. Require MFA for admins:
# Realm Settings → Authentication → Required Actions → TOTP → Set as Default Action
# Or scope it to admin users only via Authentication flows

# 5. Disable user self-registration if not needed:
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh update realms/myapp \
  --server https://auth.yourdomain.com --realm master --user admin \
  -s registrationAllowed=false

# 6. Verify master realm is locked down:
# Master realm should have NO regular users — only admin accounts
# No clients should use master realm except kcadm internal operations

# 7. Rotate the realm keys periodically:
# Realm Settings → Keys → Add provider → RSA
# Set Priority higher than current key → Old key remains for token verification
# After enough time, set old key to passive then remove

Exporting and Migrating Realm Configuration

# Export realm configuration for version control and disaster recovery
# This exports realm settings WITHOUT user passwords (hashes are included)
# Store in Git — review changes before applying to production

# Export a single realm:
docker exec keycloak /opt/keycloak/bin/kc.sh export \
  --dir /tmp/exports \
  --realm myapp \
  --users realm_file
docker cp keycloak:/tmp/exports/myapp-realm.json ./keycloak-realm-myapp.json

# Version control it:
git add keycloak-realm-myapp.json
git commit -m "Export Keycloak realm config: myapp"

# Import on a new Keycloak instance:
# During startup (before first boot):
docker run -v ./keycloak-realm-myapp.json:/opt/keycloak/data/import/realm.json \
  quay.io/keycloak/keycloak:24.0 start --import-realm

# Or import into a running instance:
docker cp keycloak-realm-myapp.json keycloak:/tmp/myapp-realm.json
docker exec keycloak /opt/keycloak/bin/kc.sh import \
  --file /tmp/myapp-realm.json \
  --override true

# Automate realm export weekly:
# 0 3 * * 0 docker exec keycloak /opt/keycloak/bin/kc.sh export \
#   --dir /tmp/exports --realm myapp --users realm_file && \
#   docker cp keycloak:/tmp/exports/myapp-realm.json \
#   /opt/backups/keycloak-$(date +%Y-%m-%d).json

Tips, Gotchas, and Troubleshooting

LDAP Users Can Log In But Missing Attributes

# Check if attributes are being synced from LDAP:
# Admin Console → Users → [find LDAP user] → Attributes tab
# If attributes are empty, the mapper isn't working

# Test LDAP attribute values directly:
docker exec -it keycloak /opt/keycloak/bin/kcadm.sh \
  get users?search=username \
  --server https://auth.yourdomain.com --realm myapp --user admin | \
  jq '.[0] | {username: .username, attributes: .attributes}'

# Force a sync for a specific user:
docker exec keycloak /opt/keycloak/bin/kcadm.sh \
  trigger-sync --server https://auth.yourdomain.com \
  --realm myapp --user admin \
  --id LDAP_COMPONENT_ID \
  --action triggerChangedUsersSync

# Check Keycloak logs for LDAP errors:
docker logs keycloak --since 30m | grep -iE '(ldap|federation|sync|error)'

# Common issues:
# - LDAP attribute name case mismatch (mail vs Mail vs MAIL)
# - User not in the Users DN subtree
# - Service account doesn't have read access to the attribute
# - Always Read From LDAP not enabled on mapper

Cluster Nodes Not Seeing Each Other

# Check cluster formation in Keycloak logs:
docker logs keycloak_node1 | grep -iE '(cluster|ispn|jgroups|view)'

# A healthy cluster shows:
# ISPN000094: Received new cluster view: [...node1, node2...]
# If only one node is listed, clustering failed

# Common causes:
# 1. Nodes can't reach each other on the network
#    Test: docker exec keycloak_node1 ping keycloak_node2
#    If this fails, check Docker network configuration

# 2. JGroups multicast blocked (common in cloud environments)
#    Switch to TCP unicast discovery:
JAVA_OPTS_APPEND=>
  -Djgroups.tcp.address=match-interface:eth0
  -Djgroups.tcp.port=7600
  -Djgroups.tcpping.initial_hosts=keycloak_node1[7600],keycloak_node2[7600]
# And set KC_CACHE_STACK=tcp in environment

# 3. Nodes using different DB — verify both point to the same PostgreSQL:
docker exec keycloak_node1 env | grep KC_DB_URL
docker exec keycloak_node2 env | grep KC_DB_URL
# Must be identical

# Verify via Keycloak admin console:
# When clustered, the admin console shows "Cluster" indicator
# Admin → Server Info → Provider Info → look for cluster info

Custom Theme Not Loading After Restart

# Check theme is mounted correctly:
docker exec keycloak ls /opt/keycloak/themes/
# Your theme directory should appear here

# Check theme properties syntax:
docker exec keycloak cat /opt/keycloak/themes/myapp/login/theme.properties

# Enable theme caching off for development (never in production):
# Add to Keycloak environment:
KC_SPI_THEME_STATIC_MAX_AGE: "-1"
KC_SPI_THEME_CACHE_THEMES: "false"
KC_SPI_THEME_CACHE_TEMPLATES: "false"

# Check Keycloak logs for theme errors:
docker logs keycloak | grep -iE '(theme|template|ftl|error)'

# Verify the realm is using the custom theme:
curl -s "https://auth.yourdomain.com/admin/realms/myapp" \
  -H 'Authorization: Bearer $ADMIN_TOKEN' | \
  jq '{loginTheme: .loginTheme, accountTheme: .accountTheme}'
# loginTheme should be "myapp" not "keycloak" or "keycloak.v2"

Pro Tips

  • Use realm events to build an authentication audit trail — go to Realm Settings → Events → Enable Save Events and set retention to 90 days. Every login, logout, failed auth attempt, and token refresh is logged with IP address and client. Essential for security audits and compliance.
  • Use the Keycloak Operator for Kubernetes deployments — if you're running on Kubernetes rather than Docker Compose, the Keycloak Operator handles HA clustering, rolling updates, and configuration management declaratively. Don't hand-roll Kubernetes YAML for Keycloak.
  • Test your theme changes before deploying to production — run a local Keycloak development instance with theme caching disabled. Make changes, reload the browser, and verify every screen (login, registration, password reset, MFA enrollment) before committing the theme.
  • Monitor Keycloak's JVM heap — the default heap is 512MB which runs out under load in a large realm. Add -Xms512m -Xmx2g to JAVA_OPTS_APPEND for production. Watch heap usage via Keycloak's health metrics endpoint: /health/live and /metrics.
  • Use service accounts for machine-to-machine auth — for APIs that call each other without a user in context, use client credentials flow rather than hardcoded API keys. Keycloak issues short-lived JWTs that your services validate locally without a network call on each request.

Wrapping Up

Advanced Keycloak Docker setup capabilities — LDAP federation that makes Active Directory users first-class citizens, custom themes that make authentication feel like part of your product, fine-grained authorization that enforces complex access rules at the identity layer, and HA clustering that makes authentication a reliable service rather than a liability — these are what separate a Keycloak deployment your organization actually depends on from one that's just running.

Together with the foundational Keycloak guide covering basic deployment, realm configuration, and OIDC client setup, these two guides give you a complete, production-grade self-hosted SSO platform that handles enterprise authentication requirements without handing your user data to a third party.


Need Enterprise SSO Built and Managed for Your Organization?

Designing an enterprise Keycloak deployment — with LDAP federation from your Active Directory, custom authorization models for your application's permission structure, HA clustering, and integration into your existing DevOps pipeline — requires deep experience with both Keycloak and enterprise identity infrastructure. The sysbrix team designs and implements production-grade SSO platforms for organizations that need authentication they can rely on.

Talk to Us →
Portainer Docker Setup: API Automation, Edge Deployments, Security Hardening, and Container Logging at Scale
Learn how to automate Portainer management via its REST API, deploy and manage edge devices from one dashboard, harden your Portainer installation against attack, and build centralized logging across all your container environments.