Deployment guide

Deploy Keycloak for SSO on Your VPS

Deploy on a VPS Cloud →

Tutorial

Security & Monitoring

Deploy Keycloak for SSO on Your VPS

Keycloak is the open source reference for enterprise SSO, backed by Red Hat and used even in highly regulated environments. With its realms, LDAP federation and full support for OIDC and SAML, it covers needs that few IdPs can match. Self-hosted on a VPS, it gives you this level of industrialization without any licensing or cloud dependency.

10 min read15 steps
Contents· Why deploy Keycloak on your VPS1/13
  1. 01Why deploy Keycloak on your VPS
  2. 02The strengths of a self-hosted Keycloak
  3. 03Prerequisites for this deployment
  4. 04Deploy Keycloak with PostgreSQL
  5. 05Pre-optimized image: separate the build from startup
  6. 06Build and run a pre-optimized image
  7. 07Keycloak or Authentik: which to choose?
  8. 08Back up and migrate without losing a realm
  9. 09Operations checklist
  10. 10Troubleshooting: startup fails with a read-only filesystem
  11. 11Error messages to recognize
  12. 12Reported workaround in Docker Compose
  13. 13Read-only: a hardening measure the vendor does not document

Why deploy Keycloak on your VPS

Where some IdPs aim for simplicity, Keycloak aims for completeness: multi-tenant isolation through realms, LDAP/Active Directory directory federation, brokering to external providers (Google, GitHub, another OIDC), full theming of login pages and fine-grained authorization. For an agency managing several clients or an IT department with compliance requirements, this level of control is decisive. Self-hosting on a VPS spares you the per-user costs of managed IdP offerings and keeps the entire identity base under your jurisdiction, a critical point for sensitive data you would rather not entrust to a third party.

The strengths of a self-hosted Keycloak

  • Multiple realms to fully isolate several clients or environments on a single instance.
  • Native, complete support for OpenID Connect, OAuth2 and SAML 2.0.
  • LDAP / Active Directory federation to reuse an existing directory without migration.
  • Identity brokering: sign-in via Google, GitHub or any external IdP in a few clicks.
  • Fully customizable login pages (themes, languages, per-realm branding).
  • Fine-grained authentication policies: conditional MFA, session duration, role-based constraints.

Prerequisites for this deployment

Keycloak runs on the JVM (Quarkus since v17+) and remains more resource-hungry than a lightweight IdP. Plan for a VPS with at least 2 vCPUs and 4 GB of RAM, ideally 6 GB if you expect load or several active realms. An external PostgreSQL database is strongly recommended in production (never use the embedded H2 database beyond testing). Docker and Docker Compose, a dedicated domain (sso.mydomain.com) and a reverse proxy handling SSL round out the prerequisites, because Keycloak expects to be told that it is served behind an HTTPS proxy.

Deploy Keycloak with PostgreSQL

  1. Define Keycloak and PostgreSQL in Compose

    In your docker-compose.yml, declare the postgres service (image postgres:16) and the keycloak service (image quay.io/keycloak/keycloak). Connect Keycloak to the database via KC_DB=postgres, KC_DB_URL, KC_DB_USERNAME and KC_DB_PASSWORD.

  2. Configure production mode and the proxy

    Launch with the start command (not start-dev). Set KC_HOSTNAME=sso.mydomain.com and KC_PROXY_HEADERS=xforwarded so that Keycloak generates correct URLs behind the reverse proxy. Define the initial admin via KC_BOOTSTRAP_ADMIN_USERNAME / KC_BOOTSTRAP_ADMIN_PASSWORD.

  3. Start and complete the build

    Run docker compose up -d. On first start, Keycloak performs an optimized Quarkus build and then migrates the PostgreSQL schema. Follow docker compose logs -f keycloak until the message indicating that the server is listening.

  4. Set up the SSL reverse proxy

    Route sso.mydomain.com to port 8080 of the Keycloak container via Caddy, Traefik or Nginx, with a Let's Encrypt certificate. Be sure to pass the X-Forwarded-* headers, otherwise login redirects will break.

  5. Create a realm and a client

    Log in to the admin console, create a dedicated realm (never the master realm for your apps), then an OIDC client. Set your application's Valid redirect URIs and retrieve the Client ID and secret in the Credentials tab.

  6. Connect an application and test the flow

    Configure your app with the discovery URL https://sso.mydomain.com/realms/mon-realm/.well-known/openid-configuration. Create a test user in the realm, start a login and verify the token issuance in the console's Sessions.

