Workload · Running your own single sign-on

Self-hosted Keycloak: one sign-in for all your apps, on a server you control

To self-host Keycloak properly you run it in production mode with start, give it a PostgreSQL database and a fixed public address, and put a reverse proxy in front that lets the world reach the login pages while the admin console answers only you, on a name of its own. Keycloak is the open-source sign-in server behind single sign-on (one account for many apps) over OpenID Connect and SAML. Below you will find what it needs, a setup that keeps your passwords out of sight, and the habits that keep it running.

The short version
  1. Production mode only: start, never start-dev
  2. Plan CPU by logins per second, not by number of users
  3. The public name serves /realms/, /resources/ and /.well-known/, nothing else
  4. The admin console gets its own name, open to your IP address alone

What does a sign-in server need?

Mostly processor time at the moment people log in. When someone types a password, Keycloak spends most of its CPU checking it against a deliberately slow hash, so the number that matters is how many people sign in during your busiest minute, not how many accounts you have.

  • Processor: the Keycloak sizing guide allocates 1 vCPU for every 15 password logins per second, and 1 vCPU for every 120 token refreshes or machine-to-machine (client credential) requests per second, then adds 150% on top for spikes, restarts and failover.
  • Memory: about 1,250 MB per Keycloak instance as a base, which already includes 10,000 cached sessions. The containers guide suggests capping the container at 2 GB for smaller production setups.
  • Database: PostgreSQL, versions 14 to 18 are supported. The built-in dev-file database is for development only.

Where those CPU figures come from: the Keycloak team's test ran two sites side by side, each with three Keycloak pods on Red Hat OpenShift in Amazon's cloud, on ARM (Graviton c7g) machines, sharing an Aurora PostgreSQL database spread over availability zones, with a million users, 20,000 apps and every session kept in the database.

They expect a single site to do slightly better, call the numbers a starting point, and ask you to load-test your own setup. On x86 processors they saw token refreshes and client credential requests go up to twice as fast per core, while password checks stayed the same.

A rough example of ours, not from the guide: if 30 people a second log in at your Monday-morning peak, that is 2 vCPU of hashing, or 5 vCPU once the 150% headroom is added.

Why your sign-in deserves a machine of its own

If Keycloak stalls, nobody gets into anything it protects. Its own sizing guide notes that performance fell sharply whenever Keycloak could not get the processor time it asked for, and on a shared machine a noisy neighbour can cause exactly that. On a dedicated server the physical computer is yours alone, so every core is there for your login rush.

  • You log in as root and decide everything that runs on it, including where your user database lives.
  • The port is unmetered, so nothing changes on your invoice as more apps start asking for tokens.
  • Rack servers and blades include remote console access out of band: the screen and the power button, even if a firewall rule cuts off SSH.
  • No contract. Stay month to month, and reinstall the operating system yourself from the control panel whenever you need a clean start.

The server that fits

For one team or one company, an everyday one-processor server is plenty: Keycloak and its PostgreSQL side by side on the same box. Choose memory with room to spare. More can be added to the same machine later, with no migration and no new address. If your peak logins or the other apps you put on the machine call for more cores, step up to a two-processor server. Browse them on our dedicated servers page; a ready server is handed over within 30 minutes of the payment being approved. If you would rather pick every part, see the custom server page, and if the database will grow into its own machine, read about a PostgreSQL dedicated server.

Up and running in production mode

  1. Point two names at your server, login.example.org for everyone and admin.example.org for you, install Docker, nginx and certbot, and get one certificate that covers both names.
  2. Turn on the firewall, allowing only SSH, 80 and 443, before any service starts listening.
  3. Create the two password files with permissions 600 and generate the passwords straight into them. Nothing secret is typed as a command argument, because arguments are visible to anyone running ps, and they stay in the shell's history file; the database guide itself warns against putting the database password on the command line.
  4. Copy the Compose file into /opt/keycloak as docker-compose.yml, then start it and wait for it to report ready.
  5. Notice the 127.0.0.1: in front of both ports. Docker's port publishing goes around ufw, so without it Keycloak and its health port would be open to the internet no matter what the firewall says. Bound to 127.0.0.1, nothing is published on IPv6 either.
