Files
2026-09-12 16:53:15 +01:00

323 lines
15 KiB
Markdown

---
title: "Docker"
description: "Run the Kandev control plane in Docker and understand Docker-based agent execution."
---
# Docker
The published image runs the Kandev control plane from a native Go binary. The
binary contains the compiled web UI and serves the API, WebSocket endpoint, and
external MCP endpoint. The image also includes the host-side `agentctl`. This
is different from the **Local Docker executor**, which creates a separate
container for an agent.
For Kubernetes, see [Kubernetes](k8s.md). For executor profiles, see [Executors](executors.md#local-docker).
## Quick start
Bind to host loopback unless an authenticated reverse proxy protects the service:
```bash
docker volume create kandev-data
docker run -d \
--name kandev \
--restart unless-stopped \
-p 127.0.0.1:38429:38429 \
-v kandev-data:/data \
ghcr.io/kdlbs/kandev:latest
```
Open `http://localhost:38429` and follow logs with:
```bash
docker logs -f kandev
```
Kandev ships with its experimental [Authentication & Users](authentication.md) feature disabled, so the default container has no multi-user web login or API authorization boundary. Setting `auth.jwtSecret` does not enable it. Docker's unqualified `-p 38429:38429` publishes on every host interface, so do not use that form on an untrusted network. Use loopback, a private network/VPN, or an authenticated reverse proxy with TLS.
## Published images
<details>
<summary>Published image details</summary>
The release workflow publishes multi-architecture `linux/amd64` and `linux/arm64` images to `ghcr.io/kdlbs/kandev`.
| Flavor | Moving tag | Version tags | Contents |
|---|---|---|---|
| Base | `latest` | `X.Y.Z`, `vX.Y.Z` | Kandev, Node 24/npm, Git, `gh`, Python/pipx, Apprise, Azure CLI, and the Azure DevOps extension |
| Universal | `universal` | `X.Y.Z-universal`, `vX.Y.Z-universal` | Base plus Go, Rust, pnpm, build tools, common developer CLIs, and Playwright Chromium system libraries |
The universal image does not include Playwright browser downloads, JDKs, .NET, or database servers. Its tool versions are pinned in `Dockerfile.universal` for each release. See the [image guide](https://github.com/kdlbs/kandev/blob/main/docs/images.md) for the inclusion policy and derived-image examples.
Use a version tag or digest in a persistent deployment:
```bash
docker pull ghcr.io/kdlbs/kandev:X.Y.Z
docker pull ghcr.io/kdlbs/kandev:X.Y.Z-universal
```
Replace `X.Y.Z` with a real release. `latest` moves on release. `universal` moves on release and is also rebuilt from the current `latest` base each Monday by the repository's scheduled workflow; dated `universal-weekly-YYYYMMDD` tags identify those rebuilds. Release-versioned universal tags remain unchanged.
The repository root `Dockerfile` is release packaging, not a source-build Dockerfile. The release workflow first builds a native bundle for each architecture, prepares a build context containing `bundle/` plus `docker-entrypoint.sh`, builds the amd64 and arm64 images separately, and joins their digests into the published manifest. A plain `docker build .` from a checkout fails because `bundle/` is intentionally absent.
For extra OS tools, derive from a pinned published image so the release bundle and entrypoint stay intact:
```dockerfile
FROM ghcr.io/kdlbs/kandev:X.Y.Z
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends postgresql-client \
&& rm -rf /var/lib/apt/lists/*
```
Leaving the final configured user as root is intentional for this base flavor: the inherited entrypoint repairs `/data` and drops to `kandev` before starting the command. If a derivative finishes with `USER kandev`, provision writable volume ownership in advance, as the universal flavor does.
</details>
> **Persistence:** Always mount `/data`. Removing a container without a volume removes its database, workspaces, installed agent CLIs, and authentication state.
<details>
<summary>Image runtime behavior and persistence details</summary>
## Image runtime behavior
The base image:
- uses Debian Bookworm and exposes only TCP 38429;
- sets `KANDEV_HOME_DIR=/data`, `HOME=/data/home`, and `NPM_CONFIG_PREFIX=/data/.npm-global`;
- disables the Docker executor with `KANDEV_DOCKER_ENABLED=false`;
- starts `kandev start --backend-port 38429 --verbose` under `tini`;
- starts as root only long enough to create `/data/home` and recursively `chown /data`, then runs Kandev as user `kandev` (UID 1000) through `gosu`.
The universal derivative sets `USER kandev`, so it skips the base entrypoint's root ownership repair. Pre-create a writable bind mount before using that flavor. Recursive ownership repair in the base image makes named volumes convenient, but can be slow on a large bind mount and can fail on root-squashed network storage. To run either image directly as UID 1000, pre-create every required path with suitable ownership and test the storage driver.
## Persistence
| Volume path | Data |
|---|---|
| `/data/data/` | SQLite `kandev.db`, WAL/SHM files, and SQLite backup snapshots |
| `/data/tasks/`, `/data/worktrees/`, `/data/repos/`, `/data/sessions/`, `/data/lsp-servers/` | Task and repository runtime state |
| `/data/agent-sessions/` | Selectively seeded per-execution agent credential/session directories for Docker executors |
| `/data/.npm-global/` | Agent CLIs installed at runtime |
| `/data/home/` | Persistent home for `gh`, agent CLI auth, Azure config, caches, and user configuration |
For a bind mount:
```bash
sudo install -d -o 1000 -g 1000 /srv/kandev
docker run -d \
--name kandev \
-p 127.0.0.1:38429:38429 \
-v /srv/kandev:/data \
ghcr.io/kdlbs/kandev:X.Y.Z
```
Confirm UID/GID policy on your host before copying this example. The image guarantees UID 1000 for the user but does not promise GID 1000 for its primary group; its root entrypoint normally corrects ownership using the image's actual group.
### CLI authentication inside the container
The base and universal flavors have different configured users. Always select `kandev` explicitly for interactive login and installs so files under `/data/home` remain usable by the service:
```bash
docker exec --user kandev -it kandev gh auth login
docker exec --user kandev -it kandev sh
```
The same rule applies to `claude login`, `codex login`, and manual `npm install -g` commands. Treat `/data/home` and the database as secret material when backing them up.
</details>
## Configuration
Pass backend settings as environment variables or mount a read-only `/etc/kandev/config.yaml`. See [Configuration](configuration.md) for the canonical field names, environment mapping, validation, and precedence.
Container-specific defaults are:
| Setting | Image value | Notes |
|---|---|---|
| `KANDEV_HOME_DIR` | `/data` | Root for database and runtime state |
| `HOME` | `/data/home` | Persistent CLI credentials and caches |
| `NPM_CONFIG_PREFIX` | `/data/.npm-global` | Runtime-installed npm CLIs |
| `KANDEV_DOCKER_ENABLED` | `false` | Overrides the backend's ordinary host default |
| `KANDEV_NO_BROWSER` | `1` | Prevents browser launch |
| Internal listener | `38429` | Fixed by the image command unless that command is replaced |
| Default log level | `info` | The image command passes `--verbose`; an explicit `KANDEV_LOG_LEVEL` wins |
Example:
```bash
docker run -d \
--name kandev \
-p 127.0.0.1:38429:38429 \
-v kandev-data:/data \
-e KANDEV_LOG_LEVEL=warn \
ghcr.io/kdlbs/kandev:X.Y.Z
```
To expose a different host port, leave the internal command alone:
```bash
docker run -d \
--name kandev \
-p 127.0.0.1:9080:38429 \
-v kandev-data:/data \
ghcr.io/kdlbs/kandev:X.Y.Z
```
Open `http://localhost:9080`. If you replace the container command to change its internal port, publish that same port and retain `--verbose` if info logs are desired.
## Docker Compose
```yaml
services:
kandev:
image: ghcr.io/kdlbs/kandev:X.Y.Z
ports:
- "127.0.0.1:38429:38429"
volumes:
- kandev-data:/data
restart: unless-stopped
volumes:
kandev-data:
```
```bash
docker compose up -d
docker compose logs -f kandev
```
### PostgreSQL example
PostgreSQL moves database rows out of the Kandev volume, but `/data` is still required for workspaces, CLI installs, auth files, and other runtime state.
```yaml
services:
kandev:
image: ghcr.io/kdlbs/kandev:X.Y.Z
ports:
- "127.0.0.1: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: "${KANDEV_DB_PASSWORD:?set KANDEV_DB_PASSWORD}"
KANDEV_DATABASE_DBNAME: kandev
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:16
environment:
POSTGRES_USER: kandev
POSTGRES_PASSWORD: "${KANDEV_DB_PASSWORD:?set KANDEV_DB_PASSWORD}"
POSTGRES_DB: kandev
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U kandev -d kandev"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
volumes:
kandev-data:
postgres-data:
```
Put `KANDEV_DB_PASSWORD` in a permission-restricted deployment secret, not shell history or committed YAML. Kandev's built-in System backup/restore covers SQLite only; use `pg_dump` and a tested PostgreSQL restore procedure here.
## Reverse proxy and network policy
The backend serves SPA, API, `/ws`, `/mcp`, `/health`, and `/ready` on one origin. Proxy the entire root path and preserve WebSocket upgrades. Caddy does this automatically:
```text
kandev.example.com {
reverse_proxy kandev:38429
}
```
TLS alone is not authentication. Put an identity-aware/authenticated gateway in front, restrict direct access to the Kandev container, and apply CSRF/origin policy appropriate to that gateway. A subpath such as `/kandev/` is not a documented deployment base; prefer a dedicated host at `/`.
## Using Docker for agent environments
Host-installed Kandev can use **Settings > Executors > Docker** against a same-host daemon. The daemon must be reachable through global `docker.host`; the current runtime assumes bind-mount source paths exist on that daemon host. See [Local Docker executor](executors.md#local-docker) for image, credential, port, resume, and cleanup behavior.
### Containerized control plane limitation
Mounting `/var/run/docker.sock` into the Kandev service container is **not by itself a complete configuration**. Agent creation asks the daemon to bind-mount:
- the Linux `agentctl` helper path resolved inside the control-plane container;
- per-execution directories under the control plane's Kandev home;
- a local clone source in the filesystem-URL case.
The Docker daemon resolves those paths on the daemon host, not inside the Kandev container. A named `/data` volume and the image's `/app/...` helper therefore do not automatically exist at matching host paths. The current Docker executor also selects a Linux/amd64 helper unconditionally, so the agent container must be Linux/amd64-compatible (native or correctly emulated).
For reliable Local Docker execution, run the Kandev control plane on the Docker host. Alternatively, build a custom deployment that mirrors every required source at identical absolute host/container paths and test cleanup, architecture, permissions, and upgrades. This advanced layout has no supplied Compose or Kubernetes manifest. Use SSH or Sprites when the control plane itself must remain containerized.
Giving Kandev a Docker socket or daemon API grants near-root control of that host. Protect the endpoint, never expose an unauthenticated TCP daemon, and do not give untrusted Kandev users profile-build access.
Remote Docker profiles are not a workaround: that executor runtime is currently unimplemented.
## Health and observability
`GET /health` returns 200 as soon as the listener is accepting connections, even mid-startup. `GET /ready` returns 503 during startup and includes the current startup phase and elapsed time; it returns 200 after routes are registered:
```bash
curl --fail http://localhost:38429/ready
```
Neither is a deep database, Git, Docker, provider, or agent check. A Compose health check should use `/ready`, since Compose's single healthcheck concept (`docker ps` status, `depends_on: condition: service_healthy`) maps to "can serve real traffic," not just "process is alive":
```yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:38429/ready"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
```
The backend writes its active file to `/data/logs/backend-logs.log` on the persistent volume and prints the path at startup. Each segment accepts at most 16 MiB and is named `backend-logs-YYYY-MM-DD-NNNNNN.log` when it rolls. Active and closed backend files use at most 256 MiB in total. Kandev removes the oldest closed segments when needed, so high-volume periods keep the newest evidence. Three UTC days is the maximum file age. `docker logs` shows the bounded stdout stream; use **Settings > System > Logs** for a frontend+backend diagnostic ZIP.
## Upgrade and remove
Back up first; then pull and recreate with the same `/data` volume and configuration:
```bash
docker compose pull kandev
docker compose up -d kandev
docker compose logs -f kandev
```
SQLite schema migrations run at startup and create a pre-migration SQLite snapshot when needed. PostgreSQL does not receive that built-in snapshot. A container/image rollback does not roll back the database schema, so keep a tested backup from before the upgrade. See [Operations](operations.md) for backup and restore details.
Stopping or removing the service container leaves the named volume:
```bash
docker stop kandev
docker rm kandev
docker volume inspect kandev-data
```
Delete `kandev-data` only after verifying that its database, workspaces, and credentials are no longer needed.
## Troubleshooting
- **UI unreachable:** check `docker ps`, published address/port, host firewall, and `docker logs kandev`; then call `/ready`.
- **Permission denied under `/data`:** inspect mount ownership and root-squash behavior. The runtime user is UID 1000 after entrypoint setup.
- **CLI login works only as root:** repeat it with `docker exec --user kandev`; repair ownership of the affected files before restarting.
- **Image pull fails:** authenticate to GHCR if your network policy requires it and verify the tag/platform with `docker buildx imagetools inspect`.
- **Database connection fails:** test DNS/TCP from the Kandev container, credentials, database name, and `sslMode`.
- **WebSocket disconnects behind proxy:** forward the whole origin, enable upgrade support, and increase proxy idle timeouts.
- **Docker agent container fails to mount helper/session paths:** the control plane is probably containerized or using a remote daemon whose filesystem paths do not match; use a same-host control plane or a fully mirrored custom layout.
- **Disk growth:** inspect `/data`, retained Docker agent containers, image layers/build cache, and Docker volumes before deleting anything.
Related pages: [Configuration](configuration.md), [Executors](executors.md), [Operations](operations.md), and [Run as a Service](run-as-a-service.md).