How to Install Keycloak on Ubuntu 24.04 VPS: Enterprise-Grade Open-Source IAM
Identity and Access Management (IAM) is the beating heart of any modern application stack. Every app you deploy needs login, password reset, MFA, session management, social login, and some form of role-based access control. Building that yourself is expensive and risky. Outsourcing it to Auth0, Okta, or Azure AD B2C is fast, but the bill grows fast too — and every user authenticates against someone else's infrastructure.
Keycloak is the open-source alternative: a battle-tested, enterprise-grade IAM platform maintained by Red Hat, used by banks, government agencies, and Fortune 500s. Running it on your own VPS gives you unlimited users, unlimited realms, OIDC + SAML + LDAP federation, social login, MFA, fine-grained authorization, and full control over your authentication data — for the flat cost of a single server.
In this tutorial you will install Keycloak 25+ (the Quarkus distribution) on Ubuntu 24.04 with a PostgreSQL backend, a production systemd service, and TLS via an Nginx reverse proxy. You will then bootstrap an admin account, create a realm, register OIDC and SAML clients, wire up Google and GitHub social login, federate an LDAP directory, apply a custom theme, and set up scheduled pg_dump backups and a tested upgrade path.
Recommended Plan. Keycloak needs real memory for the JVM and PostgreSQL — the CloudCore Professional VPS (6 vCPU / 12 GB RAM / 100 GB NVMe) is the sweet spot for up to ~50,000 users and comfortable dev/staging room.
Table of Contents
What Is Keycloak and Why Self-Host It?
Keycloak is an open-source identity and access management server originally built by JBoss/Red Hat. It speaks three of the industry's standard authentication protocols fluently:
- OpenID Connect (OIDC) — the modern OAuth 2.0 identity layer used by almost every SaaS and SPA.
- OAuth 2.0 — for delegated authorization and API access tokens.
- SAML 2.0 — still dominant in enterprise and education (Shibboleth, Salesforce, AWS IAM, Atlassian).
The distribution shipped since Keycloak 20 is built on Quarkus — a Java framework that starts in seconds, has a small memory footprint, and supports native/container builds. It replaced the old Wildfly distribution and is what you will install here.
Typical Self-Hosted Keycloak Use Cases
- Single Sign-On (SSO) across internal tools — log in once, access Grafana, Gitea, Jenkins, Nextcloud, Mattermost, Harbor, and Argo CD with the same account.
- B2B/B2C product authentication — give your SaaS real login, social login, MFA, and self-service without paying per-MAU.
- Replacing Auth0/Okta/Azure AD B2C — same OIDC contract, your infrastructure, no per-user bill.
- Federating legacy LDAP/AD — modernize an on-prem Active Directory with OIDC/SAML on top without touching the directory.
- Customer portals with social login — add Sign in with Google/GitHub/Apple/Microsoft in minutes via Keycloak Identity Providers.
Self-Hosted Keycloak vs Auth0/Okta: Cost Reality
IAM SaaS prices escalate quickly once you pass the free tier. Here is what the same workload costs on the major platforms versus a CloudCore Professional VPS running Keycloak:
| Users / Scenario | Auth0 (Essentials → B2B) | Okta (Customer Identity) | Azure AD B2C | Self-hosted Keycloak (CloudCore Professional) |
|---|---|---|---|---|
| 1,000 monthly active users (MAU) | ~$35-$240/mo | ~$150+/mo | ~$0-$6/mo | EUR 19.99/mo flat |
| 10,000 MAU, social login + MFA | ~$1,200+/mo | ~$1,500+/mo | ~$55/mo + add-ons | EUR 19.99/mo flat |
| 50,000 MAU, MFA, org mgmt, custom domain | ~$3,000+/mo (Enterprise) | Custom enterprise quote | ~$275+/mo + per-auth fees | EUR 19.99/mo flat |
| Branded/custom domain | Paid add-on | Paid add-on | Paid | Free |
| Unlimited realms / tenants | Paid tier | Paid tier | Complex | Free |
| Data residency / compliance | Limited regions | Limited regions | Microsoft regions | Your VPS, your jurisdiction |
public.user_entity in a standard PostgreSQL you own.Prerequisites
Before you start, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access.
- At least 4 GB RAM (8–12 GB strongly recommended for production; the JVM plus PostgreSQL plus OS is a real workload).
- At least 20 GB disk (Keycloak + JDK + Postgres data + logs).
- A public DNS record (for example
auth.example.com) pointing at your VPS IPv4 address. - Ports 80 and 443 open in your firewall and reachable from the internet (required for Let's Encrypt HTTP-01 validation).
Sizing tip. The CloudCore Professional plan (6 vCPU / 12 GB RAM / 100 GB NVMe) handles ~50k MAU with PostgreSQL co-located on the same box. For >100k MAU or high burst traffic, split PostgreSQL onto its own VPS and run two Keycloak nodes behind a load balancer.
Connect to your server:
ssh root@your-server-ipIf you have not yet installed PostgreSQL or want a standalone database host, see our companion guide: How to Install PostgreSQL on Ubuntu 24.04.
Step 1: Update the System and Create a Service User
Refresh the package index and upgrade the system first.
sudo apt update && sudo apt upgrade -yCreate a dedicated unprivileged system user for Keycloak. Running the JVM as root is never a good idea.
sudo groupadd --system keycloak
sudo useradd --system --gid keycloak --home-dir /opt/keycloak --shell /sbin/nologin keycloakCreate the install directory:
sudo mkdir -p /opt/keycloak
sudo chown keycloak:keycloak /opt/keycloakInstall a few utilities you will use throughout the guide:
sudo apt install -y curl wget unzip ca-certificates gnupg apt-transport-https software-properties-common ufwStep 2: Install JDK 21
Keycloak 25+ requires JDK 17 or later; JDK 21 (the current LTS) is recommended for new installs. Ubuntu 24.04 ships it directly.
sudo apt install -y openjdk-21-jdk-headlessVerify:
java -versionExpected output:
openjdk version "21.0.x" 2024-xx-xx
OpenJDK Runtime Environment (build 21.0.x+xx-Ubuntu-...)
OpenJDK 64-Bit Server VM (build 21.0.x+xx-Ubuntu-..., mixed mode, sharing)Export JAVA_HOME for the service user (the systemd unit will re-declare this too):
sudo tee /etc/profile.d/java-home.sh > /dev/null <<'EOF'
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
EOFStep 3: Install and Configure PostgreSQL
The embedded dev-only H2 database is fine for tinkering, but you must use PostgreSQL (or another supported RDBMS) for anything near production.
Install PostgreSQL 16 from the Ubuntu archive:
sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable --now postgresqlCreate the Keycloak database and role:
sudo -u postgres psql <<'SQL'
CREATE DATABASE keycloak;
CREATE USER keycloak WITH ENCRYPTED PASSWORD 'CHANGE_ME_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak;
ALTER DATABASE keycloak OWNER TO keycloak;
\c keycloak
GRANT ALL ON SCHEMA public TO keycloak;
SQLReplace CHANGE_ME_STRONG_PASSWORD with a real one — you can generate a strong value with:
openssl rand -base64 32Verify the connection:
psql "host=127.0.0.1 dbname=keycloak user=keycloak password=CHANGE_ME_STRONG_PASSWORD" -c "SELECT version();"If Keycloak lives on the same server, keep PostgreSQL bound to localhost (the default in Ubuntu). For a separate database host, edit /etc/postgresql/16/main/postgresql.conf (listen_addresses) and pg_hba.conf accordingly, and open port 5432 only to the Keycloak VPS.
Step 4: Download the Keycloak 25 Quarkus Distribution
Pick a Keycloak release. At time of writing, the latest 25.x is recommended (adjust KC_VERSION if a newer release is out).
export KC_VERSION=25.0.6
cd /tmp
wget https://github.com/keycloak/keycloak/releases/download/${KC_VERSION}/keycloak-${KC_VERSION}.zip
unzip keycloak-${KC_VERSION}.zip
sudo mv keycloak-${KC_VERSION}/* /opt/keycloak/
sudo chown -R keycloak:keycloak /opt/keycloakVerify the layout:
sudo ls /opt/keycloakYou should see bin/, conf/, data/, lib/, providers/, themes/, and version.txt.
Step 5: Configure Keycloak (keycloak.conf)
Keycloak reads /opt/keycloak/conf/keycloak.conf on startup. Environment variables (KC_*) always override the file. Write a production-ready base config:
sudo tee /opt/keycloak/conf/keycloak.conf > /dev/null <<'EOF'---- Database ----
db=postgres db-url=jdbc:postgresql://127.0.0.1:5432/keycloak db-username=keycloak db-password=CHANGE_ME_STRONG_PASSWORD---- Hostname (public URL seen by browsers) ----
hostname=https://auth.example.com hostname-strict=true hostname-backchannel-dynamic=false---- HTTP / reverse proxy ----
http-enabled=true http-host=127.0.0.1 http-port=8080 proxy-headers=xforwarded---- Health & metrics (for Prometheus scraping) ----
health-enabled=true metrics-enabled=true---- Logging ----
log=console,file log-level=INFO log-file=/opt/keycloak/data/log/keycloak.log EOF
sudo chown keycloak:keycloak /opt/keycloak/conf/keycloak.conf sudo chmod 640 /opt/keycloak/conf/keycloak.conf
Replace auth.example.com with your real DNS name and the password with the one you set in Step 3.
Why these settings
db=postgres+db-url/db-username/db-password— tell Keycloak to use your PostgreSQL instance. These map to theKC_DB,KC_DB_URL,KC_DB_USERNAME,KC_DB_PASSWORDenvironment variables if you prefer env-based config.hostname=https://auth.example.com+hostname-strict=true— Keycloak 25 uses thehostname-v2options. Setting a full URL here locks the public base URL used in tokens, metadata, and emails;strictrejects mismatchedHostheaders.http-enabled=truewithhttp-host=127.0.0.1— Keycloak listens locally on plain HTTP. TLS is terminated by Nginx. This is the standard reverse-proxy pattern.proxy-headers=xforwarded— trustX-Forwarded-*headers from the reverse proxy so Keycloak knows the client's original scheme and IP.health-enabled+metrics-enabled— expose/healthand/metricsendpoints (bound to the management port 9000 by default) for Prometheus and load-balancer health checks.
Step 6: Build and Bootstrap the Admin
The Quarkus distribution uses a build step that pre-compiles config into an optimized image, then a start step at runtime. Run the build once as the keycloak user:
sudo -u keycloak /opt/keycloak/bin/kc.sh buildThis produces an augmented server under /opt/keycloak/lib/quarkus and should finish in under a minute.
Bootstrap the first admin
In Keycloak 25, the bootstrap admin user is created from environment variables on first run. This admin is scoped to the master realm and is intended to be used once to create a real admin account, then deleted.
export KC_BOOTSTRAP_ADMIN_USERNAME=tmpadmin
export KC_BOOTSTRAP_ADMIN_PASSWORD="$(openssl rand -base64 24)"
echo "Bootstrap admin password: $KC_BOOTSTRAP_ADMIN_PASSWORD"Run Keycloak once in the foreground to let it initialize the database and create the admin:
sudo -E -u keycloak /opt/keycloak/bin/kc.sh start --optimizedWait for the log line:
Running the server in production mode. DO NOT use this configuration in production.
...
Listening on: http://127.0.0.1:8080
...
Admin console listening on http://127.0.0.1:9000Press Ctrl+C to stop. The schema is now created and the bootstrap admin exists. You will promote it to a real admin in Step 9. For now, continue to systemd so the server runs persistently.
Step 7: Create a systemd Service
Create the unit file:
sudo tee /etc/systemd/system/keycloak.service > /dev/null <<'EOF' [Unit] Description=Keycloak Identity and Access Management After=network.target postgresql.service Wants=postgresql.service[Service] Type=simple User=keycloak Group=keycloak Environment="JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64" Environment="JAVA_OPTS_APPEND=-Xms1024m -Xmx3072m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m"
Bootstrap admin — comment out after the permanent admin is created
Environment="KC_BOOTSTRAP_ADMIN_USERNAME=tmpadmin" Environment="KC_BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_BOOTSTRAP_PASSWORD" ExecStart=/opt/keycloak/bin/kc.sh start --optimized Restart=on-failure RestartSec=10 LimitNOFILE=102400Hardening
NoNewPrivileges=true PrivateTmp=true ProtectSystem=full ProtectHome=true
[Install] WantedBy=multi-user.target EOF
Replace CHANGE_ME_BOOTSTRAP_PASSWORD with the password you generated in Step 6. The -Xmx3072m heap is a sensible default for a 12 GB VPS co-hosting PostgreSQL — shrink to -Xmx1536m on a 4 GB box, grow to -Xmx6144m on 16 GB+.
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now keycloak
sudo systemctl status keycloak --no-pagerYou should see Active: active (running). Tail the log as it starts:
sudo tail -f /opt/keycloak/data/log/keycloak.logWait for Keycloak ... on JVM ... started in X.Xs. Listening on: http://127.0.0.1:8080.
Step 8: TLS via Nginx Reverse Proxy
You have two options to add TLS:
- Option A (recommended): Nginx reverse proxy — simpler certificate management with Certbot, easy HTTP → HTTPS redirect, battle-tested.
- Option B: native TLS in Keycloak — skip Nginx; set
https-certificate-file/https-certificate-key-fileinkeycloak.conf. Works but you own cert rotation and there is no HTTP/80 redirect out of the box.
Install Nginx and Certbot
sudo apt install -y nginx certbot python3-certbot-nginxOpen the firewall:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enableCreate the Nginx site
sudo tee /etc/nginx/sites-available/keycloak > /dev/null <<'EOF' server { listen 80; server_name auth.example.com; return 301 https://$host$request_uri; }server { listen 443 ssl http2; server_name auth.example.com;
# Certs are filled in by certbot on first run ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
# Modern TLS ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m;
# Security headers add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header Referrer-Policy strict-origin-when-cross-origin always;
client_max_body_size 20m;
location / { proxy_pass http://127.0.0.1:8080;
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; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1; proxy_read_timeout 120s; proxy_send_timeout 120s; proxy_buffering off; } } EOF
sudo ln -s /etc/nginx/sites-available/keycloak /etc/nginx/sites-enabled/ sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx
Obtain the Let's Encrypt certificate:
sudo certbot --nginx -d auth.example.com --redirect --agree-tos -m [email protected] -nCertbot auto-renews via a systemd timer — verify with systemctl list-timers | grep certbot.
Test end to end:
curl -I https://auth.example.com/You should see HTTP/2 302 redirecting to /realms/master/.... Open https://auth.example.com/admin/ in a browser — the Keycloak admin console login screen loads.
Step 9: Create Your First Realm
Log into the admin console at https://auth.example.com/admin/ with the bootstrap credentials (tmpadmin / your bootstrap password).
Promote a permanent admin
[email protected] with the required fields.master-realm, grant admin.KC_BOOTSTRAP_ADMIN_* environment lines from /etc/systemd/system/keycloak.service, delete the tmpadmin user from the admin console, systemctl daemon-reload && systemctl start keycloak.Create a product realm
The master realm is reserved for administering Keycloak itself. All your apps and users belong in a separate realm.
myapp (or your product name). Click Create.length(12), notUsername, digits(1), specialChars(1).Step 10: Register an OIDC Client (Grafana + Custom App)
OIDC is the right protocol for 99% of new apps. Keycloak issues an ID token + access token, your app verifies it.
Example A: Grafana via OIDC
myapp realm, go to Clients → Create client.grafana. Name: Grafana. Next.https://grafana.example.com/login/generic_oauthhttps://grafana.example.com
Then in /etc/grafana/grafana.ini:
[auth.generic_oauth]
enabled = true
name = Keycloak
allow_sign_up = true
client_id = grafana
client_secret = PASTE_THE_SECRET_HERE
scopes = openid profile email
auth_url = https://auth.example.com/realms/myapp/protocol/openid-connect/auth
token_url = https://auth.example.com/realms/myapp/protocol/openid-connect/token
api_url = https://auth.example.com/realms/myapp/protocol/openid-connect/userinfo
role_attribute_path = contains(roles[], 'grafana-admin') && 'Admin' || contains(roles[], 'grafana-editor') && 'Editor' || 'Viewer'Restart Grafana, click Sign in with Keycloak — you are redirected to Keycloak, authenticate, and land back in Grafana with the mapped role.
Example B: Your own SPA / API
The same realm can serve any number of apps. For a browser SPA (React, Vue, Svelte) talking to a backend API:
- Create a public client
myapp-webwith redirect URIs for your app andWeb originsset to your SPA domain. Use PKCE (Authorization code flowwithpkce-enabled). - Create a bearer-only client
myapp-api— no login, used by your API to validate access tokens with the realm's JWKS athttps://auth.example.com/realms/myapp/protocol/openid-connect/certs. - In the SPA, use keycloak-js or any standard OIDC library (oidc-client-ts, oauth4webapi).
iss + aud + exp and you have authentication.Step 11: Register a SAML Client
SAML is still required by many enterprise tools. The workflow is similar.
urn:amazon:webservices).email, firstName, lastName, role list mappers so the SAML assertion carries the expected attributes.https://auth.example.com/realms/myapp/protocol/saml/descriptor and upload it to the target application.Apps like Atlassian Cloud, Salesforce, Zoom, AWS IAM Identity Center, and Tableau all plug in via this flow.
Step 12: Identity Providers — Google, GitHub, LDAP
Identity brokering lets Keycloak delegate authentication to an upstream IdP while still owning the session, MFA, and role mapping.
https://auth.example.com/realms/myapp/broker/google/endpointmyapp → Identity providers → Add provider → Google.GitHub
https://auth.example.com/realms/myapp/broker/github/endpointIn both cases, the First Login Flow controls what happens when a brand-new Google/GitHub user signs in — the default is to create a local Keycloak user linked to the upstream account.
LDAP / Active Directory Federation
Federation is different from brokering: Keycloak reads users and groups directly from LDAP (read-only or read-write) and treats them as first-class users.
ldaps://ldap.example.com:636cn=keycloak-service,ou=Service Accounts,dc=example,dc=comou=People,dc=example,dc=comREAD_ONLY (recommended) or WRITABLE.After this, LDAP users can log in to any OIDC/SAML app connected to the realm, and MFA rules configured in Keycloak apply to them even though their passwords live in AD.
Step 13: Custom Themes
Keycloak themes control the look of the login, account, and email pages. You can change the logo, colors, and copy without forking Keycloak.
sudo -u keycloak mkdir -p /opt/keycloak/themes/myapp/login/resources/css
sudo -u keycloak mkdir -p /opt/keycloak/themes/myapp/login/resources/imgCreate /opt/keycloak/themes/myapp/login/theme.properties:
parent=keycloak.v2
import=common/keycloak
styles=css/login.css css/custom.cssDrop a logo at themes/myapp/login/resources/img/logo.png and a css/custom.css that overrides colors and the logo URL.
Then in the realm: Realm settings → Themes → Login theme → myapp. Save. Refresh the login page — your branding loads.
For production, bake custom themes into a dedicated /opt/keycloak/themes/myapp/ tree and keep it under version control. After any theme change, Keycloak picks it up on restart — sudo systemctl restart keycloak.
Step 14: Events, Audit, and Monitoring
Keycloak has a powerful event system that records every login, logout, failed password, token refresh, and admin action.
Query events:
- Events → User events: filter by user, client, type, date.
- Events → Admin events: audit every change an admin made and by whom.
/opt/keycloak/providers/, re-run kc.sh build, and register the listener in the realm's event config.Prometheus metrics
You already enabled metrics in Step 5. Scrape them via the management port:
# prometheus.yml
scrape_configs:
- job_name: keycloak
metrics_path: /metrics
static_configs:
- targets: ['127.0.0.1:9000']Key metrics to alert on: http_server_requests_seconds_count (by status), jvm_memory_used_bytes, process_cpu_usage, agroal_active_count (DB pool), and keycloak_logins_total.
If you are building a broader observability stack, see How to Install Authelia and How to Install Authentik to compare what each IAM exposes.
Step 15: Backups with pg_dump
Keycloak keeps all state in PostgreSQL — users, realms, clients, roles, sessions, events, keys. Back up the database and you back up Keycloak.
Create the backup directory:
sudo mkdir -p /var/backups/keycloak
sudo chown postgres:postgres /var/backups/keycloakWrite the backup script:
sudo tee /usr/local/bin/keycloak-backup.sh > /dev/null <<'EOF' #!/usr/bin/env bash set -euo pipefailBACKUP_DIR=/var/backups/keycloak STAMP=$(date +%F_%H%M%S) DUMP_FILE="${BACKUP_DIR}/keycloak-${STAMP}.sql.gz"
Dump with owner/privilege info, compressed
sudo -u postgres pg_dump --clean --if-exists --no-owner keycloak \ | gzip -9 > "${DUMP_FILE}"Retain 14 daily dumps, delete older
find "${BACKUP_DIR}" -name 'keycloak-*.sql.gz' -mtime +14 -deleteecho "Backup complete: ${DUMP_FILE} ($(du -h "${DUMP_FILE}" | cut -f1))" EOF
sudo chmod +x /usr/local/bin/keycloak-backup.sh
Schedule it daily at 02:15:
sudo tee /etc/cron.d/keycloak-backup > /dev/null <<'EOF'
15 2 * root /usr/local/bin/keycloak-backup.sh >> /var/log/keycloak-backup.log 2>&1
EOFRun once manually to verify:
sudo /usr/local/bin/keycloak-backup.sh
ls -lh /var/backups/keycloak/Restoring a backup
On the target host (stop Keycloak first so schema migrations do not race):
sudo systemctl stop keycloak
gunzip -c /var/backups/keycloak/keycloak-2026-04-16_021500.sql.gz \
| sudo -u postgres psql keycloak
sudo systemctl start keycloakShip the compressed dumps off-box with rsync, restic, or S3 — a backup that sits on the same VPS is not a backup. Our Restic install guide pairs well with this.
Step 16: Upgrading Keycloak
Keycloak publishes minor releases frequently and a major release yearly. Upgrades are deliberately conservative — schema migrations run automatically on startup, but you must always back up first.
Upgrade checklist
/usr/local/bin/keycloak-backup.sh).sudo tar -czf /var/backups/keycloak/keycloak-app-$(date +%F).tar.gz /opt/keycloaksudo systemctl stop keycloakexport KC_NEW=25.0.7
cd /tmp
wget https://github.com/keycloak/keycloak/releases/download/${KC_NEW}/keycloak-${KC_NEW}.zip
unzip keycloak-${KC_NEW}.zipsudo cp /opt/keycloak/conf/keycloak.conf /tmp/keycloak-${KC_NEW}/conf/
sudo cp -r /opt/keycloak/themes/myapp /tmp/keycloak-${KC_NEW}/themes/
sudo cp -r /opt/keycloak/providers/* /tmp/keycloak-${KC_NEW}/providers/ 2>/dev/null || truesudo mv /opt/keycloak /opt/keycloak.old
sudo mv /tmp/keycloak-${KC_NEW} /opt/keycloak
sudo chown -R keycloak:keycloak /opt/keycloaksudo -u keycloak /opt/keycloak/bin/kc.sh buildsudo systemctl start keycloak
sudo journalctl -u keycloak -f On first start with a new schema version, Keycloak runs database migrations — these can take seconds to minutes depending on user count. Look for Keycloak ... started in X.Xs.
sudo rm -rf /opt/keycloak.old.For a major version upgrade (24 → 25, 25 → 26), do it first on a staging VPS with a copy of production's database and your apps pointed at it.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
HTTPS required error on first login | hostname-strict=true with a mismatched public URL, or reverse proxy not forwarding X-Forwarded-Proto | Confirm hostname=https://auth.example.com matches the URL you visit. Ensure Nginx sets proxy_set_header X-Forwarded-Proto $scheme; and proxy-headers=xforwarded in keycloak.conf. |
FATAL: password authentication failed for user "keycloak" | DB password mismatch between PostgreSQL and keycloak.conf | sudo -u postgres psql → ALTER USER keycloak WITH PASSWORD '...'; and update db-password= in keycloak.conf, restart. |
| Admin console loads but CSS/JS 404s | Nginx stripping paths or compressing incorrectly | Keep location / as-is; do not rewrite paths. Disable proxy_buffering for admin console responsiveness. |
Invalid parameter: redirect_uri after Google/GitHub login | Redirect URI registered in Google/GitHub Console does not match Keycloak's broker endpoint | Use exactly https://auth.example.com/realms/myapp/broker/<alias>/endpoint. |
OutOfMemoryError: Metaspace under load | JVM metaspace too small | Increase -XX:MaxMetaspaceSize=512m in the systemd unit. |
connection refused to 127.0.0.1:8080 after start | Keycloak still starting or bound to a different host | sudo tail -f /opt/keycloak/data/log/keycloak.log until "Listening on". Confirm http-host=127.0.0.1. |
Users can log in but ID token's iss is wrong | hostname set to an internal URL | Set hostname= to the exact public URL browsers and apps see (including scheme). |
| Very slow login after upgrade | Missed DB migration, or sessions table bloated | Check journalctl -u keycloak for "Migration" lines. Run VACUUM ANALYZE; on the Keycloak database. |
| Certbot renewal fails | Nginx config returns 301 for /.well-known/acme-challenge/ | The certbot --nginx plugin handles this automatically; do not add custom redirects above the ACME location. |
Where the logs live
- Application log:
/opt/keycloak/data/log/keycloak.log(rotated by Keycloak). - systemd journal:
sudo journalctl -u keycloak -n 200 --no-pager. - Nginx access/error:
/var/log/nginx/access.log,/var/log/nginx/error.log. - PostgreSQL:
/var/log/postgresql/postgresql-16-main.log.
FAQ
Is Keycloak production-ready for B2C apps with tens of thousands of users?
Yes. A single Keycloak node on a well-sized VPS (6 vCPU / 12 GB RAM) running against PostgreSQL comfortably serves tens of thousands of monthly active users. Beyond ~100k MAU or if you need zero-downtime deploys, run two nodes behind a TCP/HTTP load balancer with a shared PostgreSQL (managed or replicated). Keycloak clusters using the Quarkus distribution use Infinispan for distributed caches — by default it forms a cluster automatically when the nodes can reach each other over JGroups.
Can I migrate users from Auth0 / Okta / Firebase Auth to Keycloak?
Yes, with caveats. Passwords hashed with bcrypt/PBKDF2/Argon2 can be imported into Keycloak via a realm export JSON with credentialData containing the hash algorithm. If the source uses a proprietary hash you cannot read (common with Firebase Auth), the standard pattern is a just-in-time migration: stand up a custom UserStorage SPI that falls back to the old provider, re-hashes on first successful login, and stores the credential locally. Over a few weeks all active users migrate transparently.
What is the difference between Keycloak, Authelia, and Authentik?
All three are self-hosted IAM, but they target different spots. Authelia is a lightweight SSO portal focused on protecting web apps behind a reverse proxy (forward auth) with MFA — it does not speak OIDC as a full provider. Authentik is a modern OIDC/SAML provider with a slick UI, written in Python/Go, excellent for teams wanting something lighter than Keycloak. Keycloak is the enterprise heavyweight: largest ecosystem, Red Hat backing, richest feature set (UMA 2.0, CIBA, token exchange, fine-grained authz services), and the widest compatibility with enterprise SAML vendors. Choose Keycloak when you need protocol breadth, SPI extensibility, or enterprise SSO/SAML integrations.
Do I have to use PostgreSQL?
No — Keycloak supports PostgreSQL, MySQL, MariaDB, Oracle, and MSSQL. PostgreSQL is the most popular and best-documented choice for self-hosted installs. Avoid the embedded H2 database for anything except local development; it is explicitly not supported for production.
How do I rotate the admin password and database password safely?
For the admin password: log into the admin console as the admin user, Account security → Signing in → Password → Update password, and enroll MFA while you are there. For the database password: change it in PostgreSQL (ALTER USER keycloak WITH PASSWORD '...';), update db-password= in /opt/keycloak/conf/keycloak.conf (or the KC_DB_PASSWORD env var), and sudo systemctl restart keycloak. Keycloak will use the new credential on its next connection.
Can I run Keycloak in Docker or Kubernetes instead?
Yes. The official quay.io/keycloak/keycloak:25.0 image is supported and is what Red Hat uses internally. For Kubernetes, the Keycloak Operator manages the StatefulSet, PostgreSQL connection, and Ingress via custom resources. For a small-to-medium deployment on a single VPS, the native install described here uses fewer resources, starts faster, and is easier to debug.
How do I expose only specific client apps to the public internet while keeping admin internal?
Run two Nginx server blocks on separate domains. Public block (auth.example.com) proxies only /realms/… and /resources/… paths. Internal block (admin.auth.example.com) behind VPN or IP allowlist proxies /admin/…, /metrics, and /health. Set hostname-admin=https://admin.auth.example.com in keycloak.conf to tell Keycloak to emit the admin console on the internal hostname.
Next Steps
You now have a production-shape Keycloak:
- Wire up your apps. Every new service you deploy — Grafana, Gitea, Mattermost, Nextcloud, Argo CD, Harbor, Jenkins — gets an OIDC client in your realm. One login, everywhere.
- Turn on MFA by default. In Authentication → Required actions, make
Configure OTPorWebauthn Register Passwordlessa default action so every new user enrolls a second factor at first login. - Publish the realm's OIDC discovery URL to your app teams:
https://auth.example.com/realms/myapp/.well-known/openid-configuration. That one URL is all any modern OIDC library needs. - Stand up high availability. Add a second Keycloak node and a managed PostgreSQL when you cross 50k MAU or need zero-downtime deploys.
- Compare with the lighter IAM options. If your use case is mostly "protect my internal web apps behind MFA" rather than "be the OIDC provider for my SaaS", read our Authelia install guide and Authentik install guide.
- Read the official docs. The Keycloak documentation is the canonical reference — especially the Server Administration Guide and the Securing Apps guide.
Right-sized VPS for Keycloak. A single Keycloak + PostgreSQL node runs beautifully on the CloudCore Professional plan (6 vCPU / 12 GB RAM / 100 GB NVMe). EUR 19.99/month, flat — no per-MAU bill, ever.