392 lines
14 KiB
YAML
392 lines
14 KiB
YAML
# =============================================================================
|
|
# 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
|