services: backend: restart: unless-stopped # Published image of the checked-out version (KT-1468): `make start` pulls # it instead of compiling the 715 k-line backend crate (19 GB peak) on the # user's machine. `build:` stays for `make start-build` and for a VERSION # with no published image yet. The tag below is bumped with VERSION (`make bump`). image: ghcr.io/docroms/kronn-backend:${KRONN_IMAGE_TAG:-0.15.2} build: context: ./backend dockerfile: Dockerfile args: CARGO_PROFILE: ${CARGO_PROFILE:-fast} APP_UID: ${KRONN_HOST_UID:-1000} APP_GID: ${KRONN_HOST_GID:-1000} # The host's Docker socket (and its group) is NOT mounted by default: any # process in this container — every agent — could use it to become root on # the host. `KRONN_DOCKER_SOCKET=1` in .env adds both through the generated # docker-compose.override.yml (KT-979). extra_hosts: - "host.docker.internal:host-gateway" # Linux: resolve host.docker.internal → host IP (macOS/Windows have this built-in) expose: - "3140" volumes: - kronn-data:/data # Run artifacts (KT-1585): outside /data, which agents cannot read, and a # volume so a run resumed after a restart still finds its files. - kronn-run-artifacts:/run-artifacts - ${HOME}:/host-home:ro - ${KRONN_REPOS_DIR:-${HOME}/Repositories}:/host-home/${KRONN_REPOS_REL:-Repositories}:rw # rw for agent writes # 0.8.3 — Self-mount Repositories at the SAME absolute host path inside # the container. Required for docker-in-docker workflows (Exec steps # that shell out to `docker compose run/exec` from a worktree). Without # this, the cwd seen by Kronn is `/host-home/Repositories/...`, but the # host docker daemon receives that same string as the volume source — # which doesn't exist on the host (only `${HOME}/Repositories/...` does). # Self-mounting makes the path resolve to the same bind on both sides, # so `docker compose` volume specs like `./:/app` mount the right files. # Mirrors the pattern at L36 for ~/.local/share (TD-20260513-docker-in-docker-volume). - ${KRONN_REPOS_DIR:-${HOME}/Repositories}:${KRONN_REPOS_DIR:-${HOME}/Repositories}:rw - ${KRONN_GLOBAL_BIN:-/usr/local/bin}:/host-bin/global:ro - ${KRONN_GLOBAL_LIB:-/usr/local/lib}:/host-bin/lib:ro # npm global node_modules (sibling of /host-bin/global so `../lib/node_modules/...` relative symlinks resolve — fixes Fastly CLI which ships as a JS wrapper when installed via `npm -g`) - ${HOME}/.local/bin:/host-bin/local:ro - ${KRONN_NPM_BIN:-./.docker/empty}:/host-bin/npm:ro # npm global bin (macOS: ~/.npm/bin or /opt/homebrew/bin) - /usr/bin:/host-bin/sysbin:ro # Host system binaries (make, php, etc.) - /usr/sbin:/host-bin/syssbin:ro # Host system admin binaries # 0.8.2 — Common version-manager paths so workflow Exec steps can # run `cargo` / `bun` / etc. without the user symlinking each tool # into ~/.local/bin. Each path goes through env-var indirection with # an EMPTY-DIRECTORY fallback (`./.docker/empty`) so Kronn STARTS even # when the user doesn't have rustup / bun installed. # # ⚠️ The fallback is a real empty directory, NOT `/dev/null`. On WSL2 + # Docker Desktop the daemon runs in a VM whose cross-VM layer cannot relay # the /dev/null character device as a bind SOURCE — runc creates /dev/null # as a directory in the container, `open(/dev/null)` fails with "is a # directory", and container init crashes (`make start`). An empty dir binds # cleanly everywhere and degrades gracefully (empty dir on PATH = no tool). # # To enable a real path, set the env var in your shell or a `.env` file: # KRONN_CARGO_BIN=$HOME/.cargo/bin (rustup default) # KRONN_BUN_BIN=$HOME/.bun/bin # KRONN_EXTRA_BIN=$HOME/.pyenv/shims (or any other path you need) - ${KRONN_CARGO_BIN:-./.docker/empty}:/host-bin/cargo:ro # rustup Rust toolchain (cargo, rustc, clippy, fmt) # rustup's `cargo` and `rustc` are SHIMS — they need `~/.rustup` to find # the actual toolchain. Self-mount at the same absolute path so the shim's # hardcoded `$HOME/.rustup` lookup works inside the container. Set # KRONN_RUSTUP_HOME to enable. When UNSET, the empty dir is mounted at a # harmless absolute path (the rustup shim simply isn't usable — same as # cargo being absent above). - ${KRONN_RUSTUP_HOME:-./.docker/empty}:${KRONN_RUSTUP_HOME:-/host-bin/rustup-empty}:ro - ${KRONN_BUN_BIN:-./.docker/empty}:/host-bin/bun:ro # Bun runtime + installer - ${KRONN_EXTRA_BIN:-./.docker/empty}:/host-bin/extra:ro # Escape hatch — anything else (asdf shims, pyenv, custom paths) - /usr/libexec/docker/cli-plugins:/usr/libexec/docker/cli-plugins:ro # Docker compose plugin # Read the machine's hostname mappings when project Docker endpoints are # inspected. `make start` snapshots the real host file because Docker # Desktop / OrbStack resolve a direct `/etc/hosts` bind inside their VM. - ${KRONN_HOSTS_SNAPSHOT:-./.docker/host-hosts-unavailable}:/host-etc/hosts:ro - ${HOME}/.local/share:/host-home/.local/share:ro # Symlink targets (claude, vibe) # Self-mount so absolute-path symlinks created by host installers # (e.g. `~/.local/bin/claude` → `/home//.local/share/claude/versions//claude`) # resolve inside the container at the same absolute host path. # Without this, find_binary detects the binary via fallback but # exec fails on the broken symlink target # (TD-20260507-claude-symlink-not-resolvable). - ${HOME}/.local/share:${HOME}/.local/share:ro # 0.8.11 (B6) — uncomment + set KRONN_BACKUP_DIR=/backups to store scheduled # DB backups on the HOST, outside the data volume (so a lost volume doesn't # lose the backups too). Point the left side wherever you keep backups. # - ${KRONN_BACKUP_HOST_DIR:-./kronn-backups}:/backups:rw - npm-cache:/home/kronn/.npm # Persistent npm cache for npx agent execution - uv-cache:/home/kronn/.cache/uv # Isolated uv cache (named volume — see TD-20260429-uv-cache-uid-mismatch: bind-mounting ~/.cache/uv left root-owned files on host pre-APP_UID, breaking host `uvx` for MCP servers in CC CLI / Codex / Gemini) - uv-tools:/home/kronn/.local/share/uv # Persistent uv tool installs (CloudWatch, Atlassian MCP servers) # SSH auth for git push (forward host SSH keys) - ${HOME}/.ssh:/home/kronn/.ssh:ro # SSH keys + known_hosts - ${SSH_AUTH_SOCK:-./.docker/empty}:/run/host-ssh-agent.sock # SSH agent socket (Linux) — empty-dir fallback (see note above; /dev/null crashes WSL2+Docker Desktop). No agent → git push falls back to ~/.ssh keys. - ${KRONN_MACOS_SSH_SOCK:-./.docker/empty}:/run/host-services/ssh-auth.sock # SSH agent socket (macOS Docker Desktop) # Agent auth directories (local subscriptions) # 0.7.1 — User-scoped agent context directory (cross-project, multi-CLI). # Markdown files here are auto-injected into the system prompt of every # agent Kronn spawns, regardless of CLI. Bridges the per-tool gap left # by ~/.claude/CLAUDE.md, ~/.codex/..., etc. Auto-bootstrapped with a # README on first read if the dir doesn't exist. - ${HOME}/.kronn/user-context:/home/kronn/.kronn/user-context:rw - ${HOME}/.claude.json:/home/kronn/.claude.json:ro # Claude Code main config - ${HOME}/.claude:/home/kronn/.claude:rw # Claude Code auth/config + session state (needs write) - claude-projects:/home/kronn/.claude/projects # Isolate Docker sessions from host (avoids ghost /host-home project dirs) - claude-sessions:/home/kronn/.claude/sessions # Isolate PID files (TD-20260507-claude-sessions-host-leak) - ${HOME}/.codex:/home/kronn/.codex:rw # Codex auth + state (needs write) - ${HOME}/.config/codex:/home/kronn/.config/codex:ro # Codex config (legacy path) - ${HOME}/.vibe:/home/kronn/.vibe:rw # Vibe auth + state (needs write) - ${HOME}/.kiro:/home/kronn/.kiro:rw # Kiro auth + state (AWS Builder ID) - ${HOME}/.copilot:/home/kronn/.copilot:rw # GitHub Copilot auth + config - ${HOME}/.gemini:/home/kronn/.gemini:rw # Gemini CLI auth + project registry + settings - ${HOME}/.local/share/kiro-cli:/home/kronn/.local/share/kiro-cli:rw # Kiro SQLite data # RTK (Rust Token Killer) — share host's config + stats so `rtk gain` # inside the container reads the same SQLite the user's CLI writes to. # Without these, /api/rtk/savings always reports zero even when the # user has thousands of compressions on the host. - ${HOME}/.config/rtk:/home/kronn/.config/rtk:rw - ${HOME}/.local/share/rtk:/home/kronn/.local/share/rtk:rw - ./templates:/app/templates:ro # AI context templates for project init # Self-mount Kronn's scripts dir at the SAME absolute path inside # the container as on the host. Required so `kronn-internal` MCP # entries written into project-level config files (`.mcp.json`, # `.kiro/settings/mcp.json`, …) carry a path string that resolves # for BOTH the Kronn-spawned-in-container agent AND the user's # host CLI sessions (`kiro-cli`, `claude`, …). Without this the # host CLIs hit `Broken pipe (os error 32)` on every spawn. # Falls back gracefully when the source dir isn't present — the # injector skips kronn-internal rather than write a broken path. - ./backend/scripts:${PWD}/backend/scripts:ro environment: - KRONN_TEMPLATES_DIR=/app/templates - KRONN_DATA_DIR=/data - KRONN_RUN_ARTIFACTS_DIR=/run-artifacts - KRONN_IN_DOCKER=1 - KRONN_HOST_HOME=${HOME} # Default zone of crons and date templates; a container reports UTC # without it. Settings > timezone overrides it. - TZ=${TZ:-} # Expose the security boundary to diagnostics. These values do not add # mounts; they mirror the primary + generated override mounts above. - KRONN_REPOS_DIR=${KRONN_REPOS_DIR:-${HOME}/Repositories} - KRONN_EXTRA_REPOS=${KRONN_EXTRA_REPOS:-} - KRONN_HOSTS_FILE=/host-etc/hosts # Path written into project-level `.mcp.json` / `.kiro/settings/…` # / `.gemini/settings.json` / `~/.codex/config.toml` for the # `kronn-internal` MCP entry. Must resolve identically inside the # container and on the host — the self-mount above makes that work. - KRONN_INTROSPECTION_PUBLIC_PATH=${PWD}/backend/scripts/disc-introspection-mcp.py - KRONN_HOST_BIN=/host-bin/local:/host-bin/global:/host-bin/npm:/host-bin/cargo:/host-bin/bun:/host-bin/extra # 0.8.2 — Propagate the user's rustup home so the `cargo` / `rustc` # shims can locate the active toolchain. The shims read this env # var BEFORE falling back to `$HOME/.rustup`, and the container's # `HOME` is `/home/kronn` (not the host user's home). Empty when # KRONN_RUSTUP_HOME isn't set in .env — cargo will then error out # cleanly at first invocation rather than silently fail. - RUSTUP_HOME=${KRONN_RUSTUP_HOME:-} - PATH=/home/kronn/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host-bin/local:/host-bin/global:/host-bin/npm:/host-bin/cargo:/host-bin/bun:/host-bin/extra:/host-bin/sysbin:/host-bin/syssbin - KRONN_HOST_OS=${KRONN_HOST_OS:-Linux} - KRONN_HOST_UID=${KRONN_HOST_UID:-1000} - KRONN_HOST_GID=${KRONN_HOST_GID:-1000} - KRONN_HOST_IPS=${KRONN_HOST_IPS:-} - SSH_AUTH_SOCK=/run/host-ssh-agent.sock - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - OPENAI_API_KEY=${OPENAI_API_KEY:-} - GEMINI_API_KEY=${GEMINI_API_KEY:-} - GOOGLE_API_KEY=${GOOGLE_API_KEY:-} - MISTRAL_API_KEY=${MISTRAL_API_KEY:-} - GITHUB_TOKEN=${GITHUB_TOKEN:-} - GH_TOKEN=${GH_TOKEN:-${GITHUB_TOKEN:-}} - OLLAMA_HOST=${OLLAMA_HOST:-http://host.docker.internal:11434} # 0.8.11 — LAN exposure is opt-in. The gateway/frontend host ports below # bind 127.0.0.1 by default; set KRONN_BIND=0.0.0.0 (e.g. for a WSL # backend reached from a Mac frontend) to publish on the LAN. When you do, # the backend REFUSES to start unauthenticated — set KRONN_AUTH_TOKEN, or # KRONN_ALLOW_INSECURE_LAN=1 to keep it open on a trusted home network. - KRONN_BIND=${KRONN_BIND:-127.0.0.1} - KRONN_ALLOW_INSECURE_LAN=${KRONN_ALLOW_INSECURE_LAN:-} - KRONN_AUTH_TOKEN=${KRONN_AUTH_TOKEN:-} # 0.8.11 (B6) — failure webhook (Slack/Teams/generic) + scheduled backups. # A backup dir OUTSIDE the data volume needs a host bind-mount (see the # commented volume below); set KRONN_BACKUP_DIR to /backups to use it. - KRONN_FAILURE_NOTIFY_URL=${KRONN_FAILURE_NOTIFY_URL:-} - KRONN_BACKUP_DIR=${KRONN_BACKUP_DIR:-} - KRONN_BACKUP_INTERVAL_HOURS=${KRONN_BACKUP_INTERVAL_HOURS:-24} - KRONN_BACKUP_KEEP=${KRONN_BACKUP_KEEP:-7} # RUST_LOG: if unset, the backend reads `config.server.debug_mode` to # pick a sensible default (info vs debug). Set `KRONN_RUST_LOG` in the # env (or via `./kronn start --debug` / `make start DEBUG=1`) to # override for one run without touching the persisted config. - RUST_LOG=${KRONN_RUST_LOG:-} # Resource limits — prevents runaway agents from consuming all host resources deploy: resources: limits: memory: 4G cpus: '2.0' healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3140/api/health"] interval: 10s timeout: 3s # 60 s start_period covers the worst-case cold start on a fresh # machine: migrations + sidecar Python venv + tool installs + # ts-rs codegen. Inside `start_period` Docker is patient — failures # don't count toward `retries`. After the window, 5×10 s gives # another 50 s margin. Pre-fix this was 5 s × 3 retries → ~35 s # total, just below the real cold-start time, causing # `dependency failed to start` on every fresh `make start` (TD # TD-20260507-cold-start-healthcheck-race). retries: 5 start_period: 60s frontend: restart: unless-stopped image: ghcr.io/docroms/kronn-frontend:${KRONN_IMAGE_TAG:-0.15.2} build: context: ./frontend dockerfile: Dockerfile ports: # 0.8.11 — loopback by default; KRONN_BIND=0.0.0.0 opts into LAN exposure. - "${KRONN_BIND:-127.0.0.1}:3141:80" deploy: resources: limits: memory: 512M cpus: '0.5' healthcheck: test: ["CMD", "curl", "-f", "http://localhost/"] interval: 10s timeout: 3s retries: 3 start_period: 5s depends_on: backend: condition: service_healthy # Single entry point: nginx reverse proxy gateway: restart: unless-stopped image: nginx:alpine deploy: resources: limits: memory: 128M cpus: '0.25' ports: # 0.8.11 — main entry point. Loopback by default (secure out-of-the-box); # set KRONN_BIND=0.0.0.0 to expose on the LAN (backend then requires auth). - "${KRONN_BIND:-127.0.0.1}:3140:80" volumes: - ./.docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: backend: condition: service_healthy frontend: condition: service_healthy volumes: kronn-data: kronn-run-artifacts: npm-cache: uv-cache: # Isolated from host ~/.cache/uv to avoid uid-mismatch pollution (see TD-20260429) uv-tools: claude-projects: # Isolates Docker Claude Code sessions from host ~/.claude/projects claude-sessions: # Isolates `.claude/sessions/` PID files from the host daemon — without # this, the container's Claude tries to talk to a host daemon at # 127.0.0.1: that doesn't exist there and hangs silently # (TD-20260507-claude-sessions-host-leak).