
VK Remote — Self-Hosting the Kanban Backend Before the Cloud Dies
On April 10th, VibeKanban announced it was shutting down. Thirty days. The OAuth flow was already failing — likely early decommissioning. The local VibeKanbanThe task board Frank dispatches agent work through — the queue between a written plan's phases and the agents that execute them. features (workspaces, sessions, git worktrees, agent spawning) would survive. But the kanban board, issue management, the 33 Model Context ProtocolThe protocol that lets an AI agent call external tools over a documented interface. It is how the agents on Frank reach the cluster rather than guessing about it. tools that the agentic workflow depends on — all that lives in the remote crate, backed by a PostgreSQL database that was about to stop existing.
The good news: VK’s remote crate already supports self-hosting with local auth. Fork the repo, build the image, deploy three containers, point the agent at it.
Architecture
flowchart TD
subgraph Agent[secure-agent-pod — gpu-1]
VK[vk-local — port 8081<br/>SQLite workspaces]
end
subgraph VKRemote[vk-remote — agents namespace]
API[vk-remote — Rust/Axum<br/>port 8081]
PG[postgres-vk — PG 16<br/>WAL logical, 1Gi PVC]
ES[electric — ElectricSQL<br/>port 3000, WAL stream]
end
subgraph Browser[Operator Browser]
UI[VK Remote UI<br/>https://vk.cluster.derio.net]
end
subgraph Traefik[traefik-system]
TR[Traefik — Authentik forward-auth]
end
VK -->|VK_SHARED_API_BASE| API
API -->|issue/project data| PG
ES -->|logical replication| PG
ES -->|real-time sync| API
UI --> TR --> API
Three components, one namespace, zero cloud dependencies:
| Component | Image | Port | Purpose |
|---|---|---|---|
| vk-remote | ghcr.io/derio-net/vk-remote (Rust/Axum) | 8081 | Kanban API server |
| postgres-vk | postgres:16-alpine | 5432 | Issue/project data, Write-Ahead LogRecording an intended change before making it, so a crash mid-write can be replayed rather than lost. How Postgres survives being killed. logical replication |
| electric | electricsql/electric:1.4.13 | 3000 | Real-time sync engine for frontend |
ElectricSQL reads PostgreSQL’s logical replication stream to push live updates to the browser — when an issue changes status, every open tab sees it immediately. That requires wal_level=logical and a dedicated PostgreSQL instance.
Fork and Build
Forked BloopAI/vibe-kanban to derio-net/vibe-kanban. GitHub Actions workflow builds the remote crate into a container image on every push to main:
name: Build vk-remote
on:
push:
branches: [main]
paths:
- 'crates/remote/**'
- 'Cargo.toml'
- 'Cargo.lock'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: docker/build-push-action@v6
with:
context: .
file: crates/remote/Dockerfile
push: true
tags: |
ghcr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
ghcr.io/${{ env.IMAGE_NAME }}:latestImages pinned by commit SHA in manifests. Own the fork, so patches are possible if upstream disappears.
PostgreSQL with Logical Replication
Dedicated PG instance with WAL-level logical replication enabled via command-line args:
containers:
- name: postgres
image: postgres:16-alpine
args:
- "-c"
- "wal_level=logical"
- "-c"
- "max_replication_slots=5"
- "-c"
- "max_wal_senders=5"Recreate strategy because of ReadWriteOnceA PVC access mode that lets exactly one node mount the volume read-write at a time. It is the reason a RollingUpdate deadlocks: the replacement pod cannot mount the volume until the outgoing pod releases it. PersistentVolumeClaimA Kubernetes request for durable storage. The pod names a claim and the storage layer — Longhorn on Frank — binds real disk behind it, so the data outlives the pod..
A PostSync Job creates the ElectricSQL role with replication privileges:
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreationThe Job waits for PG startup, then creates the electric role with LOGIN and REPLICATION privileges plus full grants on the remote database.
Auth: Local Only
No OAuth. Single admin user:
SELF_HOST_LOCAL_AUTH_EMAIL=admin@localhost
SELF_HOST_LOCAL_AUTH_PASSWORD=<from Infisical>POST to /v1/auth/local/login returns JSON Web TokenA signed, self-describing token carrying claims — who you are, what you may do, when it expires. Readable by anyone holding it, so the expiry and signature are the only things protecting it. tokens. Browser access goes through Authentik forward-auth at Traefik — the VK remote itself does not know about Single Sign-OnOne login across many applications. On Frank, Authentik holds the session and Traefik asks it before forwarding a request..
Secrets via Infisical
Four secrets, pulled by External Secrets Operator with same ClusterSecretStore as every other Frank app:
| ExternalSecret Key | Maps To | Purpose |
|---|---|---|
VK_REMOTE_JWT_SECRET | VIBEKANBAN_REMOTE_JWT_SECRET | JWT signing key |
VK_REMOTE_LOCAL_AUTH_PASSWORD | SELF_HOST_LOCAL_AUTH_PASSWORD | Admin login password |
VK_REMOTE_ELECTRIC_PASSWORD | ELECTRIC_ROLE_PASSWORD | ElectricSQL PG role |
VK_REMOTE_PG_PASSWORD | POSTGRES_PASSWORD | Main PG user password |
Connecting the Agent
The secure-agent-pod just needs one env var to switch from cloud to self-hosted:
- name: VK_SHARED_API_BASE
value: "http://vk-remote.agents.svc.cluster.local:8081"The VK binary, MCP server, bridge, and all 33 MCP tools work unchanged — all proxy through the local VK server.
Domain Deviation
The spec originally called for vk.frank.derio.net, but Frank’s Traefik wildcard cert covers *.cluster.derio.net. Using vk.cluster.derio.net avoids provisioning a new certificate. Pragmatism over naming purity.
Missteps
| What Happened | Why It Was Wrong | How We Fixed It | Commit |
|---|---|---|---|
ElectricSQL cannot connect to PG — wal_level=logical not set, no replication slot available | Default PG wal_level is replica, not logical | Added -c wal_level=logical to postgres container args | — |
| PostSync Job backoff limit exhausted — Job fails if PG takes >5 retries to become ready on cold node | pg_isready polling with sleep loop, Job has 5-retry default | Delete failed Job, let ArgoCD re-trigger; or increase backoff limit | — |
| Blueprint needs manual outpost assignment — Authentik proxy provider and application created but not assigned to embedded outpost | Blueprints cannot append to outpost provider list without replacing existing assignments | Manual Django Object-Relational MappingA library presenting database rows as objects. Convenient until the generated query is the thing you need to reason about.: outpost.providers.add(provider) | — |
| Old cloud data inaccessible — expected migration path, but cloud was already decommissioned by the time self-hosted was ready | 30-day shutdown window, OAuth failing before migration completed | Fresh project, fresh issues, fresh start — no data migration | — |
Cross-namespace DNS confusion — agent pod tried vk-remote:8081 without Fully Qualified Domain NameA hostname written out completely, right down to the root — the difference between `grafana` and `grafana.cluster.derio.net`. Which one resolves depends on search domains. | Pod in secure-agent-pod namespace needs vk-remote.agents.svc.cluster.local | Updated env var to use FQDN | — |
Recovery Path
| Symptom | Cause | Fix |
|---|---|---|
| ElectricSQL pod crashlooping | PG not ready yet or WAL level incorrect | Check PG logs; verify wal_level=logical in container args |
| VK remote API returns 500 on login | JWT secret mismatch or local auth password incorrect | Verify ExternalSecret values match Infisical |
| Agent cannot reach VK remote | Wrong DNS or port | Verify VK_SHARED_API_BASE uses FQDN: vk-remote.agents.svc.cluster.local:8081 |
| VK remote UI shows no data | ElectricSQL not syncing | Check electric pod logs; verify PG role electric exists with REPLICATION |
| PostSync Job stuck | PG not ready within job backoff limit | Delete job, let ArgoCD recreate on next sync |
References
- VibeKanban — agent orchestration tool
- ElectricSQL — real-time sync for PostgreSQL
