Skip to content

How to Deploy Keycloak on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Run Keycloak in production mode on your own VPS — PostgreSQL instead of the dev database, a fixed hostname, TLS at a Caddy reverse proxy, and the temporary admin account replaced.

Before you start
  • A VPS with at least 2 GB RAM (Keycloak's documented floor on this site is 1 GB, plus PostgreSQL and the OS)
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain you can point at the server, such as auth.example.com
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Keycloak is

Keycloak is an open-source identity and access management server. It handles single sign-on over OpenID Connect and SAML, federates users from LDAP or Active Directory, and brokers logins from external identity providers. It is written in Java on Quarkus, licensed Apache-2.0, and is the most feature-complete self-hosted identity provider in this catalog, which also makes it the heaviest to run and learn.

The catalog's install snippet for Keycloak uses start-dev. That mode is for evaluation only: it serves plain HTTP, uses an embedded development database, and relaxes the hostname checks. This guide does the production setup instead:

  • start instead of start-dev
  • PostgreSQL as the database
  • a fixed public hostname
  • HTTP only on the loopback interface, with TLS terminated at Caddy
  • forwarded-header parsing turned on, so Keycloak sees the real scheme and client IP

If you don't need SAML, LDAP federation or fine-grained authorization, a lighter tool may suit you better. Authentik vs Keycloak and Keycloak vs Zitadel compare the options.

Server sizing

The catalog lists Keycloak's minimum at 1 GB RAM. That covers the JVM alone. PostgreSQL, the OS and a reverse proxy come on top, so start with a 2 GB instance, and use 4 GB if the same box runs anything else. Keycloak's startup is CPU-heavy: on the first start it also rebuilds its configuration for PostgreSQL, so expect a minute or two on a small vCPU. Disk use is modest; 20 GB is enough for the images, the database and a few local backups.

Prepare the server

This guide assumes Docker Engine, the Compose plugin and a ufw firewall are set up. If they aren't, work through Docker & Compose on Ubuntu first.

Only SSH and the reverse proxy should be reachable from outside. Keycloak's own ports stay on the loopback interface:

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw status verbose
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Keycloak (Docker Compose + PostgreSQL)

Create a project directory:

mkdir -p ~/keycloak && cd ~/keycloak

Generate the database password and the bootstrap admin password once, into an .env file that only your user can read. The [ -f .env ] || guard stops a second run from replacing the passwords of a database that already exists:

[ -f .env ] || cat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 24)
KC_BOOTSTRAP_ADMIN_PASSWORD=$(openssl rand -hex 16)
EOF
chmod 600 .env

Now write the compose file. Every Keycloak option can be set as a KC_-prefixed environment variable, which keeps the whole configuration in one file:

cat > docker-compose.yml <<'YAML'
services:
  postgres:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
      interval: 5s
      retries: 10

  keycloak:
    image: quay.io/keycloak/keycloak:26.7.4
    command: start
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
      KC_HOSTNAME: https://auth.example.com
      KC_HTTP_ENABLED: "true"
      KC_PROXY_HEADERS: xforwarded
      KC_HEALTH_ENABLED: "true"
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD}
    ports:
      # Loopback only: Caddy is the only way in from outside.
      - "127.0.0.1:8080:8080"
      # Management interface (health checks). Never publish it publicly.
      - "127.0.0.1:9000:9000"

volumes:
  pgdata:
YAML

What each Keycloak setting does:

  • command: start runs production mode. On a standard image, Keycloak applies the build-time options (the PostgreSQL driver, health checks) during the first start. Upstream notes that this makes startup slower than a pre-built "optimized" image. For a single server that trade-off is usually fine.
  • KC_HOSTNAME is the public URL, scheme included. Keycloak uses it for every link, redirect and token issuer it generates. Replace auth.example.com with your real domain before going live.
  • KC_HTTP_ENABLED is required when TLS ends at the proxy (edge termination). Keycloak only listens on HTTP here, and that port is bound to 127.0.0.1.
  • KC_PROXY_HEADERS: xforwarded tells Keycloak to trust the X-Forwarded-* headers Caddy sets. Without it, requests through the proxy that go through origin checks fail with 403 Forbidden.
  • KC_HEALTH_ENABLED turns on /health/ready and /health/live. They are served on the management port, 9000, not on 8080.

Start the stack:

docker compose up -d

Wait for Keycloak to report ready. The first start builds its configuration and creates the database schema, so this takes a while:

timeout 600 bash -c 'until curl -fsS http://127.0.0.1:9000/health/ready; do sleep 5; done'

A healthy server answers with {"status": "UP", ...}. To confirm the hostname setting took effect, read the OpenID discovery document for the built-in master realm. Its issuer should be your public HTTPS URL, not localhost:

curl -s http://127.0.0.1:8080/realms/master/.well-known/openid-configuration | head -c 300; echo

HTTPS + domain

Point an A record for auth.example.com at the server's public IP and wait for it to resolve. Then put Caddy in front of the loopback port. Automatic HTTPS with Caddy covers the install. The site block is:

auth.example.com {
    reverse_proxy 127.0.0.1:8080
}

