From 8e61c80aa26033522d7e835f41149da277e0344f Mon Sep 17 00:00:00 2001 From: Mark van Dijk Date: Fri, 25 Sep 2026 06:48:50 +0200 Subject: [PATCH] feat(web): support serving under a sub-path via Vite base Derive the router basepath, the same-origin /api prefix, config.js and favicon URLs from import.meta.env.BASE_URL, so a build with --base /honcho/ works behind a reverse proxy. Default base stays /, so nothing changes for existing deployments. The Docker image takes a BASE_PATH build arg. --- Dockerfile | 5 +++- docs/docker.md | 23 +++++++++++++++++++ packages/web/index.html | 2 +- .../web/src/components/layout/Sidebar.tsx | 2 +- packages/web/src/lib/dispatch.ts | 2 +- packages/web/src/main.tsx | 1 + packages/web/src/routes/settings.tsx | 2 +- packages/web/src/test/dispatch.test.ts | 18 +++++++++++++++ 8 files changed, 50 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6ac2e92..1427e1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,10 @@ RUN pnpm install --frozen-lockfile --filter @openconcho/web... # Copy remaining sources + build. COPY . . -RUN pnpm --filter @openconcho/web build +# Public URL path the SPA is served under (e.g. /honcho/ behind a reverse proxy +# that strips the prefix). Must start and end with a slash. +ARG BASE_PATH=/ +RUN pnpm --filter @openconcho/web build --base "$BASE_PATH" # ---------- Runtime stage ---------- # Unprivileged variant runs as UID 101 with no root setup steps, so it works diff --git a/docs/docker.md b/docs/docker.md index be31dd3..1a12dba 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -88,6 +88,29 @@ with `--tmpfs /tmp --tmpfs /var/cache/nginx`. Note: the entrypoint writes `--read-only` either bind-mount those paths or leave the env empty and configure the URL in Settings. +## Serving under a sub-path + +To host the UI under a path such as `https://example.net/honcho/`, build with the +`BASE_PATH` build arg (leading and trailing slash required) and let your reverse +proxy strip the prefix: + +```bash +docker build --build-arg BASE_PATH=/honcho/ -t openconcho-web:honcho . +docker run -d -p 127.0.0.1:8080:8080 \ + -e OPENCONCHO_DEFAULT_HONCHO_URL=http://host.docker.internal:8000 openconcho-web:honcho +``` + +```nginx +location = /honcho { return 308 /honcho/; } +location /honcho/ { + proxy_pass http://127.0.0.1:8080/; # trailing slash strips /honcho +} +``` + +Assets, routes, `config.js`, and the `/api` proxy all resolve under the prefix +(`/honcho/api/*`). Outside Docker, pass the same flag to the web build: +`pnpm --filter @openconcho/web build --base /honcho/`. + ## SSRF: when to set the allowlist The header-driven proxy forwards to whatever upstream the client names. With the diff --git a/packages/web/index.html b/packages/web/index.html index 4487efd..090a4e5 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -7,7 +7,7 @@ OpenConcho - +
diff --git a/packages/web/src/components/layout/Sidebar.tsx b/packages/web/src/components/layout/Sidebar.tsx index 97c9d62..46d3049 100644 --- a/packages/web/src/components/layout/Sidebar.tsx +++ b/packages/web/src/components/layout/Sidebar.tsx @@ -131,7 +131,7 @@ export function Sidebar() {
OpenConcho
OpenConcho { }); }); +describe("dispatchFor — sub-path build", () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + }); + + it("prefixes the /api proxy with the Vite base", async () => { + vi.stubEnv("BASE_URL", "/honcho/"); + vi.resetModules(); + const mod = await import("@/lib/dispatch"); + mockIsTauri.mockReturnValue(false); + expect(mod.API_PREFIX).toBe("/honcho/api"); + expect(mod.dispatchFor({ baseUrl: "https://h.example" }).baseUrl).toBe( + `${location.origin}/honcho/api`, + ); + }); +}); + describe("dispatchFor — tauri mode", () => { it("targets the absolute URL with no upstream header", () => { mockIsTauri.mockReturnValue(true);