Keycloak

Below is a complete, reusable installation guide for Keycloak 26 on Ubuntu 24.04 LTS with Docker, MariaDB, and Apache reverse proxy – all with end‑to‑end HTTPS. Every secret, URL, and name is replaced with variables that you define once in a .env file.


📁 Directory Layout

/opt/keycloak/
├── .env                  # all your secrets and settings
├── docker-compose.yaml
└── certs/                # generated self‑signed certificate for backend TLS
    ├── tls.key
    └── tls.crt

🛠️ 1. Prerequisites

  • Ubuntu 24.04 LTS (fresh or with existing services)
  • Docker & Docker Compose installed
  • Apache with mod_ssl, mod_proxy, mod_proxy_http, and mod_proxy_wstunnel enabled
  • Firewall configured to allow HTTPS (443)
sudo apt update
sudo apt install docker.io docker-compose-v2 apache2
sudo a2enmod ssl proxy proxy_http proxy_wstunnel rewrite headers
sudo systemctl restart apache2

📄 2. Create the .env File

/opt/keycloak/.env

# ----- Database -----
MARIADB_ROOT_PASSWORD=<choose-a-strong-root-password>
MARIADB_DB_NAME=<keycloak_db>            # e.g. keycloak_prod
MARIADB_USER=<keycloak_db_user>          # e.g. keycloak
MARIADB_PASSWORD=<choose-a-strong-db-password>

# ----- Keycloak Bootstrap Admin -----
# (used only to create the first permanent admin, then removed)
KC_BOOTSTRAP_ADMIN_USERNAME=<temp_admin_user>
KC_BOOTSTRAP_ADMIN_PASSWORD=<temp_admin_password>

# ----- Keycloak Hostname -----
KC_PRODUCTION_HOSTNAME=https://<your-keycloak-domain>   # e.g. https://sso.example.com
KC_HOSTNAME_STRICT=true
KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true
KC_HTTP_ENABLED=false
KC_PROXY_HEADERS=xforwarded

⚠️ Replace every <…> placeholder with your actual values.
Keep the file secure: chmod 600 /opt/keycloak/.env.


🐳 3. Create docker-compose.yaml

/opt/keycloak/docker-compose.yaml