Caddy gets and renews the certificate itself and sends X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host by default. That is exactly what KC_PROXY_HEADERS: xforwarded expects. Because Keycloak now trusts those headers, make sure nothing else can reach port 8080 directly. The loopback bind takes care of that.

If Caddy runs as a container, 127.0.0.1 is the container's own loopback. In that case, put Caddy in the same compose file, proxy to keycloak:8080, and remove the host port mapping.

Keycloak's reverse-proxy guide recommends exposing only the paths that clients need. The admin console under /admin/ should be reachable only from inside your network. If you manage Keycloak from a fixed address, one way to do that in Caddy is:

auth.example.com {
    @admin_outside {
        path /admin*
        not remote_ip 203.0.113.10
    }
    respond @admin_outside 403
    reverse_proxy 127.0.0.1:8080
}

Replace 203.0.113.10 with your own IP, or reach the admin console over an SSH tunnel to 127.0.0.1:8080 instead.

First login and the temporary admin

Open https://auth.example.com/admin/ and sign in as admin with the KC_BOOTSTRAP_ADMIN_PASSWORD from ~/keycloak/.env. Keycloak marks this bootstrap account as temporary, and the console shows a banner saying so. Replace it:

  1. In the master realm, create a new user, set a strong password under Credentials, and give it the admin realm role under Role mapping.
  2. Sign out, sign back in as the new user, and delete the temporary admin account.
  3. Remove KC_BOOTSTRAP_ADMIN_USERNAME and KC_BOOTSTRAP_ADMIN_PASSWORD from the compose file and .env, then run docker compose up -d. The bootstrap variables only matter when the master realm has no admin, so leaving them in place does nothing useful.

Next, create a separate realm for your applications. The master realm is for administering Keycloak itself. Your users and OIDC clients belong in a realm of their own, such as company.

Securing it

  • Turn on OTP for administrators. In the master realm, go to Authentication → Required actions and make Configure OTP a default action, or require it through a browser flow.
  • Turn on brute-force detection in each realm (Realm settings → Security defenses). It is off by default.
  • Keep the management port private. Port 9000 serves health endpoints, and metrics too if you turn them on. It stays on 127.0.0.1 here.
  • Restrict the admin console path at the proxy, as shown above.
  • Keep clients strict. Keep each client's Valid redirect URIs exact rather than using wildcards, and use confidential clients wherever the application can keep a secret.

Backups

Everything Keycloak knows is in PostgreSQL: realms, clients, users, credentials and keys. A consistent logical dump is the backup:

cd ~/keycloak
docker compose exec -T postgres pg_dump -U keycloak keycloak | gzip > keycloak-db-$(date +%F).sql.gz
ls -lh keycloak-db-*.sql.gz

Keep .env and docker-compose.yml with the dump. Without the database password and the exact configuration, a restore takes longer. Copy all of it off the server. To restore onto a fresh stack, start only postgres, then pipe the dump back in with gunzip -c keycloak-db-DATE.sql.gz | docker compose exec -T postgres psql -U keycloak keycloak before starting Keycloak.

Upgrades

Upgrade Keycloak by changing the pinned tag, never by pulling latest blindly:

  1. Take a database backup (above).
  2. Read the upgrade notes for every version between yours and the target. Keycloak publishes a migration guide with each release.
  3. Edit the image: line, for example from 26.7.4 to the next release, and then run:
cd ~/keycloak
docker compose pull
docker compose up -d

Keycloak migrates the database schema automatically on startup. The dump you took first is your rollback: a schema that has been migrated forward can't be used by an older Keycloak.

Troubleshooting

"HTTPS required" when you open the console. The master realm requires HTTPS for requests from outside the local network. Open Keycloak through the Caddy HTTPS URL, not http://SERVER_IP:8080, which is also not published.

403 Forbidden or a redirect loop behind the proxy. Either KC_PROXY_HEADERS is missing, or the proxy isn't sending the forwarded headers. Caddy sends them by default. Nginx needs proxy_set_header X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-For lines.

Redirects go to the wrong host. KC_HOSTNAME is what Keycloak puts in links, not the host the browser used. Set it to the exact public URL and recreate the container.

The container restarts during first boot. Run docker compose logs keycloak. Database connection errors usually mean the .env password changed after PostgreSQL was first initialized. PostgreSQL keeps the password it was created with. An OutOfMemoryError means the box is too small for the JVM plus PostgreSQL.

Health check never goes green. The health endpoints are on port 9000. A curl to :8080/health/ready returns 404 on current releases.

Verification + next steps

You're done when:

  • https://auth.example.com/realms/master/.well-known/openid-configuration loads over a valid certificate, with an https:// issuer;
  • you can sign in to the admin console as a permanent admin with OTP;
  • the temporary bootstrap account is gone;
  • a compressed pg_dump is stored off the box.

Then create your application realm, register the first OIDC client, and point an app at it. For hosting options sized for a JVM identity server, see Best VPS for Keycloak. If you only need a login screen in front of a few self-hosted apps, Authelia or Pocket ID is a much smaller deployment.

Next steps

How to self-host Keycloak →More self-hosted sso & identity tools →Best VPS for Keycloak →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.