From 3619470d79f148fcd1482208875ab791daca145d Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Mon, 20 Jul 2026 18:26:25 +0300 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01TsmUEtbyTkgQ18tCFYXo1h --- images/vibe-kanban-remote/readme.md | 28 +++++++++++ .../vibe-kanban-db.container | 40 ++++++++++++++++ .../vibe-kanban-electric.container | 45 +++++++++++++++++ .../vibe-kanban-remote/vibe-kanban.container | 48 +++++++++++++++++++ images/vibe-kanban-remote/vibe-kanban.network | 8 ++++ 5 files changed, 169 insertions(+) create mode 100644 images/vibe-kanban-remote/vibe-kanban-db.container create mode 100644 images/vibe-kanban-remote/vibe-kanban-electric.container create mode 100644 images/vibe-kanban-remote/vibe-kanban.container create mode 100644 images/vibe-kanban-remote/vibe-kanban.network diff --git a/images/vibe-kanban-remote/readme.md b/images/vibe-kanban-remote/readme.md index 99fc56e..fa564e2 100644 --- a/images/vibe-kanban-remote/readme.md +++ b/images/vibe-kanban-remote/readme.md @@ -54,6 +54,34 @@ Note ElectricSQL **cannot** use client-certificate auth to Postgres, which is wh 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-`ALTER`s 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 ```sh diff --git a/images/vibe-kanban-remote/vibe-kanban-db.container b/images/vibe-kanban-remote/vibe-kanban-db.container new file mode 100644 index 0000000..526f93e --- /dev/null +++ b/images/vibe-kanban-remote/vibe-kanban-db.container @@ -0,0 +1,40 @@ +# PostgreSQL for vibe-kanban. +# +# Deliberately NOT magrathea. magrathea is mTLS-only (`hostssl ... cert +# clientcert=verify-full map=cert_cn`, with `hostnossl ... reject`) and +# ElectricSQL cannot present a client certificate — it supports only +# sslmode=require/disable plus a CA file for verifying the *server*. Electric is +# not optional either: remote-server treats ELECTRIC_URL as mandatory. magrathea +# is also wal_level=replica, and Electric needs logical, which would mean +# restarting the shared primary. So this stack runs its own Postgres on a private +# podman network; nothing here is reachable from the LAN. +# +# Version tracks upstream's compose (postgres:16-alpine), not magrathea's 18. +# wal_level=logical is required by Electric's logical replication. +[Unit] +Description=vibe-kanban PostgreSQL +After=network-online.target +Wants=network-online.target + +[Container] +Image=docker.io/library/postgres:16-alpine +ContainerName=vibe-kanban-db +AutoUpdate=registry +Network=vibe-kanban.network +Exec=postgres -c wal_level=logical +Volume=/var/lib/vibe-kanban/postgres:/var/lib/postgresql/data:Z +Environment=POSTGRES_DB=remote +Environment=POSTGRES_USER=remote +EnvironmentFile=/etc/vibe-kanban/env +HealthCmd=pg_isready -U remote -d remote +HealthInterval=5s +HealthTimeout=5s +HealthRetries=10 +HealthStartPeriod=5s + +[Service] +Restart=always +TimeoutStartSec=300 + +[Install] +WantedBy=multi-user.target diff --git a/images/vibe-kanban-remote/vibe-kanban-electric.container b/images/vibe-kanban-remote/vibe-kanban-electric.container new file mode 100644 index 0000000..bb08f87 --- /dev/null +++ b/images/vibe-kanban-remote/vibe-kanban-electric.container @@ -0,0 +1,45 @@ +# ElectricSQL sync service for vibe-kanban. +# +# Electric streams Postgres logical replication to the browser. remote-server +# treats ELECTRIC_URL as mandatory, so this is not optional. +# +# Starts AFTER vibe-kanban.service because the `electric_sync` role, its grants +# and the publication are created by remote-server's migrations — Electric cannot +# connect until they exist, and remote-server also sets that role's password on +# start. vibe-kanban.service uses Notify=healthy, so this really does wait. +# +# ELECTRIC_INSECURE mirrors upstream's own production compose and is contained: +# Electric publishes no host port and is reachable only by container name on the +# private vibe-kanban bridge. An `electric-secret` exists in pass if we later want +# to set ELECTRIC_SECRET here and on remote-server. +[Unit] +Description=vibe-kanban ElectricSQL sync +After=network-online.target vibe-kanban.service +Wants=network-online.target +Requires=vibe-kanban.service + +[Container] +Image=docker.io/electricsql/electric:1.4.13 +ContainerName=vibe-kanban-electric +AutoUpdate=registry +Network=vibe-kanban.network +Volume=/var/lib/vibe-kanban/electric:/app/persistent:Z +# Its own env file (not the shared one) so the variable can simply be named +# DATABASE_URL — what Electric reads — without colliding with remote-server's +# SERVER_DATABASE_URL, and with no reliance on systemd expanding one Environment= +# value into another, which quadlets do not do dependably. +EnvironmentFile=/etc/vibe-kanban/electric.env +Environment=PG_PROXY_PORT=65432 +Environment=LOGICAL_PUBLISHER_HOST=vibe-kanban-electric +Environment=AUTH_MODE=insecure +Environment=ELECTRIC_INSECURE=true +Environment=ELECTRIC_MANUAL_TABLE_PUBLISHING=true +Environment=ELECTRIC_USAGE_REPORTING=false +Environment=ELECTRIC_FEATURE_FLAGS=allow_subqueries,tagged_subqueries + +[Service] +Restart=always +TimeoutStartSec=300 + +[Install] +WantedBy=multi-user.target diff --git a/images/vibe-kanban-remote/vibe-kanban.container b/images/vibe-kanban-remote/vibe-kanban.container new file mode 100644 index 0000000..f1563e3 --- /dev/null +++ b/images/vibe-kanban-remote/vibe-kanban.container @@ -0,0 +1,48 @@ +# vibe-kanban remote-server — the self-hosted server half of the suite. +# Serves both the API and the built SPA (from /srv/static in the image) on :8081 +# as uid 10001; published to the LAN on 27180 (agent-zero=5080, open-webui=5090, +# hermes=5100). Fronted by nginx on hanzalova.internal as https://kanban.internal. +# +# Image is built by lair/containers from OUR mirror of BloopAI/vibe-kanban, never +# from GitHub — upstream is sunsetting. AutoUpdate=registry picks up rebuilds. +# +# Startup order matters and is not merely cosmetic: this unit's sqlx migrations +# are what CREATE the `electric_sync` role and the publication that Electric then +# connects with, and it ALTERs that role's password from ELECTRIC_ROLE_PASSWORD on +# every start. So db -> this -> electric. Notify=healthy makes systemd hold the +# unit "starting" until /v1/health answers, so electric genuinely waits for the +# migrations rather than racing them. +# +# Auth: local single-account mode only (SELF_HOST_LOCAL_AUTH_*), deliberately no +# OAuth — the server refuses to start unless at least one provider is configured. +# Swapping to a real IdP later means adding a provider, not changing this file. +[Unit] +Description=vibe-kanban remote-server +After=network-online.target vibe-kanban-db.service +Wants=network-online.target +Requires=vibe-kanban-db.service + +[Container] +Image=git.lair.cafe/lair/vibe-kanban-remote:latest +ContainerName=vibe-kanban +AutoUpdate=registry +Network=vibe-kanban.network +PublishPort=27180:8081 +EnvironmentFile=/etc/vibe-kanban/env +Environment=SERVER_LISTEN_ADDR=0.0.0.0:8081 +Environment=ELECTRIC_URL=http://vibe-kanban-electric:3000 +Environment=SERVER_PUBLIC_BASE_URL=https://kanban.internal +Environment=RUST_LOG=info,remote=info +Notify=healthy +HealthCmd=wget --spider -q http://127.0.0.1:8081/v1/health +HealthInterval=10s +HealthTimeout=5s +HealthRetries=12 +HealthStartPeriod=20s + +[Service] +Restart=always +TimeoutStartSec=300 + +[Install] +WantedBy=multi-user.target diff --git a/images/vibe-kanban-remote/vibe-kanban.network b/images/vibe-kanban-remote/vibe-kanban.network new file mode 100644 index 0000000..dd54e76 --- /dev/null +++ b/images/vibe-kanban-remote/vibe-kanban.network @@ -0,0 +1,8 @@ +# Private bridge for the vibe-kanban stack (postgres + electric + remote-server). +# Only remote-server publishes a host port (27180); postgres and electric are +# reachable only by container name on this network, never from the LAN. +[Network] +NetworkName=vibe-kanban + +[Install] +WantedBy=multi-user.target