services:
  mariadb:
    image: mariadb:11.4
    container_name: keycloak_mariadb
    env_file: .env
    volumes:
      - mariadb_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MARIADB_DB_NAME}
      MYSQL_USER: ${MARIADB_USER}
      MYSQL_PASSWORD: ${MARIADB_PASSWORD}
    mem_limit: 512m
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 30s
      timeout: 10s
      retries: 5
    restart: always

  keycloak:
    image: quay.io/keycloak/keycloak:26.1
    container_name: keycloak
    env_file: .env
    volumes:
      - ./certs:/etc/x509/https:ro            # mount self‑signed certs
    depends_on:
      - mariadb
    environment:
      # Bootstrap admin (used only once)
      KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_BOOTSTRAP_ADMIN_USERNAME}
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD}

      # Hostname & proxy
      KC_HOSTNAME: ${KC_PRODUCTION_HOSTNAME}
      KC_HOSTNAME_STRICT: ${KC_HOSTNAME_STRICT}
      KC_HOSTNAME_BACKCHANNEL_DYNAMIC: ${KC_HOSTNAME_BACKCHANNEL_DYNAMIC}
      KC_HTTP_ENABLED: "false"                # only HTTPS
      KC_HTTPS_CERTIFICATE_FILE: /etc/x509/https/tls.crt
      KC_HTTPS_CERTIFICATE_KEY_FILE: /etc/x509/https/tls.key
      KC_PROXY_HEADERS: ${KC_PROXY_HEADERS}

      # Database
      KC_DB: mariadb
      KC_DB_URL: jdbc:mariadb://mariadb:3306/${MARIADB_DB_NAME}
      KC_DB_USERNAME: ${MARIADB_USER}
      KC_DB_PASSWORD: ${MARIADB_PASSWORD}
    command: start                           # no --optimized until after first run
    ports:
      - "127.0.0.1:8443:8443"               # only reachable from Apache
    mem_limit: 1g
    healthcheck:
      test: ["CMD", "curl", "-fk", "https://localhost:8443/health/ready"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    restart: always

volumes:
  mariadb_data:

🔐 4. Generate Self‑Signed Certificate for Backend TLS

This certificate is used only between Apache and Keycloak (both on the same machine).

sudo mkdir -p /opt/keycloak/certs
cd /opt/keycloak/certs

# Private key
sudo openssl genrsa -out tls.key 2048

# Certificate signing request
sudo openssl req -new -key tls.key -out tls.csr -subj "/CN=localhost"

# Extension file with SANs (required by modern TLS)
sudo bash -c 'cat > localhost.ext <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = DNS:localhost,IP:127.0.0.1
EOF'

# Self‑signed certificate (valid 1 year)
sudo openssl x509 -req -in tls.csr -signkey tls.key -out tls.crt -days 365 -extfile localhost.ext

# Set ownership for the Keycloak container (user ID 1000)
sudo chown -R 1000:1000 /opt/keycloak/certs
sudo chmod 600 tls.key
sudo chmod 644 tls.crt

🌐 5. Apache VirtualHost Configuration

Create or edit the HTTPS virtual host for your Keycloak domain.
(On a typical Ubuntu/Apache setup, place this in /etc/apache2/sites-available/<domain>.conf and enable it with a2ensite.)

<VirtualHost *:443>
    ServerName <your-keycloak-domain>

    # --- SSL Configuration (your real certificate) ---
    SSLEngine on
    SSLCertificateFile      /path/to/your/fullchain.pem
    SSLCertificateKeyFile   /path/to/your/privkey.pem
    # (Optional) SSLCertificateChainFile ...

    # --- Security headers ---
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-XSS-Protection "1; mode=block"

    # --- Keycloak Reverse Proxy (end‑to‑end HTTPS) ---
    ProxyPreserveHost On
    ProxyRequests Off

    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Forwarded-Port "443"

    # WebSocket support (secure)
    ProxyPassMatch ^/(.*)/towebsocket$ wss://127.0.0.1:8443/$1/towebsocket

    # All other traffic to Keycloak backend
    ProxyPass / https://127.0.0.1:8443/
    ProxyPassReverse / https://127.0.0.1:8443/

    # Accept the self‑signed backend certificate
    SSLProxyEngine on
    SSLProxyVerify none
    SSLProxyCheckPeerCN off
    SSLProxyCheckPeerName off

    # (Optional) If you need to serve ACME challenges directly
    ProxyPass /.well-known/ !
</VirtualHost>

Replace <your-keycloak-domain> with the actual domain (e.g., sso.example.com).
Adjust the SSL certificate paths to your real certificate (e.g., from Let’s Encrypt or your CA).

Enable the site and reload Apache:

sudo a2ensite <your-config-file>.conf
sudo systemctl reload apache2

🚀 6. Start the Stack

cd /opt/keycloak
docker compose --env-file .env up -d

Wait until Keycloak is healthy (check with docker ps).
Verify the backend directly:

curl -fk https://127.0.0.1:8443/

You should see a redirect to https://<your-domain>/admin/.


🔑 7. First Login & Permanent Admin Setup

  1. Open https://<your-keycloak-domain>/admin in a browser.

  2. Log in with the bootstrap credentials from your .env file.

  3. You’ll see an orange “temporary admin” warning – that’s normal.

  4. Create a permanent admin user:

    • In the Master realm, go to UsersAdd user.
    • Username: admin (or any name you like).
    • Click Create.
    • Credentials tab: set a strong password, Temporary = OFF, click Set Password.
    • Role mapping tab: click Assign role → change dropdown to Filter by realm roles → search admin → select it → Assign.
  5. Log out and log back in with the new permanent admin.

  6. Delete the temporary bootstrap user from the Users list.

Now the orange banner is gone, and your Keycloak instance is fully production‑ready.


🧹 8. Hardening & Cleanup (Optional but Recommended)

a) Use start --optimized for faster restarts

After the first successful start, the configuration is cached. Change the command in docker-compose.yaml to:

command: start --optimized

Then re‑create the container once:

docker compose down
docker compose --env-file .env up -d

(If you ever change the environment variables, you must first run start without --optimized once again.)

b) Move bootstrap credentials out of the compose file

In the current docker-compose.yaml we already use ${KC_BOOTSTRAP_ADMIN_USERNAME} and ${KC_BOOTSTRAP_ADMIN_PASSWORD} from .env. No hardcoded secrets remain.

c) Add memory limits and health checks

Already included in the compose file above.


✅ Result

You now have a hardened Keycloak instance that:

  • Runs on Docker with its own MariaDB database.
  • Is exposed exclusively over HTTPS (end‑to‑end: browser → Apache → Keycloak).
  • Uses a self‑signed certificate for internal backend communication.
  • Has a permanent administrator account and no temporary bootstrap user.
  • Starts automatically and restarts on failure.
  • Is ready to be connected to your applications (Chapter 2).

When you are ready for the next step – integrating your application(s) – just let me know. We’ll create a realm, a client, and walk through the OpenID Connect flow.