Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<title>OpenConcho</title>
<meta name="description" content="Frontend for self-hosted Honcho instances — browse memories, chat with memory context" />
<!-- Runtime config (regenerated by the Docker image at start; no-op otherwise) -->
<script src="/config.js"></script>
<script src="%BASE_URL%config.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function Sidebar() {
<div className="px-3 sm:px-5 py-5" style={{ borderBottom: "1px solid var(--border)" }}>
<div className="flex items-center gap-2.5 justify-center sm:justify-start">
<img
src="/favicon.svg"
src={`${import.meta.env.BASE_URL}favicon.svg`}
alt="OpenConcho"
className="w-7 h-7 rounded-lg shrink-0"
style={{ boxShadow: `0 0 16px ${COLOR.accentGlow}` }}
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/lib/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { httpFetch } from "@/lib/http";
import { isTauri } from "@/lib/platform";

/** Same-origin path prefix the web build issues all Honcho calls through. */
export const API_PREFIX = "/api";
export const API_PREFIX = `${import.meta.env.BASE_URL}api`;
/** Request header naming the real Honcho upstream for the proxy to forward to. */
export const UPSTREAM_HEADER = "X-Honcho-Upstream";
/** Response header the proxy sets on its OWN refusals (so they aren't read as upstream auth). */
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const queryClient = new QueryClient({

const router = createRouter({
routeTree,
basepath: import.meta.env.BASE_URL,
defaultPreload: "intent",
scrollRestoration: true,
});
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/routes/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function SettingsPage() {
>
<div className="mb-8 text-center">
<img
src="/favicon.svg"
src={`${import.meta.env.BASE_URL}favicon.svg`}
alt="OpenConcho"
className="w-14 h-14 rounded-2xl mx-auto mb-4"
style={{ boxShadow: "0 0 32px rgba(99,102,241,0.35)" }}
Expand Down
18 changes: 18 additions & 0 deletions packages/web/src/test/dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ describe("dispatchFor — web mode", () => {
});
});

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);
Expand Down