# Docker Guide Run Kandev in a Docker container. For Kubernetes deployment, see [k8s.md](k8s.md). ## Quick Start ```bash docker run -p 38429:38429 -v kandev-data:/data ghcr.io/kdlbs/kandev:latest ``` Open `http://localhost:38429` in your browser. ## Using the Pre-built Image Kandev publishes images to GitHub Container Registry for `linux/amd64` and `linux/arm64`: ```bash # Latest release docker pull ghcr.io/kdlbs/kandev:latest # Specific version docker pull ghcr.io/kdlbs/kandev:0.9.0 ``` ### Choosing your image: vanilla vs. universal Two flavors are published. The default vanilla image is smallest and bundles npm-installable agent CLIs only. The `:universal` image (~1.4 GB) adds language toolchains (Go, Rust, build-essential), linters, and Playwright Chromium system libs - pick this if your agents work on Go/Rust/Python projects or drive headless browsers. ```bash docker pull ghcr.io/kdlbs/kandev:universal ``` See [`images.md`](images.md) for the full comparison, inclusion policy, and recipes for deriving your own image. ## Building from Source From the repository root: ```bash # Build for your current architecture docker build -t kandev:latest . # Build for a specific architecture docker build --platform linux/amd64 -t kandev:latest . ``` ## Data Persistence Kandev stores its SQLite database and git worktrees in `/data`. Mount a volume to persist data across container restarts: ```bash # Named volume (recommended) docker run -v kandev-data:/data ghcr.io/kdlbs/kandev:latest # Bind mount to a host directory docker run -v /path/on/host:/data ghcr.io/kdlbs/kandev:latest ``` Without a volume, data is lost when the container is removed. ### What lives on the volume | Path | Contents | |---|---| | `/data/data/` | SQLite database (`kandev.db`, `-wal`, `-shm`) | | `/data/worktrees/`, `/data/tasks/`, `/data/repos/`, `/data/sessions/`, `/data/lsp-servers/` | Per-session state | | `/data/.npm-global/` | Agent CLIs installed via `npm install -g` (`NPM_CONFIG_PREFIX`) | | `/data/home/` | `$HOME` for the in-container `kandev` user — `gh` CLI and agent CLI auth state | ### Persistent agent and selectable `gh` CLI auth The image sets `HOME=/data/home`, so every CLI that writes its auth under `$HOME` lands on the volume and survives container restarts and image upgrades: - `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//...` A one-time `docker exec -it kandev gh auth login` makes that named GitHub account available for selection in each workspace. Agent CLI logins such as `claude login` and `codex login` also persist, so you do not need to redo them after `docker pull` and recreating the container. > Workspace PATs are stored in Kandev's encrypted secret store. A workspace configured for GitHub CLI resolves the exact selected host/login without changing the active CLI account. Host-active `gh`, backend `GITHUB_TOKEN`/`GH_TOKEN`, and old globally named secrets are consulted only by migration-only **Legacy shared** connections. Set `KANDEV_GITHUB_CREDENTIAL_BROKER_PUBLIC_BASE_URL` to an HTTPS Kandev URL reachable from agent containers and remote executors. Set the stable secret `KANDEV_GITHUB_CREDENTIAL_BROKER_REISSUE_SIGNING_KEY` to let a running executor recover its managed lease after a backend restart; changing this key invalidates outstanding execution capabilities. Drain or quiesce active agent sessions before key rotation. This is independent of GitHub App setup and is required for managed PAT or CLI credentials outside local/worktree execution. The reverse proxy must pass `GET` and `POST` on `/api/v1/git/credentials/resolve` and `POST` on `/api/v1/git/credentials/reissue`; the older `/api/v1/github/credentials/*` paths remain compatibility aliases. Executors require the non-secret `GET` readiness response before startup, credential redemption uses the lease-authenticated `POST`, and reissue uses an execution-scoped capability. ## Configuration Configuration is done via `KANDEV_`-prefixed environment variables: ```bash docker run -p 38429:38429 \ -v kandev-data:/data \ -e KANDEV_LOG_LEVEL=debug \ ghcr.io/kdlbs/kandev:latest ``` ### Environment Variables See [`configuration.md`](configuration.md) for the full reference (including the YAML form and every knob the backend reads). The table below covers the env vars most often set in a Docker deployment. | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `KANDEV_HOME_DIR` | No | `/data` | Kandev home directory - contains `data/` (DB), `tasks/`, `worktrees/`, `repos/`, `sessions/`, and `lsp-servers/` | | `KANDEV_DATABASE_DRIVER` | No | `sqlite` | Database driver (`sqlite` or `postgres`) | | `KANDEV_DATABASE_PATH` | No | `$KANDEV_HOME_DIR/data/kandev.db` | SQLite database file path (override) | | `KANDEV_LOG_LEVEL` | No | `info` | Log level: `debug`, `info`, `warn`, `error` | | `KANDEV_LOGGING_FORMAT` | No | `text` | Log format: `text` or `json` | | `KANDEV_LOGGING_OUTPUTPATH` | No | `stdout` | Log destination: `stdout`, `stderr`, or a file path (rotated when a file) | | `KANDEV_LOGGING_MAXSIZEMB` | No | `100` | Rotate the log file when it exceeds this size (MB). File output only. | | `KANDEV_LOGGING_MAXBACKUPS` | No | `5` | Max rotated files to retain (`0` = unlimited). File output only. | | `KANDEV_LOGGING_MAXAGEDAYS` | No | `30` | Max age of rotated files in days (`0` = unlimited). File output only. | | `KANDEV_LOGGING_COMPRESS` | No | `true` | Gzip rotated files. File output only. | | `KANDEV_DOCKER_ENABLED` | No | `false` | Enable Docker runtime for agents (see below) | > **File-mode note:** when `KANDEV_LOGGING_OUTPUTPATH` is a file path, the active log file is created with mode `0600` (owner read/write only). Run any log shipper or sidecar as the same user, or use `stdout`/`stderr` and let the container runtime collect logs. > **Upgrading from a pre-`KANDEV_HOME_DIR` image?** The SQLite DB path moved from `/data/kandev.db` to `/data/data/kandev.db`. When no explicit database path is set, the backend checks the legacy `kandev.db` and, when valid, installs a validated snapshot at the current path while retaining the legacy database and its `-wal`/`-shm` files. Look for the `SQLite database selected` diagnostic with outcome `legacy_adopted`. If both candidates need operator review, startup stops and names both paths; preserve them and select the intended file with `KANDEV_DATABASE_PATH`. To keep the old location explicitly, set `-e KANDEV_DATABASE_PATH=/data/kandev.db`. If you previously set `KANDEV_DATA_DIR`, replace it with `KANDEV_HOME_DIR`. ### PostgreSQL To use PostgreSQL instead of SQLite: ```bash docker run -p 38429:38429 \ -e KANDEV_DATABASE_DRIVER=postgres \ -e KANDEV_DATABASE_HOST=host.docker.internal \ -e KANDEV_DATABASE_PORT=5432 \ -e KANDEV_DATABASE_USER=kandev \ -e KANDEV_DATABASE_PASSWORD=secret \ -e KANDEV_DATABASE_DBNAME=kandev \ ghcr.io/kdlbs/kandev:latest ``` ## Port Kandev exposes a single port. The Go backend serves the API, WebSocket, static SPA assets, and page boot data — all on one port: | Port | Service | |------|---------| | `38429` | API + WebSocket + Web UI | Override the port: ```bash docker run -p 9080:9080 \ -v kandev-data:/data \ ghcr.io/kdlbs/kandev:latest \ kandev start --backend-port 9080 ``` ## Docker Compose Create a `docker-compose.yml`: ```yaml services: kandev: image: ghcr.io/kdlbs/kandev:latest ports: - "38429:38429" volumes: - kandev-data:/data restart: unless-stopped volumes: kandev-data: ``` ```bash docker compose up -d ``` ### With PostgreSQL ```yaml services: kandev: image: ghcr.io/kdlbs/kandev:latest ports: - "38429:38429" volumes: - kandev-data:/data environment: KANDEV_DATABASE_DRIVER: postgres KANDEV_DATABASE_HOST: postgres KANDEV_DATABASE_PORT: "5432" KANDEV_DATABASE_USER: kandev KANDEV_DATABASE_PASSWORD: secret KANDEV_DATABASE_DBNAME: kandev depends_on: postgres: condition: service_healthy restart: unless-stopped postgres: image: postgres:17 environment: POSTGRES_USER: kandev POSTGRES_PASSWORD: secret POSTGRES_DB: kandev volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U kandev"] interval: 5s timeout: 3s retries: 5 restart: unless-stopped volumes: kandev-data: postgres-data: ``` ## Reverse Proxy Since Kandev serves everything on a single port, a reverse proxy only needs to forward all traffic to port 38429. No extra environment variables are needed — the frontend automatically uses `window.location.origin` to reach the API. ### Docker Compose with Caddy ```yaml services: kandev: image: ghcr.io/kdlbs/kandev:latest volumes: - kandev-data:/data restart: unless-stopped caddy: image: caddy:2 ports: - "80:80" - "443:443" volumes: - caddy-data:/data - ./Caddyfile:/etc/caddy/Caddyfile restart: unless-stopped volumes: kandev-data: caddy-data: ``` Example `Caddyfile`: ``` kandev.example.com { reverse_proxy kandev:38429 } ``` ## Docker-in-Docker (Agent Containers) By default, `KANDEV_DOCKER_ENABLED=false` inside the container. To enable Docker-based agent execution, mount the Docker socket: ```bash docker run -p 38429:38429 \ -v kandev-data:/data \ -v /var/run/docker.sock:/var/run/docker.sock \ -e KANDEV_DOCKER_ENABLED=true \ ghcr.io/kdlbs/kandev:latest ``` > **Note:** Mounting the Docker socket gives the container full access to the host's Docker daemon. Only do this in trusted environments. ## Upgrading ```bash docker pull ghcr.io/kdlbs/kandev:latest docker compose up -d # or: docker stop kandev && docker rm kandev && docker run ... ``` The volume at `/data` carries over the database, worktrees, npm globals, and `$HOME` for agent CLIs, so there is no manual migration step. > **Upgrading across the `HOME=/data/home` change:** if you previously ran `docker 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` inside the container and is not carried over. Log in once on the new container and it will persist for all subsequent upgrades. If you want to preserve the old state, copy it onto the volume before recreating the container: > > ```bash > docker exec kandev sh -c 'cp -a /home/kandev/. /data/home/ 2>/dev/null || true' > ``` ## Health Check The backend exposes a `/health` endpoint (liveness: the process is up and its listener is bound, even mid-startup) and a `/ready` endpoint (readiness: startup finished and it can serve real traffic): ```bash curl http://localhost:38429/health curl http://localhost:38429/ready ``` Compose's single `healthcheck:` concept maps to "can serve real traffic," so it should point at `/ready`, not `/health`: ```yaml healthcheck: test: ["CMD", "curl", "-f", "http://localhost:38429/ready"] interval: 30s timeout: 5s retries: 3 start_period: 15s ``` ## Troubleshooting ```bash # View logs docker logs kandev # Follow logs docker logs -f kandev # Shell into the container docker exec -it kandev /bin/bash # Check data volume docker volume inspect kandev-data ```