Pre-optimized image: separate the build from startup

By default, the start command checks at startup whether a Quarkus build phase is needed and runs it if so: this is what lengthens the first start, and it is what writes into the installation tree. The vendor documentation recommends the opposite for a container: run kc.sh build while building the image, then start with start --optimized. The --optimized parameter tells Keycloak it is running in an already optimized image and can skip that check. Build options, such as the database type (db) or enabling health probes (health-enabled), are frozen into the image: passed again at startup with the same value they are ignored, and with a different value they cause an error and require a new build. Secrets, database password included, belong to configuration options and stay supplied at runtime.

Build and run a pre-optimized image

  1. Write a two-stage Dockerfile

    The first stage starts from quay.io/keycloak/keycloak named builder, sets ENV KC_DB=postgres and ENV KC_HEALTH_ENABLED=true, then runs RUN /opt/keycloak/bin/kc.sh build. Pin a precise version number rather than latest, so that every rebuild stays reproducible.

  2. Copy the result into the final image

    The second stage starts again from the same image and runs COPY --from=builder /opt/keycloak/ /opt/keycloak/. Place your custom themes and providers before the build: they are then taken into account by the optimization. If you add files with COPY, add --chown=keycloak:keycloak so that Keycloak can write into the folders concerned.

  3. Start with the optimized option

    In Compose, replace the command with start --optimized. Behind a reverse proxy that terminates TLS, add KC_HTTP_ENABLED=true. The KC_HOSTNAME and KC_PROXY_HEADERS variables and the database credentials remain runtime configuration.

  4. Wire up the health probes

    With KC_HEALTH_ENABLED=true set at build time, the /health/started, /health/live and /health/ready endpoints are served on management port 9000, separate from the application port 8080. Do not publish this port on the Internet: use it only for the Compose healthcheck or your monitoring.

Keycloak or Authentik: which to choose?

Scroll the table

CriterionKeycloakAuthentik
Target use caseEnterprise SSO, multi-tenant, complianceSelf-hosting, homelab, agencies
ProtocolsOIDC, OAuth2, SAML, LDAPOIDC, OAuth2, SAML, LDAP, forward auth
Forward auth (proxy)Not native (via extensions)Native and central to the tool
Resource footprintHigh (JVM, 4 GB+ RAM)Moderate (2-4 GB RAM)
Multi-realm / tenantsExcellent (isolated realms)Tenants supported, less advanced
Flow customizationThemes and Java SPIVisual step-based flows
Learning curveSteep, very completeMore accessible
Directory federationMature (LDAP/AD)Good (LDAP)

Back up and migrate without losing a realm

Keycloak's state lives in two places: the PostgreSQL database, and your configuration files (themes, providers, import files). The reference backup is a scheduled pg_dump, copied off the VPS; test the restore on a throwaway instance, because a backup that has never been restored remains a hypothesis. Realm export complements this setup without replacing it: kc.sh export --dir <folder>, or --realm my-realm for a single realm, with the --users option to choose the user export strategy. The documentation specifies that the server must not be running during the export, that consistency is only guaranteed with all nodes stopped, and that events, persisted sessions and revoked tokens are not exported. Beyond 50,000 users, export to a directory rather than to a single file. On import, start --import-realm reads the .json files of the /opt/keycloak/data/import folder and skips a realm that already exists.

Operations checklist

  • Pinned version — fix the image version number, read the release notes before changing major branch and rebuild the optimized image on every version upgrade.
  • Backed-up database — scheduled pg_dump, copy off the VPS, restore proven on a throwaway instance.
  • Bounded memory — always set a memory limit on the container: the JVM computes its heap from it, with 70% maximum and 50% initial by default.
  • Probes monitored — follow /health/ready on the unpublished port 9000 from your monitoring.
  • Proxy headers under control — the reverse proxy must overwrite the X-Forwarded-* headers received from the client rather than append to them, and must never be bypassed.
  • Events enabled — log sign-ins and administration actions in each realm, to know who signed in and who changed what.

