From 45f853b7f537d2c7e316970b47e936abaae1032c Mon Sep 17 00:00:00 2001 From: geens Date: Sun, 3 May 2026 09:24:15 +0200 Subject: [PATCH] initial commit keycloak & localai --- .gitignore | 3 + docker-compose.yml | 391 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 .gitignore create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c29def --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.claude/ +.env +volumes/ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b4b3e16 --- /dev/null +++ b/docker-compose.yml @@ -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