The local username/password path is retired (ENABLE_LOGIN_FORM + ENABLE_PASSWORD_AUTH false, ui.enable_login_form DB override false) and qa-e2e-admin is out of the authentik Admins group. Documented in the SSO section and History.
12 KiB
type, description, tags, timestamp
| type | description | tags | timestamp | ||
|---|---|---|---|---|---|
| reference | OpenWebUI is a self-hosted chat interface for LLMs. |
|
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/modelsis Ollama-shaped. llama.cpp returns{"models":[{"name":...}]}; OpenWebUI'sextract_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 setmodel_idsin each connection'sapi_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 usesmodel_idstoo 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 (dankserver192.168.0.235) but not to its own host's non-published ports via the host LAN IP (192.168.0.143:7770/7771is 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.
Web search
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=jsondirectly — gluetun's published port, NOT thesearch.franksdank.cloudvhost. 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). Thepass_iplist is checked FIRST and short-circuits the whole botdetection chain, and OpenWebUI's bridge egress (10.89.x) is inpass_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(slugopen-webui) with the OAuth2 providerOpen WebUI(confidential client,authorization_code+refresh_token, Strict redirect URIhttps://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 viaEnvironmentFile=). - Roles: the scope mapping
Open WebUI roles(scoperoles) reads the application entitlementsOpen WebUI Users→userandOpen WebUI Admins→admin. The entitlements are bound to the groupsopenwebui_users/openwebui_admins.akadmin(frank8465@protonmail.com) is inopenwebui_admins;qa-e2e-adminis inopenwebui_users. - Account merge:
OAUTH_MERGE_ACCOUNTS_BY_EMAIL=truefolds 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=truelets otheropenwebui_userscreate accounts on first login. - IdP reachability: the container is on its own bridge (10.89.3.0/24) and
cannot reach
authentik-server:9000by name. It reaches the IdP over the public URLhttps://auth.franksdank.cloud— Caddy publishes0.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=falsehides the form andENABLE_PASSWORD_AUTH=falserefuses password logins, so the only way in is the authentik OIDC flow. Theui.enable_login_formconfig-DB override was flipped tofalsein 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: setui.enable_login_formback totruein 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:
- App liveness —
GET http://127.0.0.1:8080/healthreturns 200 ({"status":true}). - Network liveness — a non-loopback interface exists in
/sys/class/net. The dead-tap zombie state (History) leaves onlylobehind, 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.
Retired instances
- The compose
open-webuiservice (port 3000) was never run: 3000 is Gitea's HTTP port, so it could not bind. - The bare-container
open-webuion 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
searxngon 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 viahost.containers.internal, NOT the host LAN IP: the container's netavark bridge cannot hairpin to its own host's non-published ports via192.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 IP192.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 onlyloin 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 missingfrank-medium. Rebuilt themodeltable to exactlyfrank-large/frank-medium/frank-smalland setmodel_idson each connection (llama.cpp's Ollama-shaped/v1/modelsis invisible to OpenWebUI's discovery). Pointed the two on-host endpoints athost.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
rolesscope claim. E2E coverage intests/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=falsein the quadlet andui.enable_login_formset tofalsein the config DB (the DB wins over env, so both were changed together).qa-e2e-adminwas demoted out of theauthentik Adminsgroup (it keepsopenwebui_users/homepage-adminsfor app access).
See Also
- Dankclaw Stack
- Hermes — the agent OpenWebUI can talk to
- SearXNG — the search engine on this host
- LLM knowledge base — the vLLM and llama.cpp endpoints