diff --git a/docker-compose.yml b/docker-compose.yml index b4b3e16..6f5bcc3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,29 +23,35 @@ networks: configs: # --------------------------------------------------------------------------- - # nginx: reverse proxy for LocalAI (/) and Keycloak (/auth/) + # nginx: one server block per subdomain — no path tricks needed # --------------------------------------------------------------------------- nginx_localai_conf: content: | server { listen 80; listen [::]:80; - server_name ${DOMAIN:-localhost}; - + server_name ${DOMAIN_WEBUI:-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://openwebui: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; } + } + + server { + listen 80; + listen [::]:80; + server_name ${DOMAIN_LOCALAI:-localhost}; + client_max_body_size 512M; location / { proxy_pass http://localai:8080; @@ -61,6 +67,25 @@ configs: } } + server { + listen 80; + listen [::]:80; + server_name ${DOMAIN_AUTH:-localhost}; + + location / { + proxy_pass http://keycloak: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_read_timeout 120s; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + } + } + # --------------------------------------------------------------------------- # Terraform: main.tf — realm, client, roles, and user resources. # seed_users values are declared in terraform.tfvars below, not here. @@ -80,7 +105,7 @@ configs: client_id = "admin-cli" username = var.keycloak_admin_user password = var.keycloak_admin_password - url = "http://keycloak:8080/auth" + url = "http://keycloak:8080" realm = "master" } @@ -93,7 +118,13 @@ configs: type = string sensitive = true } - variable "localai_base_url" { type = string } + variable "localai_base_url" { type = string } + variable "openwebui_base_url" { type = string } + + variable "openwebui_client_secret" { + type = string + sensitive = true + } variable "seed_users" { type = list(object({ @@ -133,12 +164,10 @@ configs: 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", ] } @@ -182,8 +211,38 @@ configs: ] } + resource "keycloak_openid_client" "openwebui" { + realm_id = keycloak_realm.localai.id + client_id = "openwebui" + name = "Open WebUI" + enabled = true + access_type = "CONFIDENTIAL" + standard_flow_enabled = true + direct_access_grants_enabled = false + service_accounts_enabled = false + client_secret = var.openwebui_client_secret + + valid_redirect_uris = [ + "$${var.openwebui_base_url}/oauth/oidc/callback", + ] + + web_origins = [ + var.openwebui_base_url, + ] + } + + resource "keycloak_openid_user_realm_role_protocol_mapper" "openwebui_realm_roles" { + realm_id = keycloak_realm.localai.id + client_id = keycloak_openid_client.openwebui.id + name = "realm-roles" + claim_name = "roles" + multivalued = true + add_to_id_token = true + add_to_access_token = true + } + output "oidc_issuer" { - value = "http://keycloak:8080/auth/realms/$${keycloak_realm.localai.realm}" + value = "http://keycloak:8080/realms/$${keycloak_realm.localai.realm}" } # --------------------------------------------------------------------------- @@ -222,7 +281,7 @@ configs: #!/bin/sh set -e echo "Waiting for Keycloak..." - until wget -qO- http://keycloak:8080/auth/realms/master > /dev/null 2>&1; do + until wget -qO- http://keycloak:8080/realms/master > /dev/null 2>&1; do sleep 5 done echo "Keycloak ready." @@ -275,9 +334,8 @@ services: 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_HOSTNAME: https://${DOMAIN_AUTH:-localhost} + KC_PROXY_HEADERS: xforwarded KC_HTTP_ENABLED: "true" volumes: - ./volumes/keycloak/data:/opt/keycloak/data @@ -288,7 +346,7 @@ services: 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)"] + test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /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 @@ -303,10 +361,12 @@ services: 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} + 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_LOCALAI:-localhost} + TF_VAR_openwebui_client_secret: ${OPENWEBUI_OIDC_CLIENT_SECRET:-openwebui_oidc_secret} + TF_VAR_openwebui_base_url: https://${DOMAIN_WEBUI:-localhost} volumes: - ./volumes/terraform/tf:/tf configs: @@ -341,10 +401,10 @@ services: capabilities: [gpu] environment: LOCALAI_AUTH: "true" - LOCALAI_OIDC_ISSUER: https://${DOMAIN:-localhost}/auth/realms/localai + LOCALAI_OIDC_ISSUER: https://${DOMAIN_AUTH:-localhost}/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_BASE_URL: https://${DOMAIN_LOCALAI:-localhost} LOCALAI_ADMIN_EMAIL: ${LOCALAI_ADMIN_EMAIL:-admin@example.com} LOCALAI_API_KEY: ${LOCALAI_API_KEY:-} LOCALAI_REGISTRATION_MODE: invite @@ -370,7 +430,45 @@ services: start_period: 30s # --------------------------------------------------------------------------- - # 5. nginx — reverse proxy + # 5. Open WebUI — chat frontend + # --------------------------------------------------------------------------- + openwebui: + image: ghcr.io/open-webui/open-webui:main + container_name: localai-openwebui + <<: *restart + environment: + WEBUI_URL: https://${DOMAIN_WEBUI:-localhost} + WEBUI_SECRET_KEY: ${OPENWEBUI_SECRET_KEY:-change_me_webui_secret} + OPENAI_API_BASE_URL: http://localai:8080/v1 + OPENAI_API_KEY: ${LOCALAI_API_KEY:-} + OPENID_PROVIDER_URL: https://${DOMAIN_AUTH:-localhost}/realms/localai/.well-known/openid-configuration + OAUTH_CLIENT_ID: openwebui + OAUTH_CLIENT_SECRET: ${OPENWEBUI_OIDC_CLIENT_SECRET:-openwebui_oidc_secret} + OAUTH_PROVIDER_NAME: Keycloak + OAUTH_SCOPES: openid email profile + OAUTH_ROLES_CLAIM: roles + OAUTH_ALLOWED_ROLES: localai-user,localai-admin + OAUTH_ADMIN_ROLES: localai-admin + ENABLE_OAUTH_SIGNUP: "true" + ENABLE_LOGIN_FORM: "false" + volumes: + - ./volumes/openwebui/data:/app/backend/data + networks: + - localai + depends_on: + localai: + condition: service_healthy + keycloak: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + + # --------------------------------------------------------------------------- + # 6. nginx — reverse proxy # --------------------------------------------------------------------------- nginx: image: nginx:alpine