initial commit keycloak & localai
This commit is contained in:
391
docker-compose.yml
Normal file
391
docker-compose.yml
Normal 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
|
||||
Reference in New Issue
Block a user