docker-compose.yml
Rama main · réplica de lectura del repositorio original.
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/<user>/.local/share/claude/versions/<v>/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:<port> that doesn't exist there and hangs silently
# (TD-20260507-claude-sessions-host-leak).