Why Self-Host Keycloak?
Auth0 is great — until you're paying $240/month for 1,000 monthly active users and a feature set you're using 20% of. Okta is enterprise-grade and enterprise-priced. Clerk is elegant but opinionated. If you want SSO, OIDC, SAML, social login, MFA, role-based access control, and user federation under one roof — and you want to own the data — Keycloak is the answer.
Keycloak is a battle-hardened, open-source identity and access management (IAM) platform backed by Red Hat. It's been in production at large enterprises for over a decade. The Docker setup is genuinely approachable, and once it's running, it handles auth for every app in your stack.
This guide gets you from zero to a production-ready Keycloak instance: Docker Compose, PostgreSQL for persistence, a reverse proxy for HTTPS, and a configured realm with a working OIDC client.
If you're running this on Coolify, see our Keycloak Docker Setup Guide: Self-Host SSO and Auth That Actually Works for the platform-specific walkthrough.
Prerequisites
Before you start, have these in place:
- A Linux server or VPS — Ubuntu 22.04/24.04 LTS recommended. 2 CPU cores and 2 GB RAM minimum; 4 GB RAM strongly recommended for production workloads.
- Docker and Docker Compose — Docker Engine 24+ and Compose v2 (the
docker composeplugin, not the legacydocker-composebinary). - A domain name with an A record pointed at your server — e.g.,
auth.yourdomain.com. Keycloak requires HTTPS in production mode; a resolvable domain is non-negotiable. - A reverse proxy — This guide uses Traefik. Nginx works too. The proxy handles TLS termination so Keycloak doesn't have to.
- Basic Docker and Linux familiarity — you should be comfortable editing YAML files and running commands over SSH.
- Ports 80 and 443 open on your server firewall for Let's Encrypt and HTTPS traffic.
Step 1: Quick Start — Run Keycloak Locally with Docker
Before setting up the full production stack, spin up a local Keycloak instance to get familiar with the admin console. This uses start-dev mode, which uses an embedded H2 database and skips HTTPS — fine for local exploration, not for production.
docker run -p 127.0.0.1:8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:26.6.3 start-dev
Open http://localhost:8080/admin and log in with admin / admin. You'll land in the Keycloak Admin Console. This is your control plane for everything — realms, clients, users, roles, identity providers, and federation.
Important: Keycloak only allows the initial admin user to be created from a local network connection. The KC_BOOTSTRAP_ADMIN_* environment variables (replacing the older KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD vars in Keycloak 26+) seed this account on first start only. Once created, remove these vars from your production config.
Step 2: Production Docker Compose with PostgreSQL
The dev mode container uses an in-memory H2 database. Data doesn't survive restarts. For production, you need PostgreSQL. Here's a complete docker-compose.yml for a production-grade Keycloak stack.
Project Structure
keycloak/
├── docker-compose.yml
├── .env
└── data/
└── postgres/ # PostgreSQL data volume (auto-created)
The .env File
Never hardcode credentials in your Compose file. Use an .env file and add it to .gitignore:
# .env — do NOT commit this file
POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=change_me_use_a_strong_password
KC_BOOTSTRAP_ADMIN_USERNAME=admin
KC_BOOTSTRAP_ADMIN_PASSWORD=change_me_use_a_strong_password
KC_HOSTNAME=auth.yourdomain.com
KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=change_me_use_a_strong_password
docker-compose.yml
services:
postgres:
image: postgres:16-alpine
container_name: keycloak-postgres
restart: unless-stopped
volumes:
- ./data/postgres:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- keycloak-net
keycloak:
image: quay.io/keycloak/keycloak:26.6.3
container_name: keycloak
restart: unless-stopped
command: start
environment:
KC_DB: postgres
KC_DB_URL: ${KC_DB_URL}
KC_DB_USERNAME: ${KC_DB_USERNAME}
KC_DB_PASSWORD: ${KC_DB_PASSWORD}
KC_HOSTNAME: ${KC_HOSTNAME}
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: "true"
KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_BOOTSTRAP_ADMIN_USERNAME}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD}
depends_on:
postgres:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.rule=Host(`auth.yourdomain.com`)"
- "traefik.http.routers.keycloak.entrypoints=websecure"
- "traefik.http.routers.keycloak.tls.certresolver=letsencrypt"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
networks:
- keycloak-net
- traefik-net # must match your Traefik external network name
networks:
keycloak-net:
driver: bridge
traefik-net:
external: true
Key points in this config:
command: startruns Keycloak in production mode — unlikestart-dev, this enforces HTTPS hostname checks and uses the real database.KC_PROXY_HEADERS: xforwardedtells Keycloak to trustX-Forwarded-*headers from Traefik. This replaces the deprecatedKC_PROXY=edgesetting from older versions.KC_HTTP_ENABLED: trueallows Keycloak to accept plain HTTP internally (port 8080), while Traefik handles TLS externally. Without this, Keycloak in production mode refuses non-HTTPS connections.- The PostgreSQL
healthcheckensures Keycloak only starts after the database is ready.
For the full Traefik + PostgreSQL stack on Ubuntu, including Let's Encrypt configuration and hardened Docker networking, see our Production Guide: Deploy Keycloak with Docker Compose + Traefik + PostgreSQL on Ubuntu.
Step 3: Create a Realm and Configure Your First Client
Once Keycloak is running, the first thing you do is create a realm. The master realm is for administering Keycloak itself — never use it for your applications.
Create a Realm
- Log into the Admin Console at
https://auth.yourdomain.com/admin - Click the realm dropdown at the top-left (it shows "master")
- Click Create realm
- Set Realm name to something meaningful — e.g.,
myapp - Leave Enabled on and click Create
You're now operating inside the myapp realm. Every user, client, role, and group you create here is isolated from the master realm.
Create an OIDC Client
A "client" in Keycloak represents an application that uses Keycloak for authentication. For a web app using OpenID Connect:
- In the left sidebar, go to Clients → Create client
- Set Client type to
OpenID Connect - Set Client ID to your app name (e.g.,
my-web-app) - Click Next
- Enable Client authentication (this creates a confidential client with a client secret)
- Click Next, then set your Valid redirect URIs:
# Valid redirect URIs — add the real URLs for your app
https://app.yourdomain.com/*
http://localhost:3000/* # dev only — remove for production
# Web origins (for CORS)
https://app.yourdomain.com
- Click Save
- Go to the Credentials tab to copy the generated Client secret
Your OIDC discovery endpoint is available at:
https://auth.yourdomain.com/realms/myapp/.well-known/openid-configuration
Any OIDC-compatible library or framework can auto-configure itself from this endpoint — it contains the token, authorization, userinfo, and JWKS URLs.
Create a Test User
- Go to Users → Create new user
- Set a username (e.g.,
alice), first name, last name, and set Email verified to On - Click Create
- Go to the Credentials tab and click Set password
- Enter a password and toggle Temporary to Off (so the user isn't forced to reset on first login)
Step 4: Roles, Groups, and Token Claims
Keycloak's real power is in its authorization model. Here's how the pieces fit together.
Realm Roles vs. Client Roles
- Realm roles are global — available across all clients in the realm. Use them for broad permissions like
admin,user, ormoderator. - Client roles are scoped to a specific client. Use them for fine-grained, app-specific permissions like
my-web-app:readormy-web-app:write.
Create and Assign a Role via Admin CLI
You can do everything through the UI, but the Admin CLI is faster for scripting and automation:
# Authenticate the CLI against your running Keycloak instance
docker exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 \
--realm master \
--user admin \
--password your_admin_password
# Create a realm role
docker exec keycloak /opt/keycloak/bin/kcadm.sh create roles \
-r myapp \
-s name=app-user \
-s description="Standard application user"
# Assign the role to a user
docker exec keycloak /opt/keycloak/bin/kcadm.sh add-roles \
-r myapp \
--uusername alice \
--rolename app-user
Include Roles in the JWT Token
By default, realm roles appear under the realm_access.roles claim in the access token. To add a custom claim or map a client role, go to your client's Client scopes tab and add a Role mapper. This lets your backend extract roles directly from the token without a round-trip to Keycloak.
Step 5: Reverse Proxy, HTTPS, and Production Hardening
Running Keycloak behind a reverse proxy is the right architecture for production. Traefik handles TLS termination, Let's Encrypt certificate provisioning, and HTTP → HTTPS redirects. Keycloak listens on plain HTTP internally and trusts the proxy headers.
Critical Reverse Proxy Environment Variables
# These three variables must be set correctly together
# for Keycloak to work behind a reverse proxy
# The public-facing hostname (no protocol, no port)
KC_HOSTNAME=auth.yourdomain.com
# Trust X-Forwarded-For and X-Forwarded-Proto headers from the proxy
# Use "xforwarded" for Nginx/Traefik sending X-Forwarded-* headers
# Use "forwarded" for proxies sending the RFC 7239 Forwarded header
KC_PROXY_HEADERS=xforwarded
# Allow Keycloak to accept HTTP internally (proxy terminates TLS)
KC_HTTP_ENABLED=true
If you skip KC_PROXY_HEADERS, Keycloak generates redirect URLs with http:// instead of https://, causing mixed-content errors and broken login flows. This is the single most common production misconfiguration.
Production Hardening Checklist
- Remove bootstrap admin credentials after first login. Delete or unset
KC_BOOTSTRAP_ADMIN_USERNAMEandKC_BOOTSTRAP_ADMIN_PASSWORDfrom your environment once the admin account is created. Recreating the container won't create a new admin if one already exists in the database. - Enable brute-force protection — Go to Realm Settings → Security defenses → Brute force detection and turn it on. Set a reasonable lockout threshold.
- Set short token lifespans — Access tokens default to 5 minutes. Don't increase this. Use refresh tokens appropriately.
- Enable HTTPS-only cookies — Under Realm Settings → Sessions, verify that the session cookie is marked Secure and HttpOnly.
- Restrict the admin console — Consider blocking
/adminat the reverse proxy level for all IPs except your own. - Back up PostgreSQL regularly — Your Keycloak data lives entirely in PostgreSQL. Set up automated
pg_dumpbackups or use your VPS provider's snapshot feature. - Pin the image version — Never use
:latestin production. Pin to a specific version tag (e.g.,26.6.3) and upgrade deliberately.
For a deeper dive into Keycloak production configuration including user federation, custom themes, fine-grained authorization, and high availability setups, see our Keycloak Docker Setup Guide: User Federation, Custom Themes, Fine-Grained Authorization, and High Availability.
Step 6: Integrating an Application with Keycloak OIDC
With Keycloak running and a client configured, integrating your app takes minutes. Here's what the auth flow looks like and what your application needs.
The OIDC Authorization Code Flow
- User clicks "Login" in your app
- Your app redirects to Keycloak's authorization endpoint with your
client_idand aredirect_uri - Keycloak shows the login page, authenticates the user
- Keycloak redirects back to your app with an authorization code
- Your app exchanges the code for an access token and ID token by posting to Keycloak's token endpoint
- Your app validates the JWT and extracts the user's identity and roles
Sample Token Exchange (curl)
# Exchange an authorization code for tokens
curl -X POST \
https://auth.yourdomain.com/realms/myapp/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=my-web-app" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "code=AUTH_CODE_FROM_REDIRECT" \
-d "redirect_uri=https://app.yourdomain.com/callback"
Using a Library
In practice, you won't write raw OAuth calls. Every major framework has an OIDC library that reads Keycloak's discovery endpoint and handles the flow for you:
- Node.js:
openid-client,passport-keycloak-bearer, or NextAuth.js with the Keycloak provider - Python:
python-keycloak,authlib, or Django'ssocial-auth-app-django - Go:
coreos/go-oidc - Java / Spring Boot: Spring Security OAuth2 Resource Server with Keycloak issuer URI
- Generic: Any library that supports OIDC discovery and the authorization code flow works with Keycloak out of the box
Tips and Troubleshooting
"Invalid parameter: redirect_uri"
Keycloak performs exact pattern matching on redirect URIs. The URI your app sends must match one of the Valid redirect URIs configured on the client. The wildcard /* suffix covers subpaths, but the base URL must match exactly (protocol, hostname, and port included). Double-check for trailing slashes.
Login redirects to http:// instead of https://
You're missing KC_PROXY_HEADERS=xforwarded or your proxy isn't sending the X-Forwarded-Proto: https header. Verify your Traefik or Nginx config is forwarding this header. Also confirm KC_HOSTNAME matches your actual public domain exactly.
Admin console is inaccessible after first setup
If you've blocked the admin console at the network level and locked yourself out, you can temporarily bring up a new container with a direct host-network connection to create an emergency admin account. Alternatively, access via SSH tunnel: ssh -L 8080:localhost:8080 user@yourserver and connect to http://localhost:8080/admin.
Keycloak starts but throws database errors
Check that PostgreSQL is healthy before Keycloak starts. The depends_on: condition: service_healthy block in the Compose file handles this, but if you deployed manually, Keycloak may have started before the database was ready. Restart the Keycloak container:
# Check PostgreSQL health
docker inspect keycloak-postgres | grep -A5 Health
# Check Keycloak logs for DB connection errors
docker logs keycloak --tail 50
# Restart Keycloak after confirming Postgres is healthy
docker compose restart keycloak
"User not found" after federation setup
If you're using LDAP or Active Directory user federation, Keycloak imports users lazily on first login by default. Run a manual sync from User Federation → [your provider] → Synchronize all users to force an import. Check the sync logs for errors like invalid attribute mappings or connection timeouts.
Token expired errors in your app
Access tokens are short-lived by design (5 minutes default). Your app should use the refresh token to obtain a new access token silently. If you're seeing frequent expiry errors, you likely have a silent-refresh bug rather than a Keycloak configuration issue. Check your OIDC library's session management settings.
Upgrading Keycloak versions
Always read the Keycloak migration guide for your target version before upgrading. Some releases change the database schema. Never upgrade by more than one major version at a time. Test in a staging environment with a copy of your production database first. Update the image tag in your Compose file and run:
# Pull the new image
docker compose pull keycloak
# Apply database migrations and restart
# Keycloak runs migrations automatically on start in production mode
docker compose up -d keycloak
# Watch the logs to confirm migrations completed successfully
docker logs keycloak -f --tail 100
What to Do Next
You now have a self-hosted SSO stack that can handle authentication for every app you run. Here's where to go from here:
- Add social login — Go to Identity Providers in your realm to add Google, GitHub, Microsoft, or any OIDC/SAML provider. Users can then log in with their existing accounts.
- Enable MFA — Under Authentication → Policies, set up OTP as a required or optional authentication factor for any user or group.
- Set up user federation — Connect Keycloak to your existing LDAP directory or Active Directory so your org's users can log in without a separate Keycloak account. See our user federation and high availability guide for the full setup.
- Customize the login theme — Keycloak's login pages are fully themeable. Drop your HTML/CSS/FTL templates into a custom theme directory and mount it into the container to match your brand.
- Protect internal tools with Keycloak + OAuth2 Proxy — You can put any HTTP app behind SSO using
oauth2-proxyas a sidecar. This works for Grafana, internal dashboards, Jupyter notebooks, and anything else that doesn't have native OIDC support. - High availability — For zero-downtime auth, deploy Keycloak in cluster mode across multiple nodes with a shared PostgreSQL backend. See our high availability Keycloak guide for the architecture.
Need Enterprise-Grade Identity Infrastructure?
Self-hosting Keycloak is powerful, but standing up a production-hardened, highly available auth stack — with LDAP federation, custom themes, multi-environment configurations, and ongoing maintenance — takes real effort. If your team needs it done right without the learning curve, Sysbrix can help.
Talk to our team → We design, deploy, and maintain self-hosted identity infrastructure for teams of all sizes — so you get the control of self-hosting without the operational overhead.