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:
Offending Commit
2026-05-28 16:06:03 -05:00
parent dd2edbae99
commit 282ba1b76c
12 changed files with 278 additions and 106 deletions

View File

@@ -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 };
}

View 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;
}