|Docs

Self-Host Keycloak for Authentication

keycloakauthenticationself-hostedpostgres

Keycloak is an open-source identity and access management server. It provides login pages, user management, single sign-on, and social login through standard protocols: OpenID Connect, OAuth 2.0, and SAML 2.0. Your applications delegate authentication to Keycloak instead of implementing it themselves.

This guide deploys Keycloak on Railway from the official Docker image, backed by a Postgres database. Postgres holds all realms, users, clients, and sessions, so the Keycloak service itself is stateless and needs no volume.

What you will set up

  • A Keycloak instance from the official Docker image, running in production mode
  • A Postgres database for all Keycloak state
  • A public domain with TLS for the admin console and login pages
  • A realm and an OIDC client your applications can authenticate against

One-click deploy from a template

Railway has a Keycloak template that provisions Keycloak and Postgres together:

Deploy on Railway

After deploying, open the service domain, log in with the bootstrap admin credentials from the service variables, and skip to Create a realm and client.

If you prefer manual setup, continue below.

Deploy manually

1. Create the project and database

  1. Create a new project on Railway.
  2. Add a Postgres database: click + New, then Database, then PostgreSQL.

Keycloak supports Postgres directly, so do not run it against the embedded dev database in production; that database is not durable across deploys.

2. Deploy the Keycloak Docker image

  1. Click + New, then Docker Image.
  2. Enter quay.io/keycloak/keycloak:26.3 as the image name.
  3. In the service Settings, set the custom start command to:
/opt/keycloak/bin/kc.sh start

start runs Keycloak in production mode. The default start-dev command is for local development only.

3. Configure environment variables

Add the following variables to the Keycloak service. The ${{Postgres.*}} entries are reference variables that resolve to your database's credentials. The database connection uses private networking, so traffic between Keycloak and Postgres never leaves your project.

VariableValue
KC_DBpostgres
KC_DB_URLjdbc:postgresql://${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/${{Postgres.PGDATABASE}}
KC_DB_USERNAME${{Postgres.PGUSER}}
KC_DB_PASSWORD${{Postgres.PGPASSWORD}}
KC_BOOTSTRAP_ADMIN_USERNAMEadmin
KC_BOOTSTRAP_ADMIN_PASSWORDA strong random password
KC_HTTP_ENABLEDtrue
KC_PROXY_HEADERSxforwarded
KC_HEALTH_ENABLEDtrue

Two of these need explanation:

  • KC_HTTP_ENABLED=true and KC_PROXY_HEADERS=xforwarded tell Keycloak it is running behind a TLS-terminating proxy. Railway terminates TLS at the edge and forwards plain HTTP to your container with X-Forwarded-* headers. Without these settings, production mode refuses HTTP connections.
  • KC_BOOTSTRAP_ADMIN_* creates a temporary admin account on first startup. After first login, create a permanent admin user in the master realm and remove these variables.

4. Generate a domain and set the hostname

  1. In the Keycloak service Settings, under Networking, click Generate Domain. Keycloak listens on port 8080; select it as the target port if prompted.
  2. Add one more variable, using the domain you just generated:
VariableValue
KC_HOSTNAMEhttps://your-keycloak.up.railway.app

Keycloak uses KC_HOSTNAME to build issuer URLs, redirect URLs, and admin console links. It must match the public domain exactly, including the https:// scheme. If you later add a custom domain, update KC_HOSTNAME to match.

After the deploy completes, open the domain and log in to the admin console with your bootstrap credentials.

5. Configure a healthcheck for zero-downtime deploys

Keycloak serves its health endpoints on a separate management port, 9000, not on the main HTTP port. Railway healthchecks probe the port in the PORT variable, so point it at the management port:

  1. Add a service variable PORT with the value 9000. Keycloak ignores PORT (it configures its listener with KC_HTTP_PORT), so this only affects the healthcheck. Public traffic still reaches port 8080 through the domain's target port.
  2. In the service Settings, set the healthcheck path to /health/ready.

With the healthcheck configured, Railway keeps the old deployment serving traffic until the new one reports ready.

Create a realm and client

A realm is an isolated set of users, credentials, and clients. Keep the built-in master realm for Keycloak administration and create a separate realm for your applications.

  1. In the admin console, open the realm dropdown in the top left and click Create realm. Name it, for example, myapp.
  2. Go to Clients, then Create client. Set the client ID (for example web-app), keep the type OpenID Connect, and click Next.
  3. Enable Standard flow for browser-based login. Click Next, then set Valid redirect URIs to your application's callback URL (for example https://myapp.up.railway.app/auth/callback) and Web origins to your application's origin.
  4. Go to Users, then Create new user, and set a password under the Credentials tab.

Your realm now exposes a standard OIDC discovery document:

https://your-keycloak.up.railway.app/realms/myapp/.well-known/openid-configuration

Any OIDC library can configure itself from this URL. It lists the authorization, token, userinfo, and JWKS endpoints.

Test the token endpoint

To verify the deployment end to end, create a client with Client authentication enabled and Service accounts roles checked (a confidential client), copy its secret from the Credentials tab, then request a token:

curl -s -X POST \
  "https://your-keycloak.up.railway.app/realms/myapp/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=my-service" \
  -d "client_secret=YOUR_CLIENT_SECRET"

A JSON response containing access_token confirms Keycloak, Postgres, and the public domain are wired correctly.

Connect applications inside the same project

Services in the same Railway environment can reach Keycloak over private networking at keycloak.railway.internal:8080 (the name matches your service name). This is useful for backend token validation because the traffic stays inside the project.

Keep the issuer consistent: tokens are issued with the public KC_HOSTNAME URL in the iss claim. If a backend fetches the discovery document over the internal hostname, most OIDC libraries will reject tokens because the issuer does not match. So the simplest reliable setup uses the public domain everywhere and reserves the internal hostname for non-OIDC calls such as the admin REST API.

Sizing and production notes

A few points matter once real users depend on this deployment:

  • Keycloak runs on the JVM. Expect a baseline of several hundred MB of memory; 1 GB is a reasonable starting allocation for small deployments. Railway bills that memory by actual usage (per MB per minute), so an idle instance costs less than its limit.
  • All state lives in Postgres, so redeploys, image upgrades, and restarts do not lose users or sessions Keycloak persists there.
  • Pin the image to a specific version (as this guide does with 26.3) and upgrade deliberately. Keycloak upgrades run database migrations on startup.
  • Remove the KC_BOOTSTRAP_ADMIN_* variables after creating a permanent admin account.

Next steps