Files
containers/images/vibe-kanban-remote/readme.md
rob thijssen 3619470d79
Some checks failed
images / hermes (push) Failing after 17m32s
images / vibe-kanban-remote (push) Successful in 21m35s
docs(vibe-kanban-remote): commit the deployed quadlets
Mirrors the hermes convention of keeping the consuming quadlet alongside the
image definition. Four units: a private network, postgres 16 with
wal_level=logical, remote-server, and electric.

Records why the start order matters -- remote-server's migrations create the
electric_sync role, its grants and the publication that electric then connects
with, so electric cannot come up first -- and why electric has its own env
file, which is to avoid depending on systemd expanding one Environment= value
into another inside a quadlet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsmUEtbyTkgQ18tCFYXo1h
2026-07-20 18:26:25 +03:00

4.1 KiB

vibe-kanban-remote

The remote-server from vibe-kanban — the self-hostable server half of the suite. Published as git.lair.cafe/lair/vibe-kanban-remote:{version,latest}.

What it is

A single Rust binary (/usr/local/bin/remote) serving both an API and the built remote-web SPA from /srv/static, listening on :8081 as uid 10001. Upstream ships the Dockerfile (crates/remote/Dockerfile, context = repo root), so nothing is vendored here — build.sh and the workflow build straight from that.

Built from our mirror, deliberately

Unlike hermes, this builds from git.lair.cafe/BloopAI/vibe-kanban, not GitHub, and resolves its version from the mirror's tags rather than GitHub's releases API.

BloopAI has announced a sunset. The mirror exists so this suite survives upstream going away, which is worthless if the build still calls GitHub to find out what to build. Nothing in this image's build path touches github.com.

Gitea mirrors carry tags but not GitHub releases, so "latest" is resolved by filtering tags to the strict release pattern v<semver>-<14-digit datestamp> and sorting on the datestamp. The filter also excludes malformed historical tags in the upstream repo (vv.20250708094151, vv0.0.40-nbump.2.…) that would otherwise sort unpredictably.

No private access required

crates/remote/Cargo.toml declares a billing dependency on the private BloopAI/vibe-kanban-private repo. The Dockerfile deliberately strips it — and deletes crates/remote/Cargo.lock — whenever the FEATURES build arg is empty, which is the documented self-host path. Never set FEATURES; we have no access to that repo and the build would fail trying to reach it over SSH.

Build args

Arg Value here Why
FEATURES (unset) Strips the private billing crate. Setting it breaks the build.
VITE_RELAY_API_BASE_URL (empty) Baked into the SPA at build time; empty disables relay/tunnel, which we don't deploy. Changing it needs a rebuild, not a restart.

Runtime

Consumed by the vibe-kanban quadlets on bob (kanban.internal). Needs a PostgreSQL with wal_level=logical and an ElectricSQL sync service alongside; SERVER_DATABASE_URL, ELECTRIC_URL and VIBEKANBAN_REMOTE_JWT_SECRET are mandatory, and at least one auth provider must be configured or the server refuses to start. It runs its own sqlx migrations on startup.

Note ElectricSQL cannot use client-certificate auth to Postgres, which is why this deployment runs its own Postgres rather than using magrathea's mTLS-only instance.

Deployment (bob)

The four quadlets in this directory are the deployed configuration, copied to /etc/containers/systemd/ on bob:

Unit What
vibe-kanban.network private bridge; only remote-server publishes a host port
vibe-kanban-db.container PostgreSQL 16 with wal_level=logical
vibe-kanban.container remote-server, published on 27180
vibe-kanban-electric.container ElectricSQL sync

Start order is load-bearing, not cosmetic. remote-server's sqlx migrations are what create the electric_sync role, its grants and the publication that Electric connects with — and remote-server re-ALTERs that role's password from ELECTRIC_ROLE_PASSWORD on every start. So db → remote-server → electric, with Notify=healthy on the first two so systemd waits for readiness rather than mere process start.

Secrets live in /etc/vibe-kanban/{env,electric.env} (0600 root), mirroring pass lair/vibe-kanban/*. Electric gets its own file purely so its variable can be named DATABASE_URL without colliding with remote-server's SERVER_DATABASE_URL, and so nothing depends on systemd expanding one Environment= into another.

Fronted by nginx on hanzalova.internal as https://kanban.internal (internal CA, renewed by step@kanban.timer). Upstream's Caddy service is dropped entirely — TLS terminates at the proxy, per architecture/reverse-proxies.md.

Local build

./build.sh                              # latest tag from the mirror
VK_REF=v0.1.44-20260424091429 ./build.sh   # a specific tag