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 sudo privileges.
  • A domain name with a DNS A record, such as auth.example.com, pointing to the server's public IP. Replace auth.example.com with 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:

  • start runs Keycloak in production mode, which requires a hostname and TLS. TLS is terminated by Nginx, so KC_HTTP_ENABLED allows plain HTTP between Nginx and the container, and KC_PROXY_HEADERS: xforwarded tells Keycloak to trust the X-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.

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:

PurposeURL
Issuerhttps://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.