Files
serverAppConfigs/serverAppConfigs-docs/Dankclaw/OpenWebUI.md
T
Frank 33eae71d33 docs: distill the managed skill library into the vault (waves 1-7)
All ~173 managed skills read in full and merged, one polished reference
page per skill-cluster:

- LLM cross-cutting (17 pages): GGUF Compatibility, Llama Cpp Serving
  Sizing, Llama Cpp Multi-Device Splits, PCIe Link and Atomics, RDNA4
  Container Serving, RDNA4 MoE Expert Offload VRAM Budget, vLLM Boot
  Forensics, vLLM Porting and Compile Triage, LLM Bench Windows, LLM
  Performance Diagnosis, Model Swap and Rollback, Tcclaviger Fork Ops,
  Heterogeneous Split Triage, FreeToken Builds + deltas folded into the
  retired runbooks (GLM DFlash2 port laws, Lucebox dual-R9700, R9V
  qualification pipeline, FLM pack registration, ik hybrid envelope,
  frank-medium split rebalance, llama.cpp GGUF preflight, batch-ubatch
  split section, bench-window D-state/stale-read notes, second-endpoint
  bring-up).
- Architecture (3): Stale Signal Audit, Quadlet Lifecycle, Deploy State
  Triage.
- Operations (6 + index): Remote Job Runner, Payload and Disk Reclaim,
  Container Host Reachability, Fleet Ops Playbook, Fleet Audit and
  Posture, SSH and Remote Access.
- Infrastructure: Observability Provisioning + Homepage Status Probes;
  updates to Authentik, Anubis, LAN-DNS (per-client DoH bypass, Porkbun
  zone ops), Swap Configuration, OpenWebUI, Hermes (pre-existing page
  preserved and extended).
- FrankBook-Pro: ARR-Stack filled (API split, schema-first probing,
  auth-posture, auth-config trap); qBittorrent E2E verification.
- Desktop/Agent/Research: Noctalia and Niri, OMP Harness, Agent Docs and
  Edits, Command Center Dotfiles, PICO-8 Asset Standards.
- Indexes: LLM Cross-Cutting References section, Operations/ and Agent/
  index pages, root-index Skill Distillation section, AGENTS.md pointer.
- log.md carries the wave plan.

check-vault-links.py 0 unresolved (185 pages); ./tests/run.sh 366/366.
2026-10-05 21:19:15 -07:00

15 KiB
Raw Blame History

type, description, tags, timestamp
type description tags timestamp
reference OpenWebUI is a self-hosted chat interface for LLMs.
dankclaw
openwebui
2026-09-21

OpenWebUI

OpenWebUI is a self-hosted chat interface for LLMs. It connects to OpenAI-compatible endpoints (vLLM, llama.cpp, Ollama) and adds RAG, plugins, tool calling, and multi-user support.

Host: Dankclaw — Port: 3005 (host) → 8080 (container) — Vhost: ai.franksdank.cloud routes straight to 192.168.0.143:3005 (unguarded — OpenWebUI enforces its own auth) — Boot-persistent: Quadlet — Docs: https://github.com/open-webui/open-webui

Facts

Item Value
Image ghcr.io/open-webui/open-webui:main (verified from the live quadlet unit)
Upstream latest 0.11.4 (2026-09-21) — security/access-control advisory; :main is the rolling image, not a stable pin. Imagegen endpoint is POST /api/v1/images/generations (the comfyui engine is still upstream).
Port 3005 (host) → 8080 (container). Port 3000 is Gitea's HTTP, so it is not usable.
Data volume /home/dank/Containers/open-webui/data → /app/backend/data
Backends Three fleet endpoints: frank-large (host.containers.internal:7770), frank-medium (192.168.0.235:7770), frank-small (host.containers.internal:7771) — see The LLM backends
Network Pinned bridge systemd-open-webui (10.89.3.0/24, gateway 10.89.3.1) — see History
Persistence Quadlet unit open-webui.service; [Install] WantedBy=default.target
Deploy scripts/deploy-open-webui-quadlet.sh dankclaw

The LLM backends