sudo ufw allow proto tcp to any port 22,80,443
sudo ufw enable
sudo certbot certonly --nginx -d login.example.org -d admin.example.org

printf '%s\n' 'proxy_set_header Host $host;' \
  'proxy_set_header X-Forwarded-For $remote_addr;' \
  'proxy_set_header X-Forwarded-Proto https;' \
  'proxy_set_header X-Forwarded-Host $host;' \
  'proxy_set_header X-Forwarded-Port 443;' \
  | sudo tee /etc/nginx/snippets/keycloak-proxy.conf >/dev/null

sudo mkdir -p /opt/keycloak && cd /opt/keycloak
sudo install -m 600 /dev/null db.env
sudo install -m 600 /dev/null keycloak.env
DBPW=$(openssl rand -base64 24); ADMINPW=$(openssl rand -base64 24)
echo "POSTGRES_PASSWORD=$DBPW" | sudo tee db.env >/dev/null
printf 'KC_DB_PASSWORD=%s\nKC_BOOTSTRAP_ADMIN_PASSWORD=%s\n' \
  "$DBPW" "$ADMINPW" | sudo tee keycloak.env >/dev/null
unset DBPW ADMINPW
services:
  db:
    image: postgres:17
    restart: unless-stopped
    env_file: db.env
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
    volumes: ["dbdata:/var/lib/postgresql/data"]
  sso:
    image: quay.io/keycloak/keycloak:26.8.1
    command: ["start"]
    restart: unless-stopped
    depends_on: [db]
    mem_limit: 2g
    env_file: keycloak.env
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://db/keycloak
      KC_DB_USERNAME: keycloak
      KC_HOSTNAME: https://login.example.org
      KC_HOSTNAME_ADMIN: https://admin.example.org
      KC_HTTP_ENABLED: "true"
      KC_PROXY_HEADERS: xforwarded
      KC_HEALTH_ENABLED: "true"
      KC_METRICS_ENABLED: "true"
      KC_BOOTSTRAP_ADMIN_USERNAME: temp-admin
    ports: ["127.0.0.1:8080:8080", "127.0.0.1:9000:9000"]
volumes:
  dbdata: {}

Then, in /opt/keycloak after saving the file above, run sudo docker compose up -d and wait for the first start, which creates the database tables: until curl --head -fsS http://127.0.0.1:9000/health/ready; do sleep 5; done.

start-dev is tempting because it just works, but Keycloak's configuration guide keeps development mode for trying Keycloak out and says to use production mode for production; the containers guide goes further and says to strictly avoid it there because it has insecure defaults. start refuses to run until you give it a hostname, which is the point.

Who can reach what

Your apps and users only need a few paths. The Keycloak reverse proxy guide spells them out, and the two nginx sites below follow it:

Pathlogin.example.orgadmin.example.orgWhy
/realms/Open-Login pages and the OpenID Connect endpoints
/resources/OpenYour IPStylesheets, scripts and images
/.well-known/Open-Server metadata apps read to find Keycloak
/realms/master/BlockedYour IPThe administration realm
/admin/BlockedYour IPThe admin console and its API
/health, /metricsBlockedBlockedServed on port 9000, which is never proxied

KC_HOSTNAME_ADMIN tells Keycloak to serve its console on the admin name, the separate-hostname setup its hostname guide describes. On its own it does not close the admin API on the public name, and the hostname guide points to the reverse proxy as the place to block it. That is the job of the allow and deny lines: put your own IP address in place of 198.51.100.7.

