This directory is the self-contained, reproducible build that lets Open WebUI
run under a URL subpath (e.g. /chat) so it shares one origin and one port
with the Studio Django app, instead of living on a separate cross-origin host
embedded in an iframe.
It is the working path that spencercjh implemented in their fork (spencercjh/open-webui) — here as one auditable patch + a build script, not a fork to maintain by hand.
Stock Open WebUI bakes every reference to the site root:
/static/favicon.png /_app/immutable/entry/start.js
/api/v1/config /ws/socket.io
Put it behind a reverse proxy at /chat/ and the browser still asks for
/static/..., /api/..., /ws/socket.io — none of which match the /chat
prefix, so they fall through to the host app (Django) and the page is a white
screen. Proxy rewriting alone cannot fix this because new hardcoded routes keep
appearing (the classic nginx sub_filter whack-a-mole).
The fix is to build Open WebUI so the frontend and backend both know their
base path is /chat, so it generates URLs under /chat. That is exactly
what subpath.patch does.
| File | Purpose |
|---|---|
subpath.patch |
The subpath change (121 files, +631/−296). Verified to apply cleanly to the pinned base and to reproduce the fork's tree byte-for-byte. |
0002-node-build-heap.patch |
Historical standalone form of the Node heap fix; the same change is now included in subpath.patch. |
0003-otel-wheel-dependencies.patch |
Adds OpenTelemetry instrumentation dependencies to wheel metadata. Docker already installs them from backend/requirements*.txt; the wheel needs them for embedded Studio mode. |
build.sh |
Clones the pinned upstream, applies all maintained patches, and builds with the upstream project's own (now-patched) Dockerfile. |
Caddyfile.subpath |
The single-origin routing (forward-auth + handle /chat/* no-strip + fallthrough to Studio). Reference for cutover. |
DESIGN.md |
Distribution & maintenance decision record (fork vs. artifacts repo, why Docker/ghcr.io is the channel, why pip install is a trap as-is). |
UPGRADE.md |
Step-by-step procedure + copy-paste agent prompt for rebasing the patch onto a new upstream version. |
cd deploy/openwebui-subpath
./build.sh # -> simpleaudit/open-webui-subpath:chat (WEBUI_SUBPATH=/chat)
SUBPATH=/openwebui ./build.sh # different prefixThe script stops with a clear error if the patch does not apply to the
pinned SHA. The upstream Dockerfile gets WEBUI_SUBPATH as a build and
runtime arg purely from the patch — there is no custom Dockerfile here.
Caddyfile.subpath is the new shape. The load-bearing line:
handle /chat/* { # `handle`, NOT `handle_path`
reverse_proxy open-webui:8080
}
handle { # everything else -> Studio
reverse_proxy web:8000
}handle_path (which strips the prefix) breaks the WebSocket: the subpath
fork serves its socket at the full /chat/ws/socket.io (engineio matches the
raw path, not the root_path-relative one), so a stripped request 404s.
handle forwards /chat/... as-is, which is what the build expects. This was
verified live: raw WS handshake through Caddy returned 101 Switching Protocols.
Against a real Caddy + this image + a mock OpenAI endpoint, in a real browser:
GET /chat/→ 200 SPA with 65/chat/-prefixed refs, 0 root-relative- Raw WebSocket upgrade
/chat/ws/socket.io→ 101 - Fallthrough:
/dashboard/,/audits/,/api/*→ Studio (Studio's log shows only the explicit probes — zero OWUI leakage) - Trusted-header SSO sign-in (the same
/chat/authzendpoint the existing forward-auth proxy uses) - Real LLM conversation: registered an endpoint, sent "Say hello", got the
streamed reply back at
/chat/c/<id>; backend logPOST /chat/api/chat/completions → 200 - Patch reproducibility: fresh
git cloneof upstream →git apply→ all 9 core files byte-identical to the fork
~9 files carry the logic; ~112 Svelte files are mechanical base prefixing.
backend/.../env.py— reads + validatesWEBUI_SUBPATHbackend/.../main.py—FastAPI(root_path=WEBUI_SUBPATH); manifest/swagger URLs prefixed;/api/v1/configreturnssubpathbackend/.../__init__.py—uvicorn.run(root_path=WEBUI_SUBPATH)(serve + dev)backend/.../socket/main.py—socketio_path=f'{WEBUI_SUBPATH}/ws/socket.io'backend/.../utils/asgi_middleware.py,routers/models.py— keeproot_pathin redirectssvelte.config.js—paths = { base: WEBUI_SUBPATH, relative: false }vite.config.ts—base: WEBUI_SUBPATHsrc/lib/constants.ts—WEBUI_BASE_URL = base(the single choke point for all API calls)src/app.html— hardcoded/static/...→%sveltekit.assets%/...Dockerfile—ARG/ENV WEBUI_SUBPATH(build + runtime)src/routes/+layout.svelte+ ~111 more —goto/href/location.href→${base}/...; socket.io subpath goes onpath, never the namespace arg
Known fork defect (v0.12.0), fixed at the source: the port shipped 12 base-prefixings
where ${base} renders as LITERAL text — navigation calls written as quoted JS strings
(goto("${base}/workspace/tools")) instead of template literals. The visible symptom is
"Back" from a KB / tool / prompt / skill / model / functions / automations detail page
navigating to /chat/workspace/<section>/$%7Bbase%7D/workspace/<section> ("Not found").
This is not repairable at the Caddy front door: this Caddy build treats a bare $ in a
path matcher as a placeholder, and path_regexp is non-functional in v2.11.7 (verified:
even path_regexp foo never matches). The fix is to patch the installed frontend bundle so
the 12 quoted "${base}/…" args become template literals `${base}/…`. They live under
open_webui/frontend/_app/immutable/{nodes,chunks}/*.js (10 files). A helper does it:
uv run python scripts/patch_owui_base.py # patch in place (idempotent)
uv run python scripts/patch_owui_base.py --check # report only, no writesStatic files, so no OWUI restart is needed — but re-running Studio's dev server re-extracts
the wheel and wipes the patch, so re-run the script after a full restart (or rebuild the fork
image with real ${base} interpolation, which removes the need entirely). When re-porting to a
newer SHA, grep the regenerated patch for ["']\$\{base\}/ (quoted, not backticked) to check
whether the defect persists.
# 1. Dry-run the patch against the new upstream commit
git clone --filter=blob:none --no-checkout https://github.com/open-webui/open-webui.git o
git -C o checkout <new-sha>
git -C o apply --check subpath.patch # non-zero = needs a rebaseFull procedure: UPGRADE.md — including a copy-paste
agent prompt with the subpath invariants to preserve and the evidence bar to
hit. v0.11.4 drift was 33/121 files clean, 88 with conflicts (8 of the 9 core
files) and was ported on 2026-10-04; v0.12.0 was ported on 2026-10-14
(see UPGRADE.md outcome log). Treat each upstream bump as a porting task, not
a re-apply. Do not promote a SHA until apply --check passes and the E2E
(WS 101 + one LLM round-trip) re-passes. Distribution context (fork, ghcr.io,
pip caveats): DESIGN.md.
Built, E2E-verified, published, and wired in (2026-10-14).
subpath.patch is the v0.12.0 port (198 files) pinned to 15818d88; the fork
github.com/sushantgautam/open-webui ships main = v0.12.0 + subpath with CI
publishing to ghcr.io (root on push, -subpath on webui_subpath dispatch).
The cutover is done: docker-compose.yml pulls
ghcr.io/sushantgautam/open-webui:v0.12.4.10-subpath (set WEBUI_SUBPATH=/chat),
chat-proxy (Caddy, no profile) is the only published port and runs
Caddyfile.subpath, Studio's wrapper page moved to /playground/, and
Caddyfile.chat + the separate cross-origin port are retired. Embedded
(no-Docker) mode runs the same Caddy forward-auth — the bundled caddyserver
wheel (all platforms, incl. Windows) — on its own port.
Both modes' /static/custom.css requests go to Studio's /chat/css-mask,
which serves the admin skin per session (no Caddy-side state).