initial commit keycloak & localai

This commit is contained in:
2026-05-03 09:24:15 +02:00
commit 45f853b7f5
2 changed files with 394 additions and 0 deletions

391
docker-compose.yml Normal file
View File

@@ -0,0 +1,391 @@
# =============================================================================
# LocalAI + Keycloak + nginx-certbot + Terraform Provisioner
# =============================================================================
# Copy .env.example to .env and fill in values before starting.
#
# Start order:
# 1. docker compose up -d postgres keycloak
# 2. docker compose run --rm terraform-provisioner
# 3. docker compose up -d
# =============================================================================
x-restart: &restart
restart: unless-stopped
networks:
localai:
# =============================================================================
# CONFIGS — all config files are inlined here and mounted into containers.
# Compose interpolates ${VAR} inside content: blocks from the .env file.
# To update a config: edit below and restart the affected service (no rebuild).
# =============================================================================
configs:
# ---------------------------------------------------------------------------
# nginx: reverse proxy for LocalAI (/) and Keycloak (/auth/)
# ---------------------------------------------------------------------------
nginx_localai_conf:
content: |
server {
listen 80;
listen [::]:80;
server_name ${DOMAIN:-localhost};
client_max_body_size 512M;
location /auth/ {
proxy_pass http://keycloak:8080/auth/;
proxy_http_version 1.1;
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 $$http_x_forwarded_proto;
proxy_read_timeout 120s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
location / {
proxy_pass http://localai:8080;
proxy_http_version 1.1;
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 $$http_x_forwarded_proto;
proxy_set_header Upgrade $$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
# ---------------------------------------------------------------------------
# Terraform: main.tf — realm, client, roles, and user resources.
# seed_users values are declared in terraform.tfvars below, not here.
# ---------------------------------------------------------------------------
terraform_main_tf:
content: |
terraform {
required_providers {
keycloak = {
source = "keycloak/keycloak"
version = "~> 5.0"
}
}
}
provider "keycloak" {
client_id = "admin-cli"
username = var.keycloak_admin_user
password = var.keycloak_admin_password
url = "http://keycloak:8080/auth"
realm = "master"
}
variable "keycloak_admin_user" { type = string }
variable "keycloak_admin_password" {
type = string
sensitive = true
}
variable "localai_client_secret" {
type = string
sensitive = true
}
variable "localai_base_url" { type = string }
variable "seed_users" {
type = list(object({
username = string
email = string
first_name = string
last_name = string
password = string
is_admin = bool
}))
}
resource "keycloak_realm" "localai" {
realm = "localai"
enabled = true
display_name = "LocalAI"
login_theme = "keycloak"
registration_allowed = false
reset_password_allowed = true
remember_me = true
verify_email = false
login_with_email_allowed = true
duplicate_emails_allowed = false
}
resource "keycloak_openid_client" "localai" {
realm_id = keycloak_realm.localai.id
client_id = "localai"
name = "LocalAI"
enabled = true
access_type = "CONFIDENTIAL"
standard_flow_enabled = true
direct_access_grants_enabled = false
service_accounts_enabled = false
client_secret = var.localai_client_secret
valid_redirect_uris = [
"$${var.localai_base_url}/api/auth/oidc/callback",
"http://localhost:8080/api/auth/oidc/callback",
]
web_origins = [
var.localai_base_url,
"http://localhost:80",
]
}
resource "keycloak_role" "localai_admin" {
realm_id = keycloak_realm.localai.id
name = "localai-admin"
description = "LocalAI administrator"
}
resource "keycloak_role" "localai_user" {
realm_id = keycloak_realm.localai.id
name = "localai-user"
description = "LocalAI regular user"
}
resource "keycloak_user" "seed_users" {
for_each = { for u in var.seed_users : u.username => u }
realm_id = keycloak_realm.localai.id
username = each.value.username
email = each.value.email
first_name = each.value.first_name
last_name = each.value.last_name
enabled = true
initial_password {
value = each.value.password
temporary = false
}
}
resource "keycloak_user_roles" "seed_roles" {
for_each = { for u in var.seed_users : u.username => u }
realm_id = keycloak_realm.localai.id
user_id = keycloak_user.seed_users[each.key].id
role_ids = each.value.is_admin ? [
keycloak_role.localai_admin.id,
keycloak_role.localai_user.id,
] : [
keycloak_role.localai_user.id,
]
}
output "oidc_issuer" {
value = "http://keycloak:8080/auth/realms/$${keycloak_realm.localai.realm}"
}
# ---------------------------------------------------------------------------
# Terraform: terraform.tfvars — seed user accounts.
# Compose interpolates ${...} here, so these values come from .env.
# Add or remove objects in the list to control which accounts are created.
# is_admin=true grants the localai-admin role in Keycloak.
# ---------------------------------------------------------------------------
terraform_tfvars:
content: |
seed_users = [
{
username = "${TF_SEED_ADMIN_USERNAME:-localai-admin}"
email = "${TF_SEED_ADMIN_EMAIL:-admin@example.com}"
first_name = "LocalAI"
last_name = "Admin"
password = "${TF_SEED_ADMIN_PASSWORD:-change_me_admin}"
is_admin = true
},
{
username = "${TF_SEED_USER_USERNAME:-localai-user}"
email = "${TF_SEED_USER_EMAIL:-user@example.com}"
first_name = "LocalAI"
last_name = "User"
password = "${TF_SEED_USER_PASSWORD:-change_me_user}"
is_admin = false
},
]
# ---------------------------------------------------------------------------
# Terraform: entrypoint — waits for Keycloak, copies read-only configs to
# the writable /tf workdir, then runs init + apply.
# ---------------------------------------------------------------------------
terraform_entrypoint:
content: |
#!/bin/sh
set -e
echo "Waiting for Keycloak..."
until wget -qO- http://keycloak:8080/auth/realms/master > /dev/null 2>&1; do
sleep 5
done
echo "Keycloak ready."
cp /tf-config/main.tf /tf/main.tf
cp /tf-config/terraform.tfvars /tf/terraform.tfvars
cd /tf
terraform init -input=false
terraform apply -input=false -auto-approve
echo "Done. OIDC issuer: $(terraform output -raw oidc_issuer)"
# =============================================================================
# SERVICES
# =============================================================================
services:
# ---------------------------------------------------------------------------
# 1. PostgreSQL — backing store for Keycloak
# ---------------------------------------------------------------------------
postgres:
image: postgres:16-alpine
container_name: localai-postgres
<<: *restart
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-keycloak_secret}
volumes:
- ./volumes/postgres/data:/var/lib/postgresql/data
networks:
- localai
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
# ---------------------------------------------------------------------------
# 2. Keycloak
# ---------------------------------------------------------------------------
keycloak:
image: quay.io/keycloak/keycloak:25.0
container_name: localai-keycloak
<<: *restart
command: start-dev
environment:
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER:-admin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin_secret}
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-keycloak_secret}
KC_HOSTNAME: https://${DOMAIN:-localhost}/auth
KC_HTTP_RELATIVE_PATH: /auth
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: "true"
volumes:
- ./volumes/keycloak/data:/opt/keycloak/data
- ./volumes/keycloak/themes:/opt/keycloak/themes
networks:
- localai
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /auth/realms/master HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n' >&3 && grep -q '200 OK' <(cat <&3)"]
interval: 15s
timeout: 10s
retries: 15
start_period: 60s
# ---------------------------------------------------------------------------
# 3. Terraform provisioner (one-shot)
# ---------------------------------------------------------------------------
terraform-provisioner:
image: hashicorp/terraform:1.9
container_name: localai-terraform
restart: "no"
entrypoint: ["sh", "/entrypoint.sh"]
environment:
TF_VAR_keycloak_admin_user: ${KEYCLOAK_ADMIN_USER:-admin}
TF_VAR_keycloak_admin_password: ${KEYCLOAK_ADMIN_PASSWORD:-admin_secret}
TF_VAR_localai_client_secret: ${LOCALAI_OIDC_CLIENT_SECRET:-localai_oidc_secret}
TF_VAR_localai_base_url: https://${DOMAIN:-localhost}
volumes:
- ./volumes/terraform/tf:/tf
configs:
- source: terraform_main_tf
target: /tf-config/main.tf
mode: 0444
- source: terraform_tfvars
target: /tf-config/terraform.tfvars
mode: 0444
- source: terraform_entrypoint
target: /entrypoint.sh
mode: 0555
networks:
- localai
depends_on:
keycloak:
condition: service_healthy
# ---------------------------------------------------------------------------
# 4. LocalAI
# ---------------------------------------------------------------------------
localai:
image: localai/localai:latest-gpu-nvidia-cuda-12
container_name: localai-app
<<: *restart
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
environment:
LOCALAI_AUTH: "true"
LOCALAI_OIDC_ISSUER: https://${DOMAIN:-localhost}/auth/realms/localai
LOCALAI_OIDC_CLIENT_ID: localai
LOCALAI_OIDC_CLIENT_SECRET: ${LOCALAI_OIDC_CLIENT_SECRET:-localai_oidc_secret}
LOCALAI_BASE_URL: https://${DOMAIN:-localhost}
LOCALAI_ADMIN_EMAIL: ${LOCALAI_ADMIN_EMAIL:-admin@example.com}
LOCALAI_API_KEY: ${LOCALAI_API_KEY:-}
LOCALAI_REGISTRATION_MODE: invite
LOCALAI_DISABLE_LOCAL_AUTH: "true"
LOCALAI_MODELS_PATH: /models
LOCALAI_LOG_LEVEL: debug
LOCALAI_AUTH_DATABASE_URL: postgres://keycloak:${POSTGRES_PASSWORD:-keycloak_secret}@postgres:5432/localai?sslmode=disable
LOCALAI_PARALLEL_REQUESTS: "true"
volumes:
- ./volumes/localai/models:/models
- ./volumes/localai/data:/data
- ./volumes/localai/backends:/backends
networks:
- localai
depends_on:
keycloak:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/version || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
# ---------------------------------------------------------------------------
# 5. nginx — reverse proxy
# ---------------------------------------------------------------------------
nginx:
image: nginx:alpine
container_name: localai-nginx
<<: *restart
ports:
- "80:80"
configs:
- source: nginx_localai_conf
target: /etc/nginx/conf.d/default.conf
mode: 0444
networks:
- localai
depends_on:
localai:
condition: service_healthy
keycloak:
condition: service_healthy