Phase 12: a headless hub in Docker #20

Merged
alex merged 4 commits from refactor/phase-12-headless into refactor/phase-11-discovery 2026-10-29 18:45:47 +01:00
Owner

Phase 12: a headless hub in Docker

Kanbot can now run with no desktop: a container on a home server or a VM is the hub. Browsers pair with it to use the board, and the computers that run agents join as nodes or as desktops in Connect mode. Guide: docs/headless-hub.md.

What's in it

  • Dockerfile: multi-stage, from node:24.19.0-bookworm to -slim, both pinned by digest.
    • Runs as uid 1000 under tini, with data in the /data volume.
    • NITRO_HOST=127.0.0.1: the plain listener, which trusts its callers as this computer, is reachable only by docker exec.
    • KANBOT_HUB_ENABLED=1 and KANBOT_HUB_PORT=7433 (the one EXPOSE), plus KANBOT_LOCAL_AGENTS=0.
    • HEALTHCHECK is kanbot-hub health.
    • Comes with .dockerignore and docker-compose.yml.
  • Environment overrides:
    • KANBOT_HUB_ENABLED / KANBOT_HUB_PORT decide the hub section. The app shows it locked, and Settings.update refuses changes with SettingFromEnvironment.
    • KANBOT_LOCAL_AGENTS=0 gives the hub's own machine a capacity of 0 (Machine.agentsOff).
    • KANBOT_HUB_PUBLIC_URL is the address the hub gives out: a container only knows its Docker address.
  • kanbot-hub CLI (.output/hub-cli, DB-free, over the container's loopback):
    • offer [--kind browser|node|desktop] prints the code, its expiry, what to run on the other computer, the fingerprint and the URLs.
    • status.
    • health.
    • backup <dir> (SQLite online backup plus the rest of the data dir, 0700).
  • First-pairing hint: while hub mode listens and no browser is paired, the log says how to get a first code.
  • Shared copying: scripts/bundleExternals.mjs is shared by desktop-prepare and the CLI build.

Checks

  • npm run lint, npm run typecheck, npx vitest run: 1422 passed, 2 skipped. test/hub/dockerImage.test.ts keeps the Dockerfile and compose file consistent without a daemon.
  • Real Docker (Docker Desktop 29.7.2), image 534 MB:
    • The container is healthy, runs as uid 1000, and logs the first-pairing hint.
    • With no token, whoami gets 401 and the board redirects to /pair. Port 3000 is unreachable from the host.
    • kanbot-hub status shows hub mode set by the environment.
    • kanbot-hub offer --kind node: a kanbot-node on this Mac pairs with --fingerprint and connects over pinned wss.
    • A browser pairs over HTTPS (cookie, then whoami 200).
    • The machines list shows the hub at capacity 0 (agentsOff) and the Mac's node online.
    • After docker restart, the cookie still works and the node reconnected by itself, with no new hint.
    • kanbot-hub backup gives the database (integrity ok, both devices), certificate, key, machine id and attachments.
    • With KANBOT_HUB_PUBLIC_URL, status, offer and the hint give out that address.
    • docker compose config validates.
  • The Mac's LocalHostName didn't change.

Not in it

  • A relay or NAT traversal: nodes and browsers must reach https://<host>:7433 directly (LAN, VPN, a forwarded port).
  • LAN discovery from a bridged container: it needs network_mode: host on Linux. That also makes the loopback listener the host's, so local processes on the host are trusted (documented).
  • A CA-signed certificate or a TLS-terminating proxy: nodes pin the hub's own certificate.
  • Publishing the image: nothing was published.

Base: refactor/phase-11-discovery (PR #19).

🤖 Generated with Claude Code

## Phase 12: a headless hub in Docker Kanbot can now run with no desktop: a container on a home server or a VM is the hub. Browsers pair with it to use the board, and the computers that run agents join as nodes or as desktops in Connect mode. Guide: `docs/headless-hub.md`. ### What's in it - **`Dockerfile`:** multi-stage, from `node:24.19.0-bookworm` to `-slim`, both pinned by digest. - Runs as uid 1000 under tini, with data in the `/data` volume. - `NITRO_HOST=127.0.0.1`: the plain listener, which trusts its callers as this computer, is reachable only by `docker exec`. - `KANBOT_HUB_ENABLED=1` and `KANBOT_HUB_PORT=7433` (the one EXPOSE), plus `KANBOT_LOCAL_AGENTS=0`. - `HEALTHCHECK` is `kanbot-hub health`. - Comes with `.dockerignore` and `docker-compose.yml`. - **Environment overrides:** - `KANBOT_HUB_ENABLED` / `KANBOT_HUB_PORT` decide the `hub` section. The app shows it locked, and `Settings.update` refuses changes with `SettingFromEnvironment`. - `KANBOT_LOCAL_AGENTS=0` gives the hub's own machine a capacity of 0 (`Machine.agentsOff`). - `KANBOT_HUB_PUBLIC_URL` is the address the hub gives out: a container only knows its Docker address. - **`kanbot-hub` CLI** (`.output/hub-cli`, DB-free, over the container's loopback): - `offer [--kind browser|node|desktop]` prints the code, its expiry, what to run on the other computer, the fingerprint and the URLs. - `status`. - `health`. - `backup <dir>` (SQLite online backup plus the rest of the data dir, 0700). - **First-pairing hint:** while hub mode listens and no browser is paired, the log says how to get a first code. - **Shared copying:** `scripts/bundleExternals.mjs` is shared by desktop-prepare and the CLI build. ### Checks - `npm run lint`, `npm run typecheck`, `npx vitest run`: 1422 passed, 2 skipped. `test/hub/dockerImage.test.ts` keeps the Dockerfile and compose file consistent without a daemon. - **Real Docker (Docker Desktop 29.7.2), image 534 MB:** - The container is healthy, runs as uid 1000, and logs the first-pairing hint. - With no token, `whoami` gets 401 and the board redirects to `/pair`. Port 3000 is unreachable from the host. - `kanbot-hub status` shows hub mode set by the environment. - `kanbot-hub offer --kind node`: a `kanbot-node` on this Mac pairs with `--fingerprint` and connects over pinned wss. - A browser pairs over HTTPS (cookie, then whoami 200). - The machines list shows the hub at capacity 0 (`agentsOff`) and the Mac's node online. - After `docker restart`, the cookie still works and the node reconnected by itself, with no new hint. - `kanbot-hub backup` gives the database (integrity ok, both devices), certificate, key, machine id and attachments. - With `KANBOT_HUB_PUBLIC_URL`, status, offer and the hint give out that address. - `docker compose config` validates. - The Mac's LocalHostName didn't change. ### Not in it - **A relay or NAT traversal:** nodes and browsers must reach `https://<host>:7433` directly (LAN, VPN, a forwarded port). - **LAN discovery from a bridged container:** it needs `network_mode: host` on Linux. That also makes the loopback listener the host's, so local processes on the host are trusted (documented). - **A CA-signed certificate or a TLS-terminating proxy:** nodes pin the hub's own certificate. - **Publishing the image:** nothing was published. Base: `refactor/phase-11-discovery` (PR #19). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
KANBOT_HUB_ENABLED / KANBOT_HUB_PORT force the hub settings section (the
environment wins, Settings.update refuses it with SettingFromEnvironment,
Hub.status says setByEnvironment and the card shows it locked).
KANBOT_LOCAL_AGENTS=0 gives this machine a capacity of 0 (Machine.agentsOff).
kanbot-hub offer/status/health/backup talks to the server over loopback,
bundled on its own to .output/hub-cli (npm run build:hub-cli), sharing
desktop-prepare's externals vendoring (scripts/bundleExternals.mjs). The
log says how to get a first code while hub mode listens with no browser paired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headless hub image: Dockerfile, compose example, docs
Some checks failed
CI / check (push) Has been cancelled
3c4391d718
Multi-stage build on node:24.19.0-bookworm (pinned by digest), runtime on
bookworm-slim with git, ca-certificates and tini, as uid 1000, data in the
/data volume, hub mode forced on 7433 (the one EXPOSE), the plain listener
on the container's loopback, no local agents, kanbot-hub on PATH and as the
HEALTHCHECK. docs/headless-hub.md covers building, pairing browsers, nodes
and desktops, backups (kanbot-hub backup), upgrades, limits and security.
test/hub/dockerImage.test.ts keeps the files consistent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md: Headless hub
Some checks failed
CI / check (push) Has been cancelled
75a3072b3f
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KANBOT_HUB_PUBLIC_URL: where a containerised hub is reached
Some checks failed
CI / check (push) Has been cancelled
CI / check (pull_request) Has been cancelled
2387d3e94f
Inside a container the hub only sees its Docker address (172.17.0.x), which
it gave out in Hub.status, the pairing link, the first-pairing hint and
kanbot-hub offer. The variable (https origins, comma-separated) is given out
instead; the compose example reads it from .env.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alex merged commit 5079b2df8a into refactor/phase-11-discovery 2026-10-29 18:45:47 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Overlight/App!20
No description provided.