OpenWebUI serves the three fleet LLM endpoints. The model picker is built from the model table in webui.db (one row per model), NOT from live connection discovery — so the picker shows exactly the models seeded there.

Picker model Connection URL Runtime Host
frank-large http://host.containers.internal:7770/v1 unsloth llama.cpp (Strix Halo iGPU, 200k ctx) Dankclaw
frank-medium http://192.168.0.235:7770/v1 vllm-radiance TP=2 (FP8, DFlash2, 262k ctx) Dankserver
frank-small http://host.containers.internal:7771/v1 llama.cpp (RTX 3090, Gemma4-12B, 262k ctx) Dankclaw

Two gotchas make the wiring non-obvious:

  • llama.cpp /v1/models is Ollama-shaped. llama.cpp returns {"models":[{"name":...}]}; OpenWebUI's extract_data() only reads the OpenAI {"data":[{"id":...}]} shape, so connection discovery silently misses the llama.cpp endpoints (frank-large, frank-small). The fix is to set model_ids in each connection's api_config — when non-empty, OpenWebUI builds the model list directly and never fetches /v1/models. frank-medium (vLLM) serves the OpenAI shape and would discover fine, but uses model_ids too for consistency.
  • The host's own services are reached via host.containers.internal. The container sits on a netavark bridge; it can hairpin to a different host's LAN IP (dankserver 192.168.0.235) but not to its own host's non-published ports via the host LAN IP (192.168.0.143:7770/7771 is refused). host.containers.internal (169.254.1.2) resolves to the host on the bridge and reaches both llama.cpp services.

The Ollama connection is disabled (ollama.enable=false).

All three endpoints are bare-metal systemd services managed by this repository. Model swaps, context sizing, and spec tuning happen there. See the LLM knowledge base.

Container introspection (DB and backend python)

  • Working dir for imports: /app/backend; package name open_webui (underscore), NOT openwebui. os.environ["WEBUI_SECRET_KEY"] MUST be set BEFORE importing any open_webui module (import-time hard requirement); the secret lives at /app/backend/.webui_secret_key.

  • The sqlite3 CLI is NOT in the container — use Python's sqlite3 module (podman exec open-webui python3 -c "import sqlite3; conn = sqlite3.connect('/app/backend/data/webui.db'); …"). For config keys use parameterized LIKE queries (avoids shell quoting).

  • Backend python: podman exec -w /app/backend open-webui python3 -c "…" — Models.get_all_models() is a coroutine (await it); model objects are pydantic ModelModel instances (attribute access, not .get()), with NO status/enabled attribute — use is_active from the DB.

  • API auth for verification: create_token lives in open_webui.utils.auth (NOT open_webui.utils.utils); get_verified_user requires an id claim (not just sub); SESSION_SECRET = WEBUI_SECRET_KEY. Mint an admin token inside the container:

    podman exec -i open-webui sh -c 'export WEBUI_SECRET_KEY=$(cat /app/backend/.webui_secret_key); \
      python -c "from open_webui.utils.auth import create_token; \
      print(create_token({\"id\":\"<UID>\",\"sub\":\"<UID>\",\"email\":\"…\",\"role\":\"admin\"}))"'
    

    (admin UID from the user table).

Verification battery

  1. GET /api/v1/models/list?limit=100 → returns EXACTLY the intended models. Response key is items, not data.
  2. POST /openai/chat/completions per model with a trivial prompt ("Reply with exactly one word: pong") → each returns content pong. Reasoning models need max_tokens>=100 — the thinking block eats the budget.
  3. A 500 "Server Connection Error" → podman logs open-webui for ClientConnectorError: a network-reachability problem (host LAN IP vs host.containers.internal), not a model problem.

Durability: the model table + connections are DB-only (not git-managed); a fresh data volume loses them. For reproducibility, add an idempotent first-start seed script or document the setup in the service doc.

