deploy: bring the quadlets in from lair/containers
These describe how this repo's image is run; they belong with it rather than in the third-party image repo. Captured from the live units on bob, with the postgres password redacted out of the DSN. Records the two things that are easy to get wrong and expensive to rediscover: the db -> server -> electric ordering (the server's migrations create the role and publication Electric attaches to, and Electric's Requires= means a server restart stops it without bringing it back), and the firewalld/aardvark interaction that presents as an auth failure. Image= is a pinned immutable tag and AutoUpdate=registry is gone: podman-auto-update.timer is enabled on bob, so a floating tag would let the timer decide which build is running.
This commit is contained in:
55
deploy/readme.md
Normal file
55
deploy/readme.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# deploy
|
||||
|
||||
The podman quadlets for the self-hosted stack, as deployed on
|
||||
`bob.hanzalova.internal` in `/etc/containers/systemd/`. Fronted by nginx on
|
||||
`hanzalova.internal` as `https://kanban.internal`.
|
||||
|
||||
The image is built and published by `.gitea/workflows/container.yml` in this
|
||||
repo as `git.lair.cafe/lair/vibe-kanban-remote`.
|
||||
|
||||
## Ordering is load-bearing
|
||||
|
||||
`db -> server -> electric`, and not merely cosmetically:
|
||||
|
||||
- The server's sqlx migrations are what create the `electric_sync` role and the
|
||||
publication Electric attaches to, so Electric started first has nothing to
|
||||
attach to.
|
||||
- `Notify=healthy` on the server holds systemd in "starting" until `/v1/health`
|
||||
answers, so Electric genuinely waits for the migrations rather than racing them.
|
||||
- `vibe-kanban-electric` sets `Requires=vibe-kanban.service`, so restarting the
|
||||
server stops Electric but does **not** bring it back. Restart it explicitly.
|
||||
|
||||
## Pinned images, deliberately
|
||||
|
||||
`Image=` names an immutable `${version}-g${sha}` tag, not `:latest`, and
|
||||
`AutoUpdate=registry` is deliberately absent. `podman-auto-update.timer` is
|
||||
enabled on bob, so a floating tag would mean the running version is decided by
|
||||
whenever the timer last fired. Deploys here are a one-line edit plus a restart.
|
||||
|
||||
To roll forward: take the tag the container workflow prints, edit `Image=`,
|
||||
`systemctl daemon-reload`, restart the server, then restart Electric.
|
||||
|
||||
## Secrets
|
||||
|
||||
`/etc/vibe-kanban/env` (mode 0600, root:root) holds the postgres DSN and the
|
||||
local-auth settings; it mirrors `pass lair/vibe-kanban/*`. It is not in this
|
||||
repo. Note the DSN embeds the password inline, so redact on the URL shape rather
|
||||
than on variable names when pasting any of it around.
|
||||
|
||||
Auth is local single-account mode (`SELF_HOST_LOCAL_AUTH_*`) with no OAuth
|
||||
provider; the server refuses to start unless at least one provider is
|
||||
configured. Moving to a real IdP means adding a provider, not editing these
|
||||
files.
|
||||
|
||||
## Networking
|
||||
|
||||
`vibe-kanban.network` is a private bridge with `dns_enabled`, so the containers
|
||||
resolve each other by name via aardvark-dns on the bridge address. That requires
|
||||
the bridge to sit in firewalld's `trusted` zone — `firewall-cmd --reload` drops
|
||||
runtime-only zone assignments, which silently kills all DNS inside these
|
||||
containers and surfaces as login 500s (`failed to fetch local auth user by
|
||||
email ... Temporary failure in name resolution`). The assignment is made
|
||||
permanent on bob; if it is ever lost:
|
||||
|
||||
firewall-cmd --permanent --zone=trusted --add-interface=podman1
|
||||
firewall-cmd --reload
|
||||
40
deploy/vibe-kanban-db.container
Normal file
40
deploy/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
deploy/vibe-kanban-electric.container
Normal file
45
deploy/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
|
||||
47
deploy/vibe-kanban.container
Normal file
47
deploy/vibe-kanban.container
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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:0.1.44-gccff6fc
|
||||
ContainerName=vibe-kanban
|
||||
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
deploy/vibe-kanban.network
Normal file
8
deploy/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