A service/job's env is now exactly its defaults.env — castle injects no hidden
convention vars. Values support ${port}/${data_dir}/${name} placeholders
(resolved at deploy, alongside ${secret:…}), so a program's own env var names
map to castle's computed values without hardcoding.
Why: the auto-injected <PREFIX>_PORT/<PREFIX>_DATA_DIR were a guess at the
program's env names — right for castle-scaffolded services, dead weight for
adopted ones (lakehouse carried two dead vars; notification-bridge/backup jobs
too). They also weren't visible in the config editor (computed at deploy), which
was the source of the 'four env vars but the UI shows none' mystery.
- core: resolve_env_vars gains a context (${port}/${data_dir}/${name});
deploy builds env from defaults.env only — no <PREFIX>_* injection, no
port_env. Removed the port_env field and the dead _env_prefix helper.
- cli: 'service/job create' gains repeatable --env KEY=VALUE (replaces
--port-env); 'program create' scaffolds <PREFIX>_PORT/_DATA_DIR: ${…} for new
daemons.
- app: removed the 'Port env' field; the Environment editor (defaults.env) is
the single place, with a placeholder hint.
- live migration: central-context/castle-api/power-graph/protonmail mapped their
real vars explicitly; lakehouse → just LAKEHOUSED_DAEMON_PORT: ${port}, data
stays in ~/.lakehoused. Verified all services healthy on their ports, dead
vars gone, zero failed units.
- docs: registry.md/design.md/stack guides + findings updated to the explicit
model.
core 94 / cli 24 / api 52 green; ruff + app build clean.
91 lines
4.6 KiB
Markdown
91 lines
4.6 KiB
Markdown
# Findings — adopting `lakehouse` as a castle service
|
|
|
|
A completeness/UX test: take an existing daemon (`/data/repos/lakehouse`, a
|
|
FastAPI `lakehoused` server on port 8420 that bundles its own SPA, normally
|
|
started by a bespoke `lakehouse start`) and bring it fully under castle —
|
|
systemd-managed, health-checked, proxied, on the dashboard.
|
|
|
|
## What worked cleanly
|
|
|
|
- `castle add` auto-detected `behavior: daemon` + `stack: python-fastapi` from
|
|
the pyproject (fastapi/uvicorn in deps).
|
|
- Adding the service via the config API (`PUT /config/services/lakehouse`)
|
|
validated and persisted correctly.
|
|
- `castle deploy lakehouse` produced a correct systemd unit, registry entry,
|
|
and Caddy route, and auto-installed the editable package.
|
|
- `castle service enable lakehouse` started it and enabled it on boot.
|
|
- Health probe (direct `127.0.0.1:8420`) and dashboard data
|
|
(program / service / deployment / `active`) were all correct.
|
|
|
|
## Gaps found
|
|
|
|
### #1 — No CLI/app path to create a service from an adopted daemon (High)
|
|
|
|
`castle add` writes a `daemon`-behavior **program**, but there is no command to
|
|
turn it into a running **service** (port, health, proxy, systemd). We had to
|
|
hand the service block to the config API. Worse: `castle install <daemon>` on a
|
|
program with no service silently falls through to the *tool* install path
|
|
(`uv tool install`) instead of running it — a dead end.
|
|
|
|
**Fix:** `castle expose <program> [--port --health --path]` to scaffold a
|
|
service entry from an existing program. (App needs an equivalent "add service"
|
|
flow — tracked separately.)
|
|
|
|
### #2 — castle can't actually configure an adopted program's port / data dir (Medium)
|
|
|
|
The generated unit injects castle's convention vars `LAKEHOUSE_PORT=8420` and
|
|
`LAKEHOUSE_DATA_DIR=…`, but `lakehoused` reads **`LAKEHOUSED_DAEMON_PORT`** and
|
|
its own storage paths — it ignores both. The port "worked" only because 8420 is
|
|
the daemon's built-in default and we declared the same value. Castle's
|
|
convention assumes a castle-aware program; an adopted one doesn't read
|
|
`<PREFIX>_PORT`.
|
|
|
|
**Fixed** (superseding the interim `port_env` field): auto-injection of the
|
|
convention vars was dropped entirely. A service's env is now exactly its
|
|
`defaults.env`, with `${port}`/`${data_dir}`/`${name}` placeholders for castle's
|
|
computed values. lakehouse maps just what it reads —
|
|
`LAKEHOUSED_DAEMON_PORT: ${port}` — and keeps its own data root, with no dead
|
|
vars. Castle-native services map their `<PREFIX>_PORT`/`_DATA_DIR` the same way
|
|
(scaffolded by `castle program create`).
|
|
|
|
### #3 — `castle deploy` regenerates the Caddyfile but never reloads Caddy (High, bug)
|
|
|
|
Deploy wrote the new `/lakehouse` route to the Caddyfile on disk but didn't
|
|
reload the running gateway, so every `/lakehouse/*` request fell through to the
|
|
castle-app catch-all and returned the dashboard's `index.html` (a misleading
|
|
`200`). `castle service enable` doesn't reload either, and the deploy output
|
|
never mentions `castle gateway reload`.
|
|
|
|
**Fix:** `castle deploy` (and `service enable`) reload the gateway when proxy
|
|
routes changed.
|
|
|
|
### #4 — Path-prefix proxying breaks root-based SPAs (Medium, architectural)
|
|
|
|
`lakehoused`'s bundled webapp is built with Vite `base: "/"` — its `index.html`
|
|
references assets at absolute `/assets/…`. Proxied at `/lakehouse/`, the browser
|
|
requests `/assets/…` (no prefix), which hits the castle-app catch-all → the UI
|
|
can't boot. The API works through the gateway; the UI only works directly at
|
|
`:8420/`.
|
|
|
|
Two distinct cases, because Vite bakes `base` in at **build** time (a runtime
|
|
env in the unit is too late to rebase a pre-built bundle):
|
|
|
|
- **Case A — frontends castle builds** (the `react-vite` stack, served by
|
|
`file_server`). Castle runs `pnpm build` *and* knows the serve prefix, so it
|
|
can pass `--base=/<prefix>/` automatically. Today `power-graph-app` only works
|
|
because its base was hand-set to match.
|
|
**Fix:** derive the build `--base` from the serve path prefix.
|
|
- **Case B — adopted daemons that self-serve a root SPA** (lakehouse). Castle
|
|
never runs their build, so it can't pass `--base`. The clean answer is
|
|
**host-based routing** (`lakehouse.civil.lan → :8420`) so `base: "/"` stays
|
|
valid — also a generally useful castle capability.
|
|
**Fix:** support proxy-by-hostname in the Caddyfile generator + manifest.
|
|
|
|
### Nits
|
|
|
|
- **Redundant editable reinstall:** `castle deploy` installs the package, then
|
|
`castle install`/activate may `uv tool install` it again.
|
|
- **Entry-point discovery:** the service's `run.program` had to be `lakehoused`,
|
|
not `lakehouse` — the package ships two console scripts and castle gives no
|
|
hint which is the server. Surfacing a package's `[project.scripts]` would help.
|