vibe-kanban-remote: move the build to lair/vibe-kanban
Some checks failed
images / hermes (push) Failing after 9m0s
Some checks failed
images / hermes (push) Failing after 9m0s
This repo is for third-party images built from someone else's source. We now own a fork at lair/vibe-kanban, so its image is built there, next to the code, by .gitea/workflows/container.yml. The quadlets move with it. Leaving this job here was actively harmful, not just redundant: it built from the BloopAI mirror and pushed to the same git.lair.cafe/lair/vibe-kanban-remote:latest on a daily 07:00 cron. The mirror still contains upstream's product sunset, which replaced the kanban board with an export-only page — so the next scheduled run would have republished a sunset image over :latest. bob is pinned to an immutable tag and would not have picked it up, but the tag would have been wrong for anything that did. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182wzZE8DguMPWhxD21gfP2
This commit is contained in:
@@ -80,62 +80,3 @@ jobs:
|
||||
podman push "${IMAGE}:${VERSION}"
|
||||
podman push "${IMAGE}:latest"
|
||||
echo "published ${IMAGE}:${VERSION} (and :latest)"
|
||||
|
||||
vibe-kanban-remote:
|
||||
runs-on:
|
||||
- metal
|
||||
- podman
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Resolved from OUR MIRROR, not GitHub: BloopAI has announced a sunset and
|
||||
# the mirror exists so this build outlives them — which is pointless if the
|
||||
# build asks github.com what to build. Gitea mirrors carry tags but not
|
||||
# releases, so filter to the strict release pattern and sort on the trailing
|
||||
# datestamp (also skips malformed historical tags like `vv.20250708094151`).
|
||||
- name: resolve latest upstream release (from the mirror)
|
||||
id: rel
|
||||
run: |
|
||||
tag=$(for p in 1 2 3 4 5; do
|
||||
curl -fsS "https://git.lair.cafe/api/v1/repos/BloopAI/vibe-kanban/tags?limit=100&page=${p}" \
|
||||
| jq -r '.[].name'
|
||||
done \
|
||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+-[0-9]{14}$' \
|
||||
| sort -t- -k2 -n \
|
||||
| tail -1)
|
||||
if [ -z "$tag" ]; then
|
||||
echo "ERROR: could not resolve a vibe-kanban tag from the mirror"; exit 1
|
||||
fi
|
||||
echo "mirror latest: $tag"
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: login to registry
|
||||
run: podman login -u ${{ gitea.actor }} -p ${{ secrets.REGISTRY_TOKEN }} git.lair.cafe
|
||||
|
||||
- name: build & push (release-triggered, self-healing)
|
||||
env:
|
||||
TAG: ${{ steps.rel.outputs.tag }}
|
||||
VERSION: ${{ steps.rel.outputs.version }}
|
||||
FORCE: ${{ github.event.inputs.force }}
|
||||
run: |
|
||||
IMAGE=git.lair.cafe/lair/vibe-kanban-remote
|
||||
if [ "$FORCE" != "true" ] && skopeo inspect "docker://${IMAGE}:${VERSION}" >/dev/null 2>&1; then
|
||||
echo "${IMAGE}:${VERSION} already published — nothing to build"
|
||||
exit 0
|
||||
fi
|
||||
# Upstream ships the Dockerfile; context is the repo root, so -f points
|
||||
# into it. FEATURES is deliberately unset — the Dockerfile strips the
|
||||
# private billing crate only when it is empty (the self-host path), and
|
||||
# we have no access to BloopAI/vibe-kanban-private.
|
||||
# VITE_RELAY_API_BASE_URL is baked into the SPA; empty = relay disabled.
|
||||
echo "building ${IMAGE}:${VERSION} from the mirror at ${TAG}"
|
||||
podman build --pull=newer \
|
||||
-f crates/remote/Dockerfile \
|
||||
--build-arg VITE_RELAY_API_BASE_URL= \
|
||||
-t "${IMAGE}:${VERSION}" \
|
||||
-t "${IMAGE}:latest" \
|
||||
"https://git.lair.cafe/BloopAI/vibe-kanban.git#${TAG}"
|
||||
podman push "${IMAGE}:${VERSION}"
|
||||
podman push "${IMAGE}:latest"
|
||||
echo "published ${IMAGE}:${VERSION} (and :latest)"
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the vibe-kanban remote-server image locally, mirroring the `images` workflow.
|
||||
#
|
||||
# vibe-kanban ships its own Dockerfile (crates/remote/Dockerfile, context = repo
|
||||
# root), so there is no vendored Containerfile here.
|
||||
#
|
||||
# Unlike hermes, the build context is OUR MIRROR (git.lair.cafe/BloopAI/vibe-kanban)
|
||||
# rather than GitHub, and the version is resolved from the mirror's tags rather than
|
||||
# GitHub's releases API. BloopAI has announced a sunset; the whole point of the
|
||||
# mirror is that this build keeps working after upstream disappears, so it must not
|
||||
# depend on GitHub at build time. See the BloopAI org description on git.lair.cafe.
|
||||
#
|
||||
# Gitea mirrors do not carry GitHub *releases*, only tags — hence tag resolution by
|
||||
# strict pattern (v<semver>-<14-digit datestamp>) sorted on the datestamp. That
|
||||
# filter also skips the malformed historical tags in this repo (e.g. `vv.2025…`).
|
||||
#
|
||||
# Override the ref with VK_REF (e.g. v0.1.44-20260424091429); empty resolves latest.
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY="${REGISTRY:-git.lair.cafe}"
|
||||
IMAGE_NAME="${REGISTRY}/lair/vibe-kanban-remote"
|
||||
MIRROR_API="https://git.lair.cafe/api/v1/repos/BloopAI/vibe-kanban/tags"
|
||||
MIRROR_GIT="https://git.lair.cafe/BloopAI/vibe-kanban.git"
|
||||
VK_REF="${VK_REF:-}"
|
||||
|
||||
# The frontend bakes this in at build time; empty disables the relay/tunnel
|
||||
# features, which we do not deploy. Changing it requires a rebuild, not a restart.
|
||||
VITE_RELAY_API_BASE_URL="${VITE_RELAY_API_BASE_URL:-}"
|
||||
|
||||
if [ -z "${VK_REF}" ]; then
|
||||
VK_REF=$(for p in 1 2 3 4 5; do
|
||||
curl -fsS "${MIRROR_API}?limit=100&page=${p}" | jq -r '.[].name'
|
||||
done \
|
||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+-[0-9]{14}$' \
|
||||
| sort -t- -k2 -n \
|
||||
| tail -1)
|
||||
fi
|
||||
[ -n "${VK_REF}" ] || { echo "could not resolve a vibe-kanban ref from the mirror"; exit 1; }
|
||||
VERSION="${VK_REF#v}"
|
||||
|
||||
echo "building ${IMAGE_NAME}:${VERSION} from ${MIRROR_GIT}#${VK_REF}"
|
||||
podman build --pull=newer \
|
||||
-f crates/remote/Dockerfile \
|
||||
--build-arg "VITE_RELAY_API_BASE_URL=${VITE_RELAY_API_BASE_URL}" \
|
||||
-t "${IMAGE_NAME}:${VERSION}" \
|
||||
-t "${IMAGE_NAME}:latest" \
|
||||
"${MIRROR_GIT}#${VK_REF}"
|
||||
|
||||
echo "built ${IMAGE_NAME}:${VERSION} and :latest"
|
||||
echo "push with: podman push ${IMAGE_NAME}:${VERSION} && podman push ${IMAGE_NAME}:latest"
|
||||
@@ -1,90 +0,0 @@
|
||||
# vibe-kanban-remote
|
||||
|
||||
The **remote-server** from [vibe-kanban](https://git.lair.cafe/BloopAI/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-`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
|
||||
./build.sh # latest tag from the mirror
|
||||
VK_REF=v0.1.44-20260424091429 ./build.sh # a specific tag
|
||||
```
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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
|
||||
@@ -1,45 +0,0 @@
|
||||
# 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
|
||||
@@ -1,48 +0,0 @@
|
||||
# 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
|
||||
@@ -1,8 +0,0 @@
|
||||
# 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