Skip to content
VK Remote — Self-Hosting the Kanban Backend Before the Cloud Dies
VK Remote — Self-Hosting the Kanban Backend Before the Cloud Dies

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:

ComponentImagePortPurpose
vk-remoteghcr.io/derio-net/vk-remote (Rust/Axum)8081Kanban API server
postgres-vkpostgres:16-alpine5432Issue/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
electricelectricsql/electric:1.4.133000Real-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 }}:latest

Images 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: BeforeHookCreation

The 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 KeyMaps ToPurpose
VK_REMOTE_JWT_SECRETVIBEKANBAN_REMOTE_JWT_SECRETJWT signing key
VK_REMOTE_LOCAL_AUTH_PASSWORDSELF_HOST_LOCAL_AUTH_PASSWORDAdmin login password
VK_REMOTE_ELECTRIC_PASSWORDELECTRIC_ROLE_PASSWORDElectricSQL PG role
VK_REMOTE_PG_PASSWORDPOSTGRES_PASSWORDMain 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 HappenedWhy It Was WrongHow We Fixed ItCommit
ElectricSQL cannot connect to PGwal_level=logical not set, no replication slot availableDefault PG wal_level is replica, not logicalAdded -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 nodepg_isready polling with sleep loop, Job has 5-retry defaultDelete 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 outpostBlueprints cannot append to outpost provider list without replacing existing assignmentsManual 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 ready30-day shutdown window, OAuth failing before migration completedFresh 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.localUpdated env var to use FQDN

Recovery Path

SymptomCauseFix
ElectricSQL pod crashloopingPG not ready yet or WAL level incorrectCheck PG logs; verify wal_level=logical in container args
VK remote API returns 500 on loginJWT secret mismatch or local auth password incorrectVerify ExternalSecret values match Infisical
Agent cannot reach VK remoteWrong DNS or portVerify VK_SHARED_API_BASE uses FQDN: vk-remote.agents.svc.cluster.local:8081
VK remote UI shows no dataElectricSQL not syncingCheck electric pod logs; verify PG role electric exists with REPLICATION
PostSync Job stuckPG not ready within job backoff limitDelete job, let ArgoCD recreate on next sync

References

Next: CI/CD Platform — Gitea, Tekton, Zot, and Cosign