docs(vibe-kanban-remote): commit the deployed quadlets
Some checks failed
images / hermes (push) Failing after 17m32s
images / vibe-kanban-remote (push) Successful in 21m35s

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
This commit is contained in:
2026-07-20 18:26:25 +03:00
parent dff283c468
commit 3619470d79
5 changed files with 169 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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