feat(docker): full self-hosted Compose support
Make the web image drop-in for a Honcho docker-compose stack:
- nginx reverse-proxies /v3 and /health to $HONCHO_UPSTREAM (variable +
Docker resolver so it starts even before the upstream resolves), giving
the SPA a same-origin path to Honcho with no browser CORS.
- Runtime config: an entrypoint writes config.js from
OPENCONCHO_DEFAULT_HONCHO_URL, so one prebuilt image targets any backend
("same-origin" | absolute URL | empty). SPA seeds a first-run default
instance from it (additive; no-op in dev/desktop).
- docker-compose.yml example service + GHCR multi-arch publish workflow on
release.
- nginx.conf -> envsubst template; docs rewritten.
Closes #21. Closes #31.
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
4
packages/web/public/config.js
Normal file
4
packages/web/public/config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Runtime configuration placeholder. In the Docker image this file is
|
||||
// regenerated at container start from the OPENCONCHO_DEFAULT_HONCHO_URL env.
|
||||
// In dev and the desktop build it stays a no-op.
|
||||
window.__OPENCONCHO_DEFAULT_HONCHO_URL__ = "";
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { httpFetch } from "@/lib/http";
|
||||
import { runtimeDefaultBaseUrl } from "@/lib/runtimeConfig";
|
||||
|
||||
const LEGACY_KEY = "openconcho:config";
|
||||
const STORE_KEY = "openconcho:instances";
|
||||
@@ -73,6 +74,18 @@ export function loadStore(): InstanceStore {
|
||||
}
|
||||
const migrated = migrateLegacy();
|
||||
if (migrated) return migrated;
|
||||
|
||||
// First-run default from a container's runtime config (no-op outside Docker).
|
||||
const runtimeUrl = runtimeDefaultBaseUrl();
|
||||
if (runtimeUrl) {
|
||||
const inst: Instance = {
|
||||
id: "runtime-default",
|
||||
name: "Honcho",
|
||||
baseUrl: runtimeUrl,
|
||||
token: "",
|
||||
};
|
||||
return { instances: [inst], activeId: inst.id };
|
||||
}
|
||||
return { instances: [], activeId: null };
|
||||
}
|
||||
|
||||
|
||||
24
packages/web/src/lib/runtimeConfig.ts
Normal file
24
packages/web/src/lib/runtimeConfig.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
const GLOBAL_KEY = "__OPENCONCHO_DEFAULT_HONCHO_URL__";
|
||||
const SAME_ORIGIN = "same-origin";
|
||||
|
||||
/**
|
||||
* Runtime-injected default Honcho base URL for container deployments.
|
||||
*
|
||||
* The Docker image writes `/config.js` from the `OPENCONCHO_DEFAULT_HONCHO_URL`
|
||||
* env at container start, so one prebuilt image can target any backend without
|
||||
* a rebuild (Vite envs are baked at build time and can't do this).
|
||||
*
|
||||
* - `"same-origin"` → the page's own origin (pairs with the nginx `/v3` reverse
|
||||
* proxy, so the browser makes same-origin requests and CORS never applies)
|
||||
* - an absolute URL → that URL
|
||||
* - empty / unset → `null` (no default; the user configures in Settings)
|
||||
*/
|
||||
export function runtimeDefaultBaseUrl(): string | null {
|
||||
const raw = (globalThis as Record<string, unknown>)[GLOBAL_KEY];
|
||||
if (typeof raw !== "string" || raw.trim() === "") return null;
|
||||
const value = raw.trim();
|
||||
if (value === SAME_ORIGIN) {
|
||||
return typeof location !== "undefined" ? location.origin : null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
29
packages/web/src/test/runtime-config.test.ts
Normal file
29
packages/web/src/test/runtime-config.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { runtimeDefaultBaseUrl } from "@/lib/runtimeConfig";
|
||||
|
||||
const KEY = "__OPENCONCHO_DEFAULT_HONCHO_URL__";
|
||||
|
||||
afterEach(() => {
|
||||
delete (globalThis as Record<string, unknown>)[KEY];
|
||||
});
|
||||
|
||||
describe("runtimeDefaultBaseUrl", () => {
|
||||
it("returns null when the global is unset", () => {
|
||||
expect(runtimeDefaultBaseUrl()).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when the global is blank", () => {
|
||||
(globalThis as Record<string, unknown>)[KEY] = " ";
|
||||
expect(runtimeDefaultBaseUrl()).toBeNull();
|
||||
});
|
||||
|
||||
it("returns an absolute URL verbatim", () => {
|
||||
(globalThis as Record<string, unknown>)[KEY] = "https://honcho.example.net";
|
||||
expect(runtimeDefaultBaseUrl()).toBe("https://honcho.example.net");
|
||||
});
|
||||
|
||||
it("resolves 'same-origin' to the page origin", () => {
|
||||
(globalThis as Record<string, unknown>)[KEY] = "same-origin";
|
||||
expect(runtimeDefaultBaseUrl()).toBe(location.origin);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user