Keycloak is an open source identity and access management server. It gives your applications single sign-on through OpenID Connect (OIDC) and SAML 2.0, and can federate users from LDAP or Active Directory. In this tutorial you will run Keycloak and PostgreSQL with Docker Compose on Ubuntu 24.04, publish it at https://auth.example.com through Nginx with a Let's Encrypt certificate, and create a realm, an OIDC client and a user that an application can sign in with.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS with at least 2 vCPUs and 2 GB of RAM (4 GB for production), for example a CubePath VPS.
- A non-root user with
sudoprivileges. - A domain name with a DNS A record, such as
auth.example.com, pointing to the server's public IP. Replaceauth.example.comwith your own hostname throughout the guide. - Ports 80 and 443 open to the internet.
Step 1 - Installing Docker and Nginx
Ubuntu 24.04 packages Docker Engine and the Compose v2 plugin, which is enough for this setup. Install them together with Nginx and Certbot:
sudo apt update
sudo apt install docker.io docker-compose-v2 nginx certbot python3-certbot-nginx
Allow web traffic through UFW. If UFW is not enabled yet, allow SSH first so you keep access:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Check that Docker and Compose work:
sudo docker compose version
Docker Compose version 2.24.6+ds1-0ubuntu2
Step 2 - Writing the Docker Compose file
Create a directory for the deployment:
sudo mkdir -p /opt/keycloak
cd /opt/keycloak
Keep the database password out of the Compose file by storing it in an .env file that Compose reads automatically. Generate a random value:
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" | sudo tee /opt/keycloak/.env > /dev/null
echo "KC_ADMIN_PASSWORD=$(openssl rand -hex 16)" | sudo tee -a /opt/keycloak/.env > /dev/null
sudo chmod 600 /opt/keycloak/.env
Print the file once and save the admin password somewhere safe; you need it to log in:
sudo cat /opt/keycloak/.env
Now create the Compose file:
sudo nano /opt/keycloak/compose.yaml
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
interval: 10s
retries: 5
restart: unless-stopped
keycloak:
image: quay.io/keycloak/keycloak:26.4
command: start
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_ADMIN_PASSWORD}
ports:
- "127.0.0.1:8080:8080"
- "127.0.0.1:9000:9000"
restart: unless-stopped
volumes:
postgres-data:
A few points about this configuration:
startruns Keycloak in production mode, which requires a hostname and TLS. TLS is terminated by Nginx, soKC_HTTP_ENABLEDallows plain HTTP between Nginx and the container, andKC_PROXY_HEADERS: xforwardedtells Keycloak to trust theX-Forwarded-*headers Nginx sends.- Ports are bound to
127.0.0.1, so Keycloak is only reachable through Nginx. Port 9000 is the management interface that serves health checks. - The image is pinned to a release line. Check the Keycloak releases page for the current version before deploying.
Start the stack. Run this and every other docker compose command in this guide from /opt/keycloak:
cd /opt/keycloak
sudo docker compose up -d
The first start takes a minute or two while Keycloak builds its configuration and creates the database schema. Wait until the readiness endpoint reports UP:
curl -s http://127.0.0.1:9000/health/ready
{
"status": "UP",
"checks": [
...
If it does not come up, read the logs with sudo docker compose logs keycloak.
Step 3 - Configuring Nginx and HTTPS
Create an Nginx server block that forwards requests to Keycloak:
sudo nano /etc/nginx/sites-available/keycloak
server {
listen 80;
listen [::]:80;
server_name auth.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Keycloak sends large cookies and headers
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
Enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/keycloak /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Request a certificate. Certbot edits the server block to listen on 443 and redirect HTTP to HTTPS:
sudo certbot --nginx -d auth.example.com --redirect
Verify that Keycloak answers over HTTPS with the correct issuer:
curl -s https://auth.example.com/realms/master/.well-known/openid-configuration | grep -o '"issuer":"[^"]*"'
"issuer":"https://auth.example.com/realms/master"
Open https://auth.example.com/admin/ in a browser and log in as admin with the KC_ADMIN_PASSWORD value from /opt/keycloak/.env.
ImportantThe bootstrap admin is meant to be temporary. In the
masterrealm, create a personal administrator under Users, assign it theadminrole, log in with it, and then delete theadminbootstrap user.
Step 4 - Creating a realm
A realm is an isolated space with its own users, clients and login settings. Keep the master realm for Keycloak administration only and create a separate realm for your applications. You can do this in the admin console (realm drop-down, Create realm) or with the kcadm.sh CLI inside the container, which is easier to repeat.
Log the CLI in. It prompts for the admin password:
sudo docker compose exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 --realm master --user admin
Logging into http://localhost:8080 as user admin of realm master
Enter password:
Create the realm myapp:
sudo docker compose exec keycloak /opt/keycloak/bin/kcadm.sh create realms \
-s realm=myapp -s enabled=true -s loginWithEmailAllowed=true -s resetPasswordAllowed=true
Created new realm with id 'myapp'
Step 5 - Registering an OIDC client
Each application that signs users in through Keycloak is a client. Create a confidential OIDC client for a web application at https://app.example.com. Service accounts are enabled so you can test token issuance from the command line:
sudo docker compose exec keycloak /opt/keycloak/bin/kcadm.sh create clients -r myapp \
-s clientId=my-webapp \
-s protocol=openid-connect \
-s publicClient=false \
-s standardFlowEnabled=true \
-s serviceAccountsEnabled=true \
-s 'redirectUris=["https://app.example.com/*"]' \
-s 'webOrigins=["https://app.example.com"]' \
-i
The -i flag prints the internal ID of the new client:
6f1c2d8a-3b4e-4f5a-9c7d-1e2f3a4b5c6d
Use that ID to read the generated client secret:
sudo docker compose exec keycloak /opt/keycloak/bin/kcadm.sh get \
clients/6f1c2d8a-3b4e-4f5a-9c7d-1e2f3a4b5c6d/client-secret -r myapp
{
"type" : "secret",
"value" : "Jk3vQ0bY7hH1pW9sXz2mL8cN5tR4aE6d"
}
Test the client with the client credentials grant. A JSON response with an access_token proves the realm, client and secret are correct:
curl -s -X POST https://auth.example.com/realms/myapp/protocol/openid-connect/token \
-d grant_type=client_credentials \
-d client_id=my-webapp \
-d client_secret=your_client_secret | head -c 120; echo
{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ4b0xrQ3N...
Step 6 - Creating a user
Create a user in the myapp realm and set a temporary password that must be changed at first login:
sudo docker compose exec keycloak /opt/keycloak/bin/kcadm.sh create users -r myapp \
-s username=jsmith -s [email protected] -s firstName=John -s lastName=Smith \
-s emailVerified=true -s enabled=true
sudo docker compose exec keycloak /opt/keycloak/bin/kcadm.sh set-password -r myapp \
--username jsmith --new-password 'your_temporary_password' --temporary
Verify by signing in to the realm's account console at https://auth.example.com/realms/myapp/account/. Keycloak asks jsmith to choose a new password and then shows the account page.
Step 7 - Connecting an application
Most frameworks and tools only need the realm's issuer URL, the client ID and the client secret; they read everything else from the discovery document at https://auth.example.com/realms/myapp/.well-known/openid-configuration. The individual endpoints are:
| Purpose | URL |
|---|---|
| Issuer | https://auth.example.com/realms/myapp |
| Authorization | /realms/myapp/protocol/openid-connect/auth |
| Token | /realms/myapp/protocol/openid-connect/token |
| User info | /realms/myapp/protocol/openid-connect/userinfo |
| Signing keys (JWKS) | /realms/myapp/protocol/openid-connect/certs |
| Logout | /realms/myapp/protocol/openid-connect/logout |
In the application, configure the redirect (callback) URL so it matches one of the redirectUris you registered, for example https://app.example.com/oauth2/callback.
For applications that only speak SAML 2.0, create a client with the SAML protocol in the admin console and give the application Keycloak's identity provider metadata, published at:
https://auth.example.com/realms/myapp/protocol/saml/descriptor
To authenticate users stored in an existing LDAP or Active Directory server, open the realm in the admin console, go to User federation, add an LDAP provider, enter the connection URL, bind DN and users DN, and use Test connection and Test authentication before saving.
Step 8 - Backing up the database
All realms, clients and users live in PostgreSQL. Take a logical dump with pg_dump from the database container:
sudo docker compose exec -T postgres pg_dump -U keycloak keycloak | gzip > ~/keycloak-$(date +%F).sql.gz
Check that the file is not empty:
ls -lh ~/keycloak-*.sql.gz
Schedule this command with cron or a systemd timer and copy the dumps off the server.
Troubleshooting
The admin console shows "HTTPS required" or redirects to http://. Nginx is not sending X-Forwarded-Proto, or KC_PROXY_HEADERS is missing. Check both, then run sudo docker compose up -d to recreate the container.
Invalid parameter: redirect_uri on login. The redirect URL the application sends must match one of the client's Valid redirect URIs exactly, including the scheme and trailing path. A pattern ending in /* matches any path below it.
502 Bad Gateway from Nginx. Keycloak is still starting or has crashed. Check sudo docker compose ps and sudo docker compose logs --tail 50 keycloak. Database authentication errors usually mean .env was changed after the PostgreSQL volume was created; the database keeps its original password.
upstream sent too big header in /var/log/nginx/error.log. Increase proxy_buffer_size and proxy_buffers in the server block.
Conclusion
Keycloak now runs on Ubuntu 24.04 with PostgreSQL storage, HTTPS through Nginx, a dedicated realm, a confidential OIDC client and a test user. From here you can put internal tools behind it with OAuth2 Proxy, enable one-time password or passkey policies under Authentication, and connect your existing LDAP directory through user federation.
