Open-WebUI#
Open-WebUI runs on ereshkigal as a Podman OCI container managed by the
tsunaminoai.openWebui NixOS module
(modules/nixos/containers/open-webui/default.nix). It is the LLM chat front-end
for the local stack and doubles as a RAG knowledge base over the
Paperless-NGX document library. All inference goes
through the local Ollama failover proxy on :11434
(work-laptop-m4 primary, mokou backup) — see Ollama for the
servers behind it.
- Web UI: chat.inaba.network — the user-facing
path, behind Cloudflare Access (Entra group
sg-voile-personal) - Web UI (tailnet fallback):
https://ereshkigal.<tailscaleDomain>:3001(admin break-glass, slated for removal once the CF path is verified — #177) - Module option:
tsunaminoai.openWebui.enable = true;
Architecture#
The container is ghcr.io/open-webui/open-webui:main, run on the
openwebui_default Podman network with the alias open-webui. The host port
(default 3000) maps to container port 8080, published on loopback only
whenever trustedEmailHeader is set (see below). Persistent state — including the
embedded ChromaDB vector store — lives in the named Podman volume
openwebui_data (/app/backend/data).
Chat and RAG embeddings both go to the endpoint named by the ollamaHost
option, which sets OLLAMA_BASE_URL to http://<ollamaHost>:11434 — on
ereshkigal that is the host’s own failover proxy. RAG uses the ollama
embedding engine backed by nomic-embed-text; web search is disabled
(ENABLE_RAG_WEB_SEARCH = "false") so retrieval stays local. The container is
started with --add-host=host.containers.internal:host-gateway so the host’s
FQDN (and the proxy upstreams behind it, over Tailscale) resolve correctly.
WEBUI_AUTH = "true", so login is required and the first user to register
becomes the admin.
Authentication (Cloudflare Access trusted header)#
trustedEmailHeader = "X-Remote-User" sets Open-WebUI’s
WEBUI_AUTH_TRUSTED_EMAIL_HEADER: the app trusts the email in that header as
the logged-in user and auto-provisions the account. That is a complete
authentication bypass for anyone who can set the header, so the module makes
the header unforgeable rather than merely unlikely:
- the
chatCloudflare origin vhost overwritesX-Remote-Userwith$http_cf_access_authenticated_user_email(validated at the CF edge); - the tailnet vhost on
port + 1clears it (proxy_set_header X-Remote-User "";), so LAN/tailnet clients cannot inject an identity; - the container publish becomes
127.0.0.1:<port>andportis dropped from the firewall, so there is no unproxied route to the app at all.
Removing any one of the three re-opens the bypass — change them together.
Note
When tsunaminoai.pki.acme.enable is true, the module also stands up an
nginx vhost on port + 1 (3001) that reverse-proxies the web UI over TLS
using the host’s step-ca ACME cert
(useACMEHost = ereshkigal.<tailscaleDomain>). WEBUI_URL is set to that
HTTPS origin unless externalUrl overrides it (ereshkigal points it at
https://chat.inaba.network) so absolute links and CSRF/CORS checks behave
behind the proxy. The firewall opens port + 1 when ACME is active; port
itself is only opened when trustedEmailHeader is unset.
Enabling the module#
# hosts/x86_64-nixos/ereshkigal/default.nix
tsunaminoai.openWebui = {
enable = true;
# The host's own FQDN = the local Ollama failover proxy on :11434
# (work-laptop-m4 primary → mokou backup).
ollamaHost = "ereshkigal.${config.tsunaminoai.nix.tailscaleDomain}";
port = 3000;
paperlessPort = 8011;
externalUrl = "https://chat.inaba.network";
trustedEmailHeader = "X-Remote-User";
};
Module options#
| Option | Default | Description |
|---|---|---|
enable |
false |
Enable Open-WebUI |
ollamaHost |
"localhost" |
Hostname/IP of the Ollama server (chat + embeddings); ereshkigal points it at its own failover-proxy FQDN |
port |
3000 |
Host port for Open-WebUI (HTTP); HTTPS is port + 1 |
paperlessPort |
8011 |
Port Paperless-NGX listens on (used by the sync timer) |
externalUrl |
null |
Public URL behind Cloudflare Access; sets WEBUI_URL. Null = the tailnet vhost URL |
trustedEmailHeader |
null |
Header holding the pre-authenticated email (WEBUI_AUTH_TRUSTED_EMAIL_HEADER). Forces the loopback publish + closed firewall port |
trustedNameHeader |
null |
Optional display-name header (WEBUI_AUTH_TRUSTED_NAME_HEADER) |
Environment variables#
| Variable | Value | Purpose |
|---|---|---|
OLLAMA_BASE_URL |
http://<ollamaHost>:11434 |
Chat inference endpoint |
WEBUI_AUTH |
"true" |
Require login; first user becomes admin |
WEBUI_URL |
externalUrl or https://<host>:3001 |
Public URL for link generation / CSRF |
WEBUI_AUTH_TRUSTED_EMAIL_HEADER |
trustedEmailHeader |
Trusted-header SSO — only set when the option is |
RAG_EMBEDDING_ENGINE |
"ollama" |
RAG embedding backend |
RAG_EMBEDDING_MODEL |
"nomic-embed-text" |
Embedding model behind ollamaHost |
OLLAMA_BASE_URL_FOR_EMBEDDINGS |
http://<ollamaHost>:11434 |
Embedding endpoint |
ENABLE_RAG_WEB_SEARCH |
"false" |
Keep RAG local — no outbound web search |
VECTOR_DB |
"chroma" |
Embedded ChromaDB in openwebui_data |
Paperless → Open-WebUI RAG sync#
A nightly systemd timer, paperless-openwebui-sync (OnCalendar = 02:00,
Persistent so it catches up after downtime), pulls already-OCR’d Paperless-NGX
documents and upserts them into an Open-WebUI Knowledge collection via the
Knowledge API. The one-shot service reads two sops secrets at start
(ExecStartPre writes them to a runtime EnvironmentFile):
paperless/api-token— Paperless admin → Profile → API Tokenopenwebui/api-key— Open-WebUI → Settings → API Keys
Both must be created after first boot. The sync talks to Paperless at
http://127.0.0.1:<paperlessPort> and Open-WebUI at http://127.0.0.1:<port>.
Dashboard#
When tsunaminoai.homer.enable is set, the module registers a Homer tile
(“Open-WebUI”, under the Media group, ai tag) pointing at the HTTPS origin
when ACME is active, otherwise the plain HTTP URL.
Related#
- Ollama — the inference servers behind the failover proxy
- Paperless-NGX — the document source for RAG sync
- ereshkigal — the host this runs on
- Container modules — overview of the Podman container modules