# everyone: sign-in pages and app endpoints
server {
    listen 443 ssl;
    server_name login.example.org;
    ssl_certificate     /etc/letsencrypt/live/login.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/login.example.org/privkey.pem;
    include snippets/keycloak-proxy.conf;
    location ^~ /realms/master/ { return 403; }
    location ~ ^/(realms|resources|\.well-known)/ {
        proxy_pass http://127.0.0.1:8080;
    }
    location / { return 404; }
}
# you only: the admin console
server {
    listen 443 ssl;
    server_name admin.example.org;
    ssl_certificate     /etc/letsencrypt/live/login.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/login.example.org/privkey.pem;
    include snippets/keycloak-proxy.conf;
    allow 198.51.100.7;  # your own IP address
    deny all;
    location ~ ^/(admin|realms/master|resources)/ {
        proxy_pass http://127.0.0.1:8080;
    }
    location / { return 404; }
}

Two settings make the proxy trustworthy. KC_PROXY_HEADERS: xforwarded tells Keycloak to read the forwarded headers (without it, requests through the proxy that Keycloak origin-checks fail with 403), and nginx sets those headers itself instead of passing along whatever a visitor sent. Here nginx and Keycloak share the server, so the hop between them is plain HTTP that never leaves the machine; the guide calls this edge termination and prefers re-encrypting to Keycloak, which you should do if the proxy ever moves to another box.

Your first login, and getting rid of it

The account named by KC_BOOTSTRAP_ADMIN_USERNAME is created only on the very first start, and Keycloak treats it as temporary. From your own connection, open https://admin.example.org/admin/:

  1. Sign in as temp-admin with the password from keycloak.env (sudo cat it, once).
  2. In the master realm, create your real admin user, assign it the admin role, and set up an authenticator app for it.
  3. Sign out, sign in as that user, and delete temp-admin.
  4. Remove the bootstrap password line from keycloak.env, and give your own apps a realm of their own.

Keeping it healthy

  • Is it up? From the server itself, curl --head -fsS http://127.0.0.1:9000/health/ready exits 0 when Keycloak is ready. curl is not included in the Keycloak image, so always check from the host.
  • Backups: everything (users, apps, realm settings) lives in PostgreSQL. Add this line to root's crontab for a nightly copy: docker compose -f /opt/keycloak/docker-compose.yml exec -T db pg_dump -U keycloak -d keycloak | gzip > /var/backups/kc-db-$(date +\%F).sql.gz. The backslash before % matters because cron gives % a meaning of its own, and -T switches off the terminal that compose exec would otherwise ask for. Keep copies off the server too.
  • Upgrades: keep the image tag pinned, dump the database, then raise the version. Startup can migrate the database, and a dump is your way back.
  • Planning for failover: In Keycloak's production guide a typical setup runs two or more instances, so people can still sign in when one stops. If your company cannot work without it, plan a second machine. Apps you might connect first include a Nextcloud server.

Questions people ask

How many users can one Keycloak server handle?
The Keycloak team sizes by activity, not accounts: 1 vCPU per 15 password logins each second, plus 150% headroom. A company whose staff log in once in the morning needs far less than a consumer app with constant sign-ins. Check your real rate with Keycloak's metrics after launch.
Do I need PostgreSQL, or is the built-in database fine?
You need a real database. The built-in dev-file database exists only for development and must be replaced before production. PostgreSQL 14 to 18 is supported, and running it next to Keycloak on the same server is a common start.
Why does the admin console not load for me?
It only answers on the admin name, and only for the IP address in the allow line. If your address has changed, update that line and reload nginx. The public name never serves /admin/ or /realms/master/, as the Keycloak reverse proxy guide recommends.
What is port 9000 for?
It is the management port, serving health checks and metrics. Keep it bound to 127.0.0.1 and never route it through the proxy, so outsiders cannot read how your server is doing.
Can I start with a small server and grow?
Yes. Memory, disks and port speed can be raised on the server you already have, and there is no contract if you would rather move to a bigger one. Your PostgreSQL dump carries every user and setting across.

Give your apps one sign-in you own

A whole server, root access and a port nobody meters, handed over within 30 minutes of approval.

Get a server for your Keycloak

Read next