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.
/opt/keycloak/
├── .env # all your secrets and settings
├── docker-compose.yaml
└── certs/ # generated self‑signed certificate for backend TLS
├── tls.key
└── tls.crt
mod_ssl, mod_proxy, mod_proxy_http, and mod_proxy_wstunnel enabledsudo 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
.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.
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:
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
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
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/.
Open https://<your-keycloak-domain>/admin in a browser.
Log in with the bootstrap credentials from your .env file.
You’ll see an orange “temporary admin” warning – that’s normal.
Create a permanent admin user:
admin (or any name you like).admin → select it → Assign.Log out and log back in with the new permanent admin.
Delete the temporary bootstrap user from the Users list.
Now the orange banner is gone, and your Keycloak instance is fully production‑ready.
start --optimized for faster restartsAfter 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.)
In the current docker-compose.yaml we already use ${KC_BOOTSTRAP_ADMIN_USERNAME} and ${KC_BOOTSTRAP_ADMIN_PASSWORD} from .env. No hardcoded secrets remain.
Already included in the compose file above.
You now have a hardened Keycloak instance that:
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.