Kandev Docs

Kubernetes

Deploy Kandev in Kubernetes environments.

This guide covers building the Kandev Docker image and deploying it to a Kubernetes cluster.

Prerequisites

  • Docker (for building the image)
  • A container registry (Docker Hub, GHCR, ECR, etc.)
  • A Kubernetes cluster with kubectl configured
  • A StorageClass that supports ReadWriteOnce PVCs (for SQLite persistence)

Building the Image

The root Dockerfile consumes a prebuilt Linux release bundle; it does not compile the repository inside Docker. On a Linux host matching the cluster architecture, prepare the same build context used by the release workflow:

make service-bundle
rm -rf ctx
mkdir -p ctx/bundle
cp -R dist/kandev/. ctx/bundle/
cp docker-entrypoint.sh ctx/
docker build -f Dockerfile -t kandev:latest ctx

For cross-architecture or multi-architecture images, produce the matching kandev-linux-x64.tar.gz and/or kandev-linux-arm64.tar.gz bundles first. For each platform, extract the bundle's top-level kandev/ directory into ctx/bundle/, copy docker-entrypoint.sh into ctx/, and build that context for the matching platform. A --platform flag alone does not cross-compile the native binaries. See the Docker guide for the context layout.

Using the Pre-built Image

Kandev publishes images to GitHub Container Registry. Pull directly:

docker pull ghcr.io/kdlbs/kandev:latest

Or reference it in your K8s deployment:

image: ghcr.io/kdlbs/kandev:latest

Choosing your image: vanilla vs. universal

Kandev publishes two flavors: the default vanilla image (smallest, npm-installable agent CLIs only) and a :universal image (~1.4 GB) that adds language toolchains (Go, Rust, build-essential), linters, and Playwright Chromium system libs - useful when your agents work on Go/Rust/Python projects or drive headless browsers.

image: ghcr.io/kdlbs/kandev:universal

See the repository image guide for the full comparison, inclusion policy, and recipes for deriving your own image when you need something neither flavor includes.

Deploying to Kubernetes

Quick Start

# Apply all manifests
kubectl apply -f k8s/

# Check status
kubectl get pods -l app=kandev
kubectl logs -l app=kandev -f

What Gets Created

ResourceFilePurpose
Deploymentdeployment.yamlSingle-replica pod running backend + web
Serviceservice.yamlClusterIP exposing port 38429
ConfigMapconfigmap.yamlNon-sensitive environment configuration
PVCpvc.yaml10Gi persistent volume for SQLite + worktrees
Ingressingress.yamlExample ingress with WebSocket support

Accessing the UI

Port-forward (quickest for testing):

kubectl port-forward svc/kandev 38429:38429
# Open http://localhost:38429

Ingress: Edit k8s/ingress.yaml to set your domain, then apply. The ingress routes all traffic to the backend on port 38429; the Go backend serves API, WebSocket, and SPA traffic on that port.

Custom Domain / Reverse Proxy

No extra configuration is needed. The frontend automatically uses window.location.origin to reach the API, which works with any domain, reverse proxy, or ingress setup.

Installing Agent CLIs

The kandev image ships with git, gh (GitHub CLI), node, and npm, but does not bundle the coding-agent CLIs (claude-code, codex, auggie, etc.) — agent choice is per-user, and bundling all of them would bloat the image significantly.

Looking to add tools beyond agent CLIs - language toolchains, build tools, internal CLIs? See the repository image guide for the universal-image option and recipes for deriving your own image.

To install an agent inside the running pod, open Settings → Agents in the UI and click Install on the agent card under "Available to Install". The backend runs the agent's hard-coded install script (npm install -g <pkg>) and rescans on success.

The image sets NPM_CONFIG_PREFIX=/data/.npm-global so user-installed npm globals land on the PV and survive pod restarts and image upgrades. The same persistence applies if you kubectl exec and install manually:

kubectl exec -it deployment/kandev -- npm install -g @anthropic-ai/claude-code

After installing, log in with the agent's own auth (e.g. claude login), then click Rescan on the agents page.

Persistent agent and gh CLI auth

The image sets HOME=/data/home for the kandev user, so every CLI that writes its auth state under $HOME lands on the PV and survives pod restarts and image upgrades. This includes:

  • gh CLI — ~/.config/gh/hosts.yml
  • Claude Code — ~/.claude/.credentials.json, ~/.claude.json
  • Codex — ~/.codex/auth.json, ~/.codex/config.toml
  • Auggie — ~/.augment/session.json
  • GitHub Copilot — ~/.copilot/...
  • OpenCode, Amp — ~/.config/<tool>/...

