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
This commit is contained in:
@@ -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
|
||||
|
||||
40
images/vibe-kanban-remote/vibe-kanban-db.container
Normal file
40
images/vibe-kanban-remote/vibe-kanban-db.container
Normal 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
|
||||
45
images/vibe-kanban-remote/vibe-kanban-electric.container
Normal file
45
images/vibe-kanban-remote/vibe-kanban-electric.container
Normal 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
|
||||
48
images/vibe-kanban-remote/vibe-kanban.container
Normal file
48
images/vibe-kanban-remote/vibe-kanban.container
Normal 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
|
||||
8
images/vibe-kanban-remote/vibe-kanban.network
Normal file
8
images/vibe-kanban-remote/vibe-kanban.network
Normal 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
|
||||
Reference in New Issue
Block a user