2026-06-21 Open WebUI Deployment and Auth Cascade
What I set out to do
Started as “what has Open WebUI shipped upstream in the last 6 months, and how do I wire up the bits I’m not using” — custom MCP connectors, native/auto tool calling, image generation, the new desktop app. Turned into a full deployment hardening pass plus a four-bug debugging cascade once I actually tried to chat.
What I actually did
Worked it end to end with Claude Code over a long session. All declarative in nix home-manager; the container runs ENABLE_PERSISTENT_CONFIG=False, so everything has to be env or a SQLite seed — the UI doesn’t persist.
Surveyed upstream (0.9.x). Desktop app, scheduled automations, KB overhaul (directory sync, filesystem tool), native MCP connectors, native function calling, redirect-based SSRF protection.
Desktop app. Installed via Homebrew cask (brew install --cask open-webui) — confirmed no nix-homebrew/Brewfile exists, so it’s a manual Tier-1.5 install. Wrote open-webui-desktop.nix to seed the app’s config.json (server connection + shell settings) via an activation-time jq merge — the app rewrites that file at runtime so a read-only home.file symlink is wrong; mirrors how the web UI’s settings get seeded into SQLite rather than symlinked.
Container features. Added native function calling (DEFAULT_MODEL_PARAMS), image generation, and the Obsidian MCP as a TOOL_SERVER_CONNECTIONS entry (type:"mcp", loopback host.docker.internal:8788/mcp, no auth). Verified the container reaches the loopback bridge directly — no Cloudflare round-trip needed for the same-host case (the connector built earlier today via 2026-06-21 Remote Obsidian MCP Connector is for the phone).
Defaults. Set default chat model to openrouter/deepseek/deepseek-v4-flash. Turned code-interpreter / image / obsidian-tool on by default — which required reading OWUI source to learn there’s no global feature-default: the chat toggles read model.info.meta.defaultFeatureIds (+ capabilities) and meta.toolIds. So I seeded a workspace model row (base_model_id NULL → merges onto the LiteLLM-listed model by id, per utils/models.py) carrying those keys.
Image via OpenRouter. Routed image gen through openrouter/google/gemini-3.1-flash-image with an explicit mode: image_generation LiteLLM model_list entry (rather than trusting the openrouter/* wildcard for the images endpoint).
The debugging cascade (the real story)
- Chat → not_found. LiteLLM’s
defaultClaudeModelwasclaude-sonnet-4-20250514, which Anthropic retired 2026-06-15 — six days earlier. Bumped toclaude-sonnet-4-6. - DeepSeek → empty tool calls. Not a config bug: OpenRouter’s DeepSeek-V4 provider dropped the stream mid-flight (
provider_unavailable→MidStreamFallbackError, no fallbacks configured). Triangulated across LiteLLM stderr, the Obsidian bridge log (ListTools 200, noCallToolever arrived → MCP healthy), and a SigNoz trace. - Image tool → 401. OWUI was calling
api.openai.comdirectly:IMAGES_OPENAI_API_BASE_URL/KEYdo not inheritOPENAI_API_BASE_URL/KEYat runtime despite the docs. Set them explicitly to the LiteLLM proxy. - Whole app → 401.
WEBUI_SECRET_KEYis unset, andstart.shwrites its auto-generated.webui_secret_keyto the ephemeral cwd (/app/backend), not the data volume — so everyhm switchrecreate rotated the JWT key and invalidated all sessions. Resisted the static-secret hack; fixed it cleanly by pointingWEBUI_SECRET_KEY_FILEat the persisted volume. Verified the key hash is now stable across a forced recreate.
What was striking
- OWUI image failures are invisible in SigNoz traces. The image path uses
aiohttp(uninstrumented); chat useshttpx(traced). The 401 lived only in logs — but the OWUI log line carries thetrace_id, so you can still jump to the chat trace. Full distributed trace OWUI→LiteLLM→Anthropic is otherwise there, and LiteLLM spans carry thegen_ai.*bodies. - The secret-rotation fix is a path, not a secret. OWUI already generates and owns its key; it was just storing it on the ephemeral layer.
WEBUI_SECRET_KEY_FILE→ data volume is the whole fix. - No global feature-default in OWUI — “turn code interpreter on by default” is genuinely per-model (
defaultFeatureIds), so it needed a seeded workspace model row. IMAGES_OPENAI_API_BASE_URLsilently falls back to api.openai.com, not the configured OpenAI base — a doc/behavior mismatch that 401s with a placeholder key.- A retired dated model snapshot 404s the same day it’s pulled; the fix is the undated alias (
claude-sonnet-4-6). - Same pattern as the morning’s MCP debugging recurred: a flaky upstream provider looks identical to a local misconfiguration — the logs + trace + MCP-server-side log together are what separated “DeepSeek/OpenRouter is down” from “my setup is broken.”
Top tomorrow
- Clear the browser’s stale OWUI token (hard refresh / clear site data on
localhost:7878) to mint a fresh JWT under the now-stable key, then confirm chat + image both work in-app. - Decide on DeepSeek V4 Flash as default — it’s flaky via OpenRouter under native function calling (empty tool calls). Either pin a more reliable provider route or pick a different default.
- Optionally add LiteLLM fallbacks so a mid-stream provider drop fails over instead of returning empty.
Related
- Config:
open-webui.nix,open-webui-desktop.nix,litellm/default.nix - Commits on
main:4fb6f34,b024f3d,ed2cdc5,b9be974,3c96986,a73d257,ff10bc0 - Sibling entry: 2026-06-21 Remote Obsidian MCP Connector
- Concepts: Model Context Protocol, Open WebUI, LiteLLM