Troubleshooting: startup fails with a read-only filesystem

Hardening a container often means read_only: true in Compose or readOnlyRootFilesystem: true in Kubernetes. With Keycloak, the topic has been open since April 2022 in issue keycloak/keycloak #11286 ("Keycloak does not work with Docker read_only: true"), and it was still active at the end of October 2026. What is established on the project side: a Keycloak contributor reproduced the failure on version 19.0.2 in September 2022, the issue is classified as an enhancement with the "help wanted" label, and it was added to a repository project board in September 2026. What is not: the official documentation describes no read-only mode to date (the page on containers does not mention it), and no maintainer has validated a workaround. The solutions below come from discussion participants; they worked for them, on their versions, with no guarantee after a version upgrade. On 26 October 2026, a contributor offered to write the missing documentation after verifying these workarounds themselves.

Error messages to recognize

  • sed: couldn't open temporary file /opt/jboss/keycloak/bin/seduHoQM0: Read-only file system — message from the original report, on the former WildFly image (version 16.1.1 in the issue): a sed -i in the startup script cannot create its temporary file. On the Quarkus distribution the symptoms differ, see the following lines.
  • KC-SERVICES0075: Failed to get theme request: java.lang.RuntimeException: Temporary directory /opt/keycloak/bin/../data/tmp does not exist and it was not possible to create it. — reported during an upgrade to 20.0.1: the server starts, then fails on the first page that serves a theme. A participant reported that a writable volume mounted on /opt/keycloak/data/tmp fixes this case.
  • Slow startup or write failure in /opt/keycloak/lib/quarkus — a participant compared the container before and after startup with docker diff: three folders change, /tmp, /opt/keycloak/data and /opt/keycloak/lib/quarkus. The last one holds the build output, which start regenerates unless the image is pre-optimized.
  • Read-only data folder after a COPY — a participant reports that forgetting --chown=keycloak:keycloak when copying a realm file leaves /opt/keycloak/data owned by root, hence not writable by Keycloak.
  • Copying /opt/keycloak into a volume at startup — an init container workaround, reported on Kubernetes: it works, but several participants dread having to retest it on every version upgrade.

Reported workaround in Docker Compose

  1. Start from the pre-optimized image

    Reuse the image built above. kc.sh build runs at build time: /opt/keycloak/lib/quarkus is no longer rewritten at startup, which is done with start --optimized.

  2. Declare the read-only filesystem

    Add read_only: true to the keycloak service of the Compose file. With no other change, the container fails: this is the behavior described by the issue.

  3. Provide the writable directories

    Mount a tmpfs on /tmp and a temporary volume on /opt/keycloak/data. In the report relying on a pre-optimized image, these two directories were enough; on a non-optimized image, other participants added /opt/keycloak/lib/quarkus.

  4. Put back what was in data

    A temporary volume starts empty: what the image placed in this folder, including data/import, is no longer visible there. If you import realms with --import-realm, mount their folder separately, read-only.

  5. Check by opening a login page

    Run docker compose up -d then docker compose logs -f keycloak, then open the sign-in page: the first theme request is what made some setups fail. Finish with docker diff to list what the container actually wrote.

Read-only: a hardening measure the vendor does not document

Keep this mode for environments where a policy requires it, such as a security audit or a cluster rule, and accept the cost: an undocumented configuration to retest on every version upgrade, which several participants of the issue dread. On a single VPS, other measures give a clearer gain for less upkeep: Linux capabilities dropped (cap_drop: [ALL]), the no-new-privileges option, unpublished management ports and the PostgreSQL database on an internal Docker network.

In production, never serve Keycloak with the embedded H2 database and never expose the master realm to the public: create a dedicated realm per application or per client, and reserve master for administration. To harden the instance, disable free registration, enable brute force detection in Realm Settings > Security Defenses, and provision everything via realm import files (--import-realm) so you version your configuration rather than clicking through the UI.

Install Keycloak on your VPS

Keycloak is available in the ServOrbit Marketplace: official image, PostgreSQL database, HTTPS and administrator account ready at install time.