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
- Point two names at your server,
login.example.orgfor everyone andadmin.example.orgfor you, install Docker, nginx and certbot, and get one certificate that covers both names. - Turn on the firewall, allowing only SSH, 80 and 443, before any service starts listening.
- 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. - Copy the Compose file into
/opt/keycloakasdocker-compose.yml, then start it and wait for it to report ready. - 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:
| Path | login.example.org | admin.example.org | Why |
|---|---|---|---|
| /realms/ | Open | - | Login pages and the OpenID Connect endpoints |
| /resources/ | Open | Your IP | Stylesheets, scripts and images |
| /.well-known/ | Open | - | Server metadata apps read to find Keycloak |
| /realms/master/ | Blocked | Your IP | The administration realm |
| /admin/ | Blocked | Your IP | The admin console and its API |
| /health, /metrics | Blocked | Blocked | Served 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/:
- Sign in as
temp-adminwith the password fromkeycloak.env(sudo catit, once). - In the master realm, create your real admin user, assign it the admin role, and set up an authenticator app for it.
- Sign out, sign in as that user, and delete
temp-admin. - 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/readyexits 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-Tswitches off the terminal thatcompose execwould 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
allowline. 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.