Chat web search is served by SearXNG on this host, wired in the quadlet (ENABLE_WEB_SEARCH, WEB_SEARCH_ENGINE=searxng, SEARXNG_QUERY_URL).

  • Access path: the backend calls http://192.168.0.143:8081/search?format=json directly — gluetun's published port, NOT the search.franksdank.cloud vhost. The Anubis→Authentik guard chain is a browser mechanism (proof-of-work + session flow); a plain HTTP client cannot solve it, and the internal port bypasses Caddy entirely.
  • Limiter: SearXNG caps non-HTML (JSON) requests at 4/IP/hour (API_MAX, a hardcoded Python constant — no config knob). The pass_ip list is checked FIRST and short-circuits the whole botdetection chain, and OpenWebUI's bridge egress (10.89.x) is in pass_ip, so the cap never applies. Verified: an 8-request burst from the container returned 8×200.
  • Config storage: OpenWebUI keeps web-search settings per-key in its SQLite config table (webui.db, web.search.*). The quadlet env vars seed that table on first start (fill-missing only); afterwards the DB is authoritative and Admin-UI edits persist there.

SSO (authentik)

Login is authentik via the OIDC authorization-code flow (wired 2026-09-24). The vhost stays exempt from the Anubis guard chain: the OAuth dance is browser-driven and only touches the two exempt vhosts (ai. and auth.).

  • authentik objects (created via the admin API, 2026-09-24): application open-webui (slug open-webui) with the OAuth2 provider Open WebUI (confidential client, authorization_code + refresh_token, Strict redirect URI https://ai.franksdank.cloud/oauth/oidc/callback, implicit consent flow). The client ID is in the quadlet; the client secret lives in the git-ignored host file /home/dank/Containers/open-webui/env (mode 600, loaded via EnvironmentFile=).
  • Roles: the scope mapping Open WebUI roles (scope roles) reads the application entitlements Open WebUI Users → user and Open WebUI Admins → admin. The entitlements are bound to the groups openwebui_users / openwebui_admins. akadmin (frank8465@protonmail.com) is in openwebui_admins; qa-e2e-admin is in openwebui_users.
  • Account merge: OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true folds an SSO login into the existing local account with the same email — the pre-SSO admin (frank8465@protonmail.com) keeps its data. ENABLE_OAUTH_SIGNUP=true lets other openwebui_users create accounts on first login.
  • IdP reachability: the container is on its own bridge (10.89.3.0/24) and cannot reach authentik-server:9000 by name. It reaches the IdP over the public URL https://auth.franksdank.cloud — Caddy publishes 0.0.0.0:443 (netavark DNATs published ports from other containers) and Pi-hole resolves the name to the host LAN IP. The server-side code exchange rides that path.
  • Config authority: the OAuth env vars in the quadlet seed the per-key config DB on first run (fill-missing only); on the live data dir the Admin Panel is authoritative. To rotate the client secret: create a new token in authentik, update the host env file, and set the new value in the Admin Panel (or the DB), then restart the container.
  • SSO-only (2026-09-24): after the OIDC round-trip was verified, the local username/password path was retired. ENABLE_LOGIN_FORM=false hides the form and ENABLE_PASSWORD_AUTH=false refuses password logins, so the only way in is the authentik OIDC flow. The ui.enable_login_form config-DB override was flipped to false in the same change — the per-key DB wins over env, so both must agree, and the DB value is the one to change on a live data dir. To restore the password form: set ui.enable_login_form back to true in the Admin Panel (or DB) and drop the two env vars from the quadlet.

Boot persistence (Quadlet)

The unit lives at dankclaw/open-webui/quadlet/open-webui.container. On the host it installs to ~/.config/containers/systemd/open-webui.container. The generator turns it into open-webui.service. Because the unit sets [Install] WantedBy=default.target and user linger is on, it starts at boot. systemctl --user is-enabled open-webui.service reports generated (the quadlet boot-start state).

Volumes

Container path Host path
/app/backend/data /home/dank/Containers/open-webui/data

The data dir holds the database, RAG indexes, and uploads. The deploy script creates it if missing and preserves it across reruns.

Healthcheck

The quadlet carries a built-in healthcheck (30s interval, 3 retries, 300s start period). It checks two things from inside the container:

  1. App liveness — GET http://127.0.0.1:8080/health returns 200 ({"status":true}).
  2. Network liveness — a non-loopback interface exists in /sys/class/net. The dead-tap zombie state (History) leaves only lo behind, and this is the check that catches it.

podman ps shows the container as unhealthy if either fails. Probe manually at http://dankclaw:3005/health.

Image generation (ComfyUI)

Image generation rides the local ComfyUI integration, not the chat picker — ComfyUI's models never appear in the chat model picker (that is chat LLMs only), so ComfyUI needs its own image-generation connection configured in OpenWebUI. ComfyUI itself runs GPU-enabled on Dankclaw and serves :8188.

Endpoint: POST /api/images/generations (image 0.11.x: upstream moved the route to POST /api/v1/images/generations; the comfyui engine is still upstream) with Authorization: Bearer <token> and body {"model":"imagegen","prompt":"…","n":1,"size":"1024x1024"}; the response carries base64 image data or a URL.

Failure modes: connection refused ⇒ OpenWebUI or ComfyUI is down; timeouts on large sizes ⇒ check ComfyUI resources; 401 ⇒ the token is bad.

Retired instances

  • The compose open-webui service (port 3000) was never run: 3000 is Gitea's HTTP port, so it could not bind.
  • The bare-container open-webui on Dankserver (port 3000) pointed at a dead backend (:9090) and was removed. Its data dir is preserved on Dankserver in case it is needed.
  • The bare-container searxng on Dankserver (port 8081) was removed. SearXNG now runs only on Dankclaw behind Caddy.

Gotchas

  • The image tracks upstream main; pin a digest in the quadlet unit if reproducible rollback matters more than tracking. (The old "CPU-only host" framing is stale: this APU runs the gfx1151 ROCm build — see ComfyUI.)
  • The default admin account is created on first login; secure it immediately.
  • If the backend is loading (3–8 minute model load), OpenWebUI shows connection errors until the model is ready.
  • The on-host LLM services (frank-large :7770, frank-small :7771) are reached via host.containers.internal, NOT the host LAN IP: the container's netavark bridge cannot hairpin to its own host's non-published ports via 192.168.0.143 (connection refused), and the bridge gateway (10.89.3.1) lives in the rootless netns while llama-server runs in the host netns, so it refuses too. host.containers.internal (169.254.1.2) resolves to the host on the bridge and works — verified end-to-end for both endpoints. The Dankserver backend (frank-medium) is a different host, reached via its LAN IP 192.168.0.235:7770.

History

  • 2026-08-22 — slirp4netns → pinned bridge. The unit ran on Network=slirp4netns. The slirp4netns helper process died (no OOM, no kill entry, no crash log found), and the container kept running with only lo in its netns: published port 3005 accepted TCP and reset every request, so chat served empty responses for ~2 days. Nothing logged an error. Moving to a kernel bridge removes the per-container helper entirely, and the healthcheck's interface check makes the zombie state visible if it ever returns.

  • 2026-09-21 — model picker rebuilt for the 3-model fleet. The picker carried 39 stale rows (37 dead Ollama models + 2 frank-default-*) and was missing frank-medium. Rebuilt the model table to exactly frank-large/frank-medium/frank-small and set model_ids on each connection (llama.cpp's Ollama-shaped /v1/models is invisible to OpenWebUI's discovery). Pointed the two on-host endpoints at host.containers.internal — the bridge refuses the host LAN IP for non-published ports. All three verified end-to-end via /openai/chat/completions.

  • 2026-09-24 — SSO via authentik. OIDC authorization-code flow wired per the SSO (authentik) section: application/provider/entitlements created in authentik, client secret in the host env file, roles from the roles scope claim. E2E coverage in tests/e2e/specs/18-openwebui-sso.spec.ts.

  • 2026-09-24 — flipped to SSO-only. The OIDC round-trip verified in a real browser (login view renders no password form, only "Continue with authentik", and the flow lands on a signed-in session), so the local username/password path was retired: ENABLE_LOGIN_FORM=false + ENABLE_PASSWORD_AUTH=false in the quadlet and ui.enable_login_form set to false in the config DB (the DB wins over env, so both were changed together). qa-e2e-admin was demoted out of the authentik Admins group (it keeps openwebui_users / homepage-admins for app access).

See Also