So a one-time kubectl exec -it deployment/kandev -- gh auth login (or claude login, codex login, etc.) is enough; you do not need to redo it after kubectl set image or a helm upgrade.

The GitHub PAT configured in Settings → Integrations → GitHub is stored as a secret in the SQLite DB (or your external Postgres) and has always persisted. The HOME=/data/home setup covers the separate gh auth login flow that the backend falls back to when no GITHUB_TOKEN secret is set.

Configuration

Kandev reads configuration via KANDEV_-prefixed environment variables (Viper). Put only non-sensitive values in k8s/configmap.yaml; inject passwords and signing keys into the Deployment from a Kubernetes Secret.

Core Settings

See configuration.md for the full reference (every backend knob and its YAML form). The tables below cover what's most commonly set in K8s manifests.

Env VarRequiredDefaultDescription
KANDEV_SERVER_PORTNo38429Server port (API + WebSocket + Web UI)
KANDEV_HOME_DIRNo/dataKandev home directory - contains data/ (DB), tasks/, worktrees/, repos/, sessions/, and lsp-servers/
KANDEV_DATABASE_DRIVERNosqliteDatabase driver (sqlite or postgres)
KANDEV_DATABASE_PATHNo$KANDEV_HOME_DIR/data/kandev.dbSQLite database file path (override)
KANDEV_NATS_URLNoemptyShared NATS event bus URL. Required when multiple backend replicas must share events; empty selects a process-local in-memory bus.
KANDEV_DOCKER_ENABLEDNofalseEnable Docker runtime for agents (requires DinD)
KANDEV_LOG_LEVELNoinfoLog level: debug, info, warn, error
KANDEV_LOGGING_FORMATNoenvironment-selected (json in K8s)Explicit format: json or text. The literal value auto is not accepted.
KANDEV_LOGGING_OUTPUTPATHNostdoutLog destination: stdout, stderr, or a file path (rotated when a file)
KANDEV_LOGGING_MAXSIZEMBNo100Rotate the log file when it exceeds this size (MB). File output only.
KANDEV_LOGGING_MAXBACKUPSNo5Max rotated files to retain (0 = unlimited). File output only.
KANDEV_LOGGING_MAXAGEDAYSNo30Max age of rotated files in days (0 = unlimited). File output only.
KANDEV_LOGGING_COMPRESSNotrueGzip rotated files. File output only.

Logging in K8s: prefer the default stdout so kubelet collects logs. If you set KANDEV_LOGGING_OUTPUTPATH to a file, the active log is created with mode 0600 (owner read/write only); any sidecar reading it must run as the same user.

Upgrading from a pre-KANDEV_HOME_DIR deployment? The SQLite DB path moved from /data/kandev.db to /data/data/kandev.db, and KANDEV_DATA_DIR is gone — point KANDEV_HOME_DIR at the same volume mount (/data) instead. (KANDEV_WORKTREE_BASEPATH still works as an explicit override if you want to keep worktrees outside the home dir.) The backend auto-migrates the legacy kandev.db (plus any -wal/-shm files) on first boot — look for Migrated SQLite database from pre-KANDEV_HOME_DIR location in the pod logs. If you'd rather pin the old path, set KANDEV_DATABASE_PATH=/data/kandev.db in the ConfigMap.

Keep KANDEV_DATABASE_PASSWORD, KANDEV_AUTH_JWTSECRET, and KANDEV_OFFICE_JWTSIGNINGKEY out of ConfigMaps. Reference Secret keys from the Deployment instead:

env:
  - name: KANDEV_DATABASE_PASSWORD
    valueFrom:
      secretKeyRef:
        name: kandev-secrets
        key: database-password
  - name: KANDEV_AUTH_JWTSECRET
    valueFrom:
      secretKeyRef:
        name: kandev-secrets
        key: auth-jwt-secret
  - name: KANDEV_OFFICE_JWTSIGNINGKEY
    valueFrom:
      secretKeyRef:
        name: kandev-secrets
        key: office-jwt-signing-key

PostgreSQL Settings (when KANDEV_DATABASE_DRIVER=postgres)

Env VarRequiredDefaultDescription
KANDEV_DATABASE_HOSTNolocalhostPostgreSQL host
KANDEV_DATABASE_PORTNo5432PostgreSQL port
KANDEV_DATABASE_USERYeskandevDatabase user
KANDEV_DATABASE_PASSWORDUsually(empty)Database password - required unless your Postgres allows passwordless auth
KANDEV_DATABASE_DBNAMEYeskandevDatabase name
KANDEV_DATABASE_SSLMODENodisableSSL mode (disable, require, verify-ca, verify-full)

Database: SQLite vs PostgreSQL

SQLite (default)

  • Zero-config, works out of the box
  • Database stored at /data/data/kandev.db on the PV (derived from KANDEV_HOME_DIR=/data)
  • Single replica only (SQLite is single-writer)
  • Deployment strategy is Recreate to prevent concurrent writes
  • Good for small teams / personal use
  • Supports multiple replicas for horizontal scaling when paired with shared NATS
  • Change deployment strategy to RollingUpdate
  • Set via environment variables:
# Non-sensitive values in configmap.yaml
KANDEV_DATABASE_DRIVER: postgres
KANDEV_DATABASE_HOST: postgres.default.svc.cluster.local
KANDEV_DATABASE_PORT: "5432"
KANDEV_DATABASE_USER: kandev
KANDEV_DATABASE_DBNAME: kandev
KANDEV_NATS_URL: nats://nats.default.svc.cluster.local:4222

Supply KANDEV_DATABASE_PASSWORD from a Secret as shown above. When using Postgres, the PVC is still needed for worktree storage but the database itself is external. An empty KANDEV_NATS_URL selects an isolated in-memory event bus in each process, so notifications and orchestration events do not coordinate across replicas; do not scale beyond one backend replica without shared NATS.

Persistent Storage

The PVC at /data stores:

  • SQLite database (/data/data/kandev.db, /data/data/kandev.db-wal, /data/data/kandev.db-shm)
  • Git worktrees (/data/worktrees/), tasks (/data/tasks/), repos (/data/repos/), sessions (/data/sessions/), and LSP servers (/data/lsp-servers/)
  • User home (/data/home/) — $HOME for the in-pod kandev user; holds gh CLI auth and agent CLI auth state (see Persistent agent and gh CLI auth above)
  • npm globals (/data/.npm-global/) — agent CLIs installed via npm install -g

The PVC uses ReadWriteOnce access mode. If your cluster requires a specific StorageClass, add it to k8s/pvc.yaml:

spec:
  storageClassName: your-storage-class

Health Checks

The deployment includes both probes on the /health endpoint:

  • Liveness probe: Restarts the pod if the backend becomes unresponsive (30s interval, 3 failures)
  • Readiness probe: Removes the pod from service during startup or issues (10s interval, 3 failures)

The CLI launcher also performs an internal health check — it waits for the backend to be healthy before starting the web server.

Scaling

Single replica (SQLite): The default configuration uses replicas: 1 with Recreate strategy. This ensures only one instance writes to SQLite at a time.

Multiple replicas (PostgreSQL + NATS): Switch to Postgres, configure the same KANDEV_NATS_URL on every replica, change the deployment strategy to RollingUpdate, and increase replicas:

spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

Upgrading

Rebuild the release bundle and refresh ctx/ using Building the Image before running the image build below.

# Build and push new image
docker build -f Dockerfile -t your-registry.com/kandev:v1.1.0 ctx
docker push your-registry.com/kandev:v1.1.0

# Update deployment
kubectl set image deployment/kandev kandev=your-registry.com/kandev:v1.1.0

# Or edit the deployment directly
kubectl edit deployment kandev

SQLite migrations run automatically on startup — no manual migration step needed.

Upgrading across the HOME=/data/home change: if you used kubectl exec to gh auth login or log in to agent CLIs on a pre-HOME=/data/home image, that state lived in the ephemeral /home/kandev and is not carried over. Log in once on the new pod and it will persist for all subsequent upgrades. If you want to keep the old state, copy it onto the PV before upgrading:

kubectl exec deployment/kandev -- sh -c 'cp -a /home/kandev/. /data/home/ 2>/dev/null || true'

Troubleshooting

# Check pod status
kubectl get pods -l app=kandev

# View logs
kubectl logs -l app=kandev -f

# Shell into the pod (if needed)
kubectl exec -it deployment/kandev -- /bin/bash

# Check PVC status
kubectl get pvc kandev-data

# Describe pod for events
kubectl describe pod -l app=kandev

On this page