feat: add Dockerfile, K8s manifests, and deployment docs (#303)

This commit is contained in:
Carlos Florêncio
2026-02-27 16:38:53 +00:00
committed by GitHub
parent 50b3543bf1
commit 9e995ba1b8
12 changed files with 781 additions and 1 deletions
+51
View File
@@ -0,0 +1,51 @@
# Git
.git
.gitignore
# Build artifacts
apps/backend/bin/
apps/web/.next/
apps/web/out/
dist/
# Dependencies (rebuilt in Docker)
node_modules/
apps/web/node_modules/
apps/cli/node_modules/
apps/packages/*/node_modules/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
# Docker
Dockerfile
.dockerignore
# CI/CD
.github/
# Documentation and config not needed at build time
docs/
k8s/
*.md
!apps/web/src/**/*.md
LICENSE
# Test artifacts
coverage/
*.test.ts
*.test.go
**/*_test.go
# Agent/Claude config
.claude/
.agents/
CLAUDE.md
AGENTS.md
+36
View File
@@ -8,6 +8,7 @@ on:
permissions:
contents: write
packages: write
jobs:
build-web:
@@ -114,6 +115,41 @@ jobs:
dist/kandev-${{ matrix.platform }}.tar.gz
dist/kandev-${{ matrix.platform }}.tar.gz.sha256
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/kdlbs/kandev
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
publish-release:
needs: build-bundles
runs-on: ubuntu-latest
+2
View File
@@ -18,6 +18,8 @@ apps/
- **Backend**: Go with Make (`make -C apps/backend test|lint|build`)
- **Frontend**: Next.js (`cd apps && pnpm --filter @kandev/web dev|lint|typecheck`)
- **UI**: Shadcn components via `@kandev/ui`
- **GitHub repo**: `https://github.com/kdlbs/kandev`
- **Container image**: `ghcr.io/kdlbs/kandev` (GitHub Container Registry)
---
+111
View File
@@ -0,0 +1,111 @@
# Kandev Server Dockerfile
# Multi-stage build: Go backend + Next.js frontend + CLI launcher
#
# Build:
# docker build -t kandev:latest .
#
# Run:
# docker run -p 8080:8080 -p 3000:3000 -v kandev-data:/data kandev:latest
# ---------------------------------------------------------------------------
# Stage 1: Go builder — compile kandev + agentctl binaries
# ---------------------------------------------------------------------------
FROM golang:1.26-bookworm AS go-builder
WORKDIR /build
# Cache Go module downloads
COPY apps/backend/go.mod apps/backend/go.sum ./
RUN go mod download
# Copy backend source and build
COPY apps/backend/ ./
RUN go build -ldflags "-s -w" -o /out/kandev ./cmd/kandev && \
go build -ldflags "-s -w" -o /out/agentctl ./cmd/agentctl
# ---------------------------------------------------------------------------
# Stage 2: Web + CLI builder — build Next.js standalone + CLI
# ---------------------------------------------------------------------------
FROM node:24-slim AS web-builder
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /build/apps
# Copy workspace config and all package.json files for dependency caching
COPY apps/package.json apps/pnpm-workspace.yaml apps/pnpm-lock.yaml ./
COPY apps/web/package.json ./web/package.json
COPY apps/cli/package.json ./cli/package.json
COPY apps/packages/ui/package.json ./packages/ui/package.json
COPY apps/packages/theme/package.json ./packages/theme/package.json
COPY apps/packages/types/package.json ./packages/types/package.json
RUN pnpm install --frozen-lockfile
# Copy full source for build
COPY apps/ ./
# Build shared packages, web app, and CLI
RUN pnpm --filter @kandev/web build && \
pnpm --filter kandev build
# ---------------------------------------------------------------------------
# Stage 3: Runtime — minimal image with both services
# ---------------------------------------------------------------------------
FROM node:24-bookworm-slim AS runtime
# Install only essential runtime dependencies, then clean up
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git \
ca-certificates \
tini && \
rm -rf /var/lib/apt/lists/*
# Create app directory structure matching what the CLI expects:
# /app/apps/backend/bin/kandev
# /app/apps/web/.next/standalone/web/server.js
RUN mkdir -p /app/apps/backend/bin /app/apps/web/.next /data/worktrees
# Copy Go binaries
COPY --from=go-builder /out/kandev /app/apps/backend/bin/kandev
COPY --from=go-builder /out/agentctl /usr/local/bin/agentctl
# Copy Next.js standalone output
COPY --from=web-builder /build/apps/web/.next/standalone/ /app/apps/web/.next/standalone/
# Copy static assets and public directory into standalone output
COPY --from=web-builder /build/apps/web/.next/static/ /app/apps/web/.next/standalone/web/.next/static/
COPY --from=web-builder /build/apps/web/public/ /app/apps/web/.next/standalone/web/public/
# Install CLI: copy built output + package.json, install prod deps, link binary
COPY --from=web-builder /build/apps/cli/dist/ /usr/local/lib/kandev-cli/dist/
COPY --from=web-builder /build/apps/cli/bin/ /usr/local/lib/kandev-cli/bin/
COPY --from=web-builder /build/apps/cli/package.json /usr/local/lib/kandev-cli/
RUN cd /usr/local/lib/kandev-cli && npm install --omit=dev && \
chmod +x bin/cli.js && \
ln -s /usr/local/lib/kandev-cli/bin/cli.js /usr/local/bin/kandev
# Data directory for SQLite and worktrees
VOLUME ["/data"]
# Environment defaults for containerized operation
ENV KANDEV_NO_BROWSER=1 \
KANDEV_DATABASE_PATH=/data/kandev.db \
KANDEV_WORKTREE_BASEPATH=/data/worktrees \
KANDEV_DOCKER_ENABLED=false \
HOSTNAME=0.0.0.0 \
NODE_ENV=production
# Run as non-root
RUN chown -R node:node /app /data
USER node
WORKDIR /app
EXPOSE 8080 3000
# tini as PID 1 for signal handling; CLI manages backend + web processes
ENTRYPOINT ["tini", "--"]
CMD ["kandev", "start", "--backend-port", "8080", "--web-port", "3000", "--verbose"]
+1 -1
View File
@@ -110,7 +110,7 @@ export async function runStart({
// Production mode: use warn log level for clean output unless verbose/debug
const showOutput = verbose || debug;
const dbPath = path.join(DATA_DIR, "kandev.db");
const dbPath = process.env.KANDEV_DATABASE_PATH || path.join(DATA_DIR, "kandev.db");
const logLevel = debug ? "debug" : verbose ? "info" : "warn";
const backendEnv = buildBackendEnv({
ports,
+217
View File
@@ -0,0 +1,217 @@
# Docker Guide
Run Kandev in a Docker container. For Kubernetes deployment, see [k8s.md](k8s.md).
## Quick Start
```bash
docker run -p 8080:8080 -p 3000:3000 -v kandev-data:/data ghcr.io/kdlbs/kandev:latest
```
Open `http://localhost:3000` 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
```
## 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.
## Configuration
Configuration is done via `KANDEV_`-prefixed environment variables:
```bash
docker run -p 8080:8080 -p 3000:3000 \
-v kandev-data:/data \
-e KANDEV_LOG_LEVEL=debug \
ghcr.io/kdlbs/kandev:latest
```
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) |
| `KANDEV_DATABASE_PATH` | `/data/kandev.db` | SQLite database file path |
| `KANDEV_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
| `KANDEV_LOGGING_FORMAT` | `text` | Log format: `text` or `json` |
| `KANDEV_DOCKER_ENABLED` | `false` | Enable Docker runtime for agents (see below) |
| `KANDEV_WORKTREE_BASEPATH` | `/data/worktrees` | Git worktree storage directory |
### PostgreSQL
To use PostgreSQL instead of SQLite:
```bash
docker run -p 8080:8080 -p 3000:3000 \
-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
```
## Ports
| Port | Service |
|------|---------|
| `8080` | Backend API + WebSocket |
| `3000` | Web UI |
Override ports by changing the published ports and passing CLI flags:
```bash
docker run -p 9080:9080 -p 4000:4000 \
-v kandev-data:/data \
ghcr.io/kdlbs/kandev:latest \
kandev start --backend-port 9080 --web-port 4000
```
## Docker Compose
Create a `docker-compose.yml`:
```yaml
services:
kandev:
image: ghcr.io/kdlbs/kandev:latest
ports:
- "8080:8080"
- "3000:3000"
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:
- "8080:8080"
- "3000:3000"
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:
```
## 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 8080:8080 -p 3000:3000 \
-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.
## Health Check
The backend exposes a `/health` endpoint:
```bash
curl http://localhost:8080/health
```
For Docker health checks in compose:
```yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
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
```
+202
View File
@@ -0,0 +1,202 @@
# Kubernetes Deployment Guide
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
From the repository root:
```bash
# Build for your current architecture
docker build -t kandev:latest .
# Build for a specific architecture (e.g., for amd64 clusters)
docker build --platform linux/amd64 -t kandev:latest .
# Multi-arch build (requires buildx)
docker buildx build --platform linux/amd64,linux/arm64 -t kandev:latest .
```
### Using the Pre-built Image
Kandev publishes images to GitHub Container Registry. Pull directly:
```bash
docker pull ghcr.io/kdlbs/kandev:latest
```
Or reference it in your K8s deployment:
```yaml
image: ghcr.io/kdlbs/kandev:latest
```
## Deploying to Kubernetes
### Quick Start
```bash
# Apply all manifests
kubectl apply -f k8s/
# Check status
kubectl get pods -l app=kandev
kubectl logs -l app=kandev -f
```
### What Gets Created
| Resource | File | Purpose |
|----------|------|---------|
| Deployment | `deployment.yaml` | Single-replica pod running backend + web |
| Service | `service.yaml` | ClusterIP exposing ports 8080 (API) and 3000 (UI) |
| ConfigMap | `configmap.yaml` | Non-sensitive environment configuration |
| PVC | `pvc.yaml` | 10Gi persistent volume for SQLite + worktrees |
| Ingress | `ingress.yaml` | Example ingress with WebSocket support |
### Accessing the UI
**Port-forward** (quickest for testing):
```bash
kubectl port-forward svc/kandev 3000:3000 8080:8080
# Open http://localhost:3000
```
**Ingress**: Edit `k8s/ingress.yaml` to set your domain, then apply. The ingress routes `/api` and `/ws` to the backend (port 8080) and everything else to the web UI (port 3000).
## Configuration
Kandev reads configuration via `KANDEV_`-prefixed environment variables (Viper). Set these in `k8s/configmap.yaml` or as environment variables in the deployment.
### Core Settings
| Env Var | Default | Description |
|---------|---------|-------------|
| `KANDEV_SERVER_PORT` | `8080` | Backend API port |
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) |
| `KANDEV_DATABASE_PATH` | `/data/kandev.db` | SQLite database file path |
| `KANDEV_DOCKER_ENABLED` | `false` | Enable Docker runtime for agents (requires DinD) |
| `KANDEV_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
| `KANDEV_LOGGING_FORMAT` | auto | Log format: `json` (auto-detected in K8s) or `text` |
| `KANDEV_WORKTREE_BASEPATH` | `/data/worktrees` | Git worktree storage directory |
### PostgreSQL Settings (when `KANDEV_DATABASE_DRIVER=postgres`)
| Env Var | Default | Description |
|---------|---------|-------------|
| `KANDEV_DATABASE_HOST` | `localhost` | PostgreSQL host |
| `KANDEV_DATABASE_PORT` | `5432` | PostgreSQL port |
| `KANDEV_DATABASE_USER` | `kandev` | Database user |
| `KANDEV_DATABASE_PASSWORD` | (empty) | Database password |
| `KANDEV_DATABASE_DBNAME` | `kandev` | Database name |
| `KANDEV_DATABASE_SSLMODE` | `disable` | SSL mode |
## Database: SQLite vs PostgreSQL
### SQLite (default)
- Zero-config, works out of the box
- Database stored at `/data/kandev.db` on the PV
- **Single replica only** (SQLite is single-writer)
- Deployment strategy is `Recreate` to prevent concurrent writes
- Good for small teams / personal use
### PostgreSQL (recommended for production)
- Supports multiple replicas for horizontal scaling
- Change deployment strategy to `RollingUpdate`
- Set via environment variables:
```yaml
# In configmap.yaml or a Secret
KANDEV_DATABASE_DRIVER: postgres
KANDEV_DATABASE_HOST: postgres.default.svc.cluster.local
KANDEV_DATABASE_PORT: "5432"
KANDEV_DATABASE_USER: kandev
KANDEV_DATABASE_PASSWORD: <from-secret>
KANDEV_DATABASE_DBNAME: kandev
```
When using Postgres, the PVC is still needed for worktree storage but the database itself is external.
## Persistent Storage
The PVC at `/data` stores:
- **SQLite database** (`/data/kandev.db`, `/data/kandev.db-wal`, `/data/kandev.db-shm`)
- **Git worktrees** (`/data/worktrees/`) for workspace isolation
The PVC uses `ReadWriteOnce` access mode. If your cluster requires a specific StorageClass, add it to `k8s/pvc.yaml`:
```yaml
spec:
storageClassName: your-storage-class
```
## Health Checks
The deployment includes both probes on the backend `/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)**: Switch to Postgres, change the deployment strategy to `RollingUpdate`, and increase replicas:
```yaml
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
```
## Upgrading
```bash
# Build and push new image
docker build -t your-registry.com/kandev:v1.1.0 .
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.
## Troubleshooting
```bash
# 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
```
+18
View File
@@ -0,0 +1,18 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: kandev-config
labels:
app: kandev
data:
# Database — SQLite on the PV. Set KANDEV_DATABASE_DRIVER=postgres for production.
KANDEV_DATABASE_PATH: /data/kandev.db
# Disable Docker runtime (no Docker-in-Docker in K8s)
KANDEV_DOCKER_ENABLED: "false"
# Worktree storage on the PV
KANDEV_WORKTREE_BASEPATH: /data/worktrees
# Logging — auto-detects K8s and uses JSON format
KANDEV_LOG_LEVEL: info
+62
View File
@@ -0,0 +1,62 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: kandev
labels:
app: kandev
spec:
# SQLite is single-writer — use 1 replica. Switch to Postgres for horizontal scaling.
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: kandev
template:
metadata:
labels:
app: kandev
spec:
containers:
- name: kandev
image: kandev:latest
ports:
- name: backend
containerPort: 8080
protocol: TCP
- name: web
containerPort: 3000
protocol: TCP
envFrom:
- configMapRef:
name: kandev-config
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
livenessProbe:
httpGet:
path: /health
port: backend
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: backend
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: kandev-data
+50
View File
@@ -0,0 +1,50 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kandev
labels:
app: kandev
annotations:
# Adjust annotations for your ingress controller (nginx, traefik, etc.)
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# WebSocket support
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
spec:
rules:
- host: kandev.example.com
http:
paths:
# Backend API + WebSocket
- path: /api
pathType: Prefix
backend:
service:
name: kandev
port:
name: backend
- path: /ws
pathType: Prefix
backend:
service:
name: kandev
port:
name: backend
- path: /health
pathType: Exact
backend:
service:
name: kandev
port:
name: backend
# Web UI (catch-all)
- path: /
pathType: Prefix
backend:
service:
name: kandev
port:
name: web
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: kandev-data
labels:
app: kandev
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
+19
View File
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: kandev
labels:
app: kandev
spec:
type: ClusterIP
selector:
app: kandev
ports:
- name: backend
port: 8080
targetPort: backend
protocol: TCP
- name: web
port: 3000
targetPort: web
protocol: TCP