Skip to content
Agent Images and the VK-Local Sidecar — Unbaking VibeKanban
Agent Images and the VK-Local Sidecar — Unbaking VibeKanban

Agent Images and the VK-Local Sidecar — Unbaking VibeKanban

The secure-agent-pod started as a single container: Kali Linux, a non-root claude user, sshd, kubectl, and a globally npm install-ed VibeKanban baked straight into the image. That meant every VibeKanban bugfix required rebuilding a 1.8 GB Kali image, every Kali tool upgrade risked breaking the node binary, and the image itself was welded to a single consumer.

This post unbakes all of it into three moves: a new derio-net/agent-images repo with matrix CI, a sidecar container in the pod, and a lockstep bumper in frank.

Architecture

    flowchart TD
  subgraph VK[vibe-kanban fork]
    CI1[Fork CI<br/>vk-remote + vk-build]
  end
  subgraph AI[agent-images repo]
    BASE[agent-base<br/>debian:bookworm-slim]
    KALI[secure-agent-kali<br/>Kali + sshd]
    VKL[vk-local<br/>compiled Rust server]
  end
  subgraph Frank[frank repo]
    BUMP[Lockstep bumper<br/>dispatch handler]
    DEPLOY[ArgoCD syncs]
  end
  CI1 -->|repository_dispatch| BASE
  BASE -->|build-children| KALI
  BASE --> VKL
  KALI -->|repository_dispatch| BUMP
  VKL -->|repository_dispatch| BUMP
  BUMP -->|opens PR| DEPLOY
  

Three repos, three CI loops, two repository_dispatch hops. Any push to the fork eventually produces a reviewable PR in frank.

The agent-images Repo

agent-images/
├── .github/workflows/build.yaml
├── base/Dockerfile
├── kali/
│   ├── Dockerfile
│   ├── entrypoint.sh
│   └── assets/{sshd_config,crontab.txt}
└── vk-local/Dockerfile

base/Dockerfile is FROM debian:bookworm-slim with every tool every agent pod needs — Claude Code CLI, gh, node 22, bun, python3, uv, git, tini, supercronic, a non-root claude user (User IdentifierThe number Linux actually checks for file permissions — the name is a lookup. In containers a uid mismatch against a mounted volume is the usual cause of a permission error. 1000 to match the 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.).

Each child starts with FROM ghcr.io/derio-net/agent-base:${AGENT_BASE_SHA}. kali/ adds Kali archive, tools, kubectl/talosctl/omnictl, sshd. vk-local/ adds nothing — it just COPY --from=vk-artifact /server from an upstream artifact image.

CI uses a matrix with needs: build-base so children always inherit the just-built base SHA from the same commit:

jobs:
  build-base:
    runs-on: ubuntu-latest
    outputs:
      sha: ${{ github.sha }}

  build-children:
    needs: build-base
    strategy:
      matrix:
        image:
          - { name: secure-agent-kali, context: kali, build_args: "AGENT_BASE_SHA=${{ needs.build-base.outputs.sha }}" }
          - { name: vk-local, context: vk-local, build_args: "AGENT_BASE_SHA=${{ needs.build-base.outputs.sha }}\nVK_FORK_SHA=${{ github.event.client_payload.vk_fork_sha || 'latest' }}" }

Dispatch chains append a dispatch-frank step that calls gh api repos/derio-net/frank/dispatches.

The Fork Artifact

VibeKanban’s fork publishes an artifact-only image (ghcr.io/derio-net/vibe-kanban-build) from a FROM scratch Dockerfile containing just /server. The vk-local Dockerfile pulls that file out using a named build stage:

ARG VK_FORK_SHA=latest
FROM ghcr.io/derio-net/vibe-kanban-build:${VK_FORK_SHA} AS vk-artifact
FROM ghcr.io/derio-net/agent-base:${AGENT_BASE_SHA}
COPY --from=vk-artifact /server /usr/local/bin/vibe-kanban

The Sidecar in frank

Two containers sharing a PVC:

spec:
  containers:
    - name: kali
      image: ghcr.io/derio-net/secure-agent-kali:<sha>
      env:
        - { name: PORT, value: "18081" }
        - { name: HOST, value: "127.0.0.1" }
      volumeMounts:
        - { name: agent-home, mountPath: /home/claude }
    - name: vk-local
      image: ghcr.io/derio-net/vk-local:<sha>
      ports:
        - { name: vk-http, containerPort: 8081 }
      env:
        - { name: PORT, value: "8081" }
        - { name: HOST, value: "0.0.0.0" }
      volumeMounts:
        - { name: agent-home, mountPath: /home/claude }
      readinessProbe: { httpGet: { path: /api/health, port: vk-http } }

The filesystem is the interface — no IPC, no shared memory, no Remote Procedure CallCalling a function that runs on another machine. The abstraction is the point and also the trap — the network failure modes do not go away..

The Lockstep Bumper

When agent-images publishes a new image, a repository_dispatch handler in frank opens a PR:

on:
  repository_dispatch:
    types: [agent-images-bumped]

jobs:
  bump:
    steps:
      - uses: actions/checkout@v4
      - name: Resolve SHAs
        run: |
          AI_SHA="${{ github.event.client_payload.agent_images_sha }}"
          VKR_SHA=$(gh api /orgs/derio-net/packages/container/vk-remote/versions --jq '.[0].metadata.container.tags[] | select(test("^[a-f0-9]{7}$"))' | head -1)
      - name: Update manifests
        run: |
          sed -i "s|secure-agent-kali:[a-f0-9]\+|secure-agent-kali:$AI_SHA|" apps/secure-agent-pod/manifests/deployment.yaml
          sed -i "s|vk-local:[a-f0-9]\+|vk-local:$AI_SHA|" apps/secure-agent-pod/manifests/deployment.yaml
      - name: Open PR
        run: |
          git checkout -b "bump/agent-images-${AI_SHA:0:7}"
          git commit -am "chore(agents): bump agent-images to ${AI_SHA:0:7}"
          git push origin HEAD
          gh pr create --base main --title "chore(agents): bump agent-images" --body "..."

Missteps

What HappenedWhy It Was WrongHow We Fixed ItCommit
Port 8081 bind race — kali’s npm VibeKanbanThe task board Frank dispatches agent work through — the queue between a written plan's phases and the agents that execute them. grabs port before sidecar boots, sidecar CrashLoopBackOff (246 restarts in 20h)Two processes racing for the same port; “lighter sidecar will win” was falseKali in-process VK binds 127.0.0.1:18081 (unrouted); sidecar owns 0.0.0.0:8081
“Please build @vibe/local-web first” — UI shows placeholder HTML, real React app never servedrust-embed embeds whatever is in dist/; build script creates a dummy index.html if directory missingFrontend must be built before Rust stage; fe-builder stage runs pnpm build before COPY into builder
PVC mount hides image-baked VK binarynpm install -g @vibe-kanban/cli in image invisible at runtimeKubernetes PVC mount at /home/claude hides image contents at that pathMoved VK to separate sidecar container; image never installs VK in user home
Bumper opened PR with no diff — build ran but no image had actually changedsed pattern [a-f0-9]\+ did not match 40-char SHAAdded --quiet check; bumper exits if no diff

Recovery Path

SymptomCauseFix
Sidecar CrashLoopBackOff on pod restartPort bind race: kali grabbed 8081 firstVerify kali uses PORT=18081 HOST=127.0.0.1
VK UI shows “Please build web app first”Frontend not built before Rust stageCheck fork’s Dockerfile: fe-builder must run before Rust
Bumper PR has no changesImage SHA not updated or pattern mismatchRun bumper workflow manually with correct SHA
Pod has two VK servers runningMigration from single-container not completeDelete old pod, verify sidecar owns 8081

References

Next: Ruflo — A Swarm Orchestrator Next to Paperclip