| type | doc | |
|---|---|---|
| title | Docker Setup | |
| description | Covers Docker Compose setup for Koan (pull vs. build from source), workspace project mounts, authentication (Claude/GitHub), volume layout, and troubleshooting common container issues. | |
| tags |
|
|
| created | 2026-05-28 | |
| updated | 2026-07-18 |
Docker provides isolated environments and simplified deployment — ideal for VPS/server hosting or keeping Koan sandboxed on your machine. For running Koan directly (no container), see INSTALL.md.
Railway (hosted): for a click-and-play single-container deploy on Railway (or a similar PaaS) behind
KOAN_DEPLOY=railway, see Deploy on Railway.
- Docker Engine 20+ and Docker Compose v2+
- Claude authentication (one of):
- ANTHROPIC_API_KEY in
.env— for API billing accounts (console.anthropic.com) claude setup-token— for Claude subscription users (requires Claude CLI on host:npm install -g @anthropic-ai/claude-code)
- ANTHROPIC_API_KEY in
- GitHub CLI (
gh) authenticated on the host (for PR/issue operations) - A messaging platform configured (Telegram or Slack — see INSTALL.md)
Pull the published image from GitHub Container Registry — no local build needed.
# 1. Clone and enter the repo (needed for compose, setup-docker.sh, env template)
git clone https://github.com/Anantys-oss/koan.git
cd koan
# 2. Create your instance directory
cp -r instance.example instance
# 3. Set up credentials
cp env.example .env
# Edit .env — set messaging credentials (Telegram or Slack)
# For API billing: also set ANTHROPIC_API_KEY in .env
# 4. Add projects to the workspace
mkdir -p workspace
ln -s /path/to/your/project workspace/myproject
# 5. Run the setup script (auto-detects host paths, generates mounts)
./setup-docker.sh
# 6. Authenticate Claude CLI (pick one)
# Option A: API key — just set ANTHROPIC_API_KEY in .env (done in step 3)
# Option B: Subscription users (requires Claude CLI on host):
make docker-auth
# Generates an OAuth token from your host's Claude CLI and saves it to .env.
# If you haven't authenticated on the host yet: claude auth login
# 7. Inject GitHub token for container use
make docker-gh-auth
# Extracts your host's gh token and saves it to .env as GH_TOKEN.
# Required because macOS Keychain tokens aren't accessible inside Docker.
# 8. Pull the prebuilt image and start (detached)
make docker-pull-upGHCR access. If
make docker-pull-upfails withdenied/unauthorized, the package is private. Either ask an org owner to make it public (github.com/orgs/Anantys-oss → Packages → koan → Package settings → Change visibility → Public), or log in first with a Personal Access Token that has theread:packagesscope:echo "$GH_PAT" | docker login ghcr.io -u YOUR_GH_USERNAME --password-stdin
Image tags & pinning.
make docker-pull-updefaults toghcr.io/anantys-oss/koan:latest. Pin a release withKOAN_IMAGE:make docker-pull-up KOAN_IMAGE=ghcr.io/anantys-oss/koan:stable make docker-pull-up KOAN_IMAGE=ghcr.io/anantys-oss/koan:1.0 # major.minorAvailable tags:
latest,stable, per-version (X.Y/X.Y.Z), and a rollingdeveltag (see below).
Rolling
:develimage. A fresh image can be published on demand — without cutting a release — via the 🐳 Publish Container workflow (repo Actions → "Run workflow", tag defaults todevel). It is built by the same job that produces release images, but pushes only the requested tag::develis a moving target (overwritten on each manual publish) and never updates:latestor:stable, which move only on a real release.make docker-pull-up KOAN_IMAGE=ghcr.io/anantys-oss/koan:devel
UID/GID on the prebuilt image. The published image runs as UID/GID 1000. On Linux hosts whose user isn't 1000, bind-mounted
instance/andlogs/can hit permission errors. If so, either build from source (below —setup-docker.shmatches your host UID/GID) or runchown -R 1000:1000 instance logs. macOS (Docker Desktop) maps UIDs automatically, so this rarely bites there.
Verify it's running:
# Watch logs
docker compose logs -f
# Send a test message (requires messaging to be configured)
make say m="hello"Build the image locally instead of pulling — useful for contributors, offline use, or to match a non-1000 host UID/GID exactly. Follow steps 1–7 above, then:
# 8. Build and start (runs setup-docker.sh first, then starts detached)
make docker-up
# Or start in the foreground to watch logs directly:
# docker compose up --buildKoan accesses your project repos through the workspace/ directory. Each
subdirectory (or symlink) becomes a project the agent can work on.
mkdir -p workspace
# Symlink existing repos (recommended — no duplication)
ln -s ~/code/myapp workspace/myapp
ln -s ~/code/backend workspace/backend
# Or copy/clone directly
git clone https://github.com/you/myapp.git workspace/myappWhen you run ./setup-docker.sh, it:
- Resolves symlinks — Docker can't follow host symlinks. The script
resolves each
workspace/<name>entry to its real path and emits a per-project bind mount:<real_path> → /app/workspace/<name>. Each project gets its own mount indocker-compose.override.yml. - Generates
projects.docker.yaml— maps each workspace entry to its container path (e.g./app/workspace/<name>). This file is mounted as/app/projects.docker.yamland copied to/app/projects.yamlat container startup so atomic writes work correctly. The script uses smart merge: if the file already exists, it only adds missing project entries — it never overwrites custom settings you've added manually. Safe to re-run at any time. - Generates
docker-compose.override.yml— adds the per-project workspace mounts, auth directory mounts, and host UID/GID.
To add a new project later, symlink it into workspace/ and re-run
./setup-docker.sh (or make docker-setup). Existing project entries in
projects.docker.yaml are preserved.
The generated file works out of the box, but you can edit it for per-project
overrides (auto-merge rules, CLI provider, etc.) — same schema as
projects.yaml:
defaults:
git_auto_merge:
enabled: false
base_branch: "main"
strategy: "squash"
projects:
myapp:
path: /app/workspace/myapp
backend:
path: /app/workspace/backend
git_auto_merge:
base_branch: "staging"The Docker image packages everything Koan needs: Python, Node.js, Claude CLI
(installed via npm), GitHub CLI (gh), git, and developer tools (ripgrep, fd,
bat, ruff, jq, etc.). No host binaries are mounted.
Two processes run inside a single container, supervised by the entrypoint script:
| Process | Purpose |
|---|---|
awake.py |
Telegram/Slack bridge — polls for messages, flushes outbox |
run.py |
Agent loop — picks missions, executes via Claude CLI |
If either process crashes, the entrypoint restarts it automatically.
The published image contains koan's runtime and the docs/ it needs, but none
of koan's own contributor tooling — CLAUDE.md, AGENTS.md, KOAN.md, and
.claude/ (project skills, agents, settings) are kept out of the build context
via .dockerignore. These files guide humans and agents developing koan itself;
if they shipped, a CLI session whose cwd fell inside the image's /app checkout
could auto-load them (the leak class addressed at runtime by isolating session
cwd). Excluding them removes the material from the artifact entirely.
This is enforced at build time: docker-publish.yml (the shared build/push
workflow behind both the release and manual-publish flows) runs
scripts/verify_image_clean.sh against the freshly built image and fails the
build if any of that tooling is found under /app. See issue #2383.
The Docker image ships with developer-friendly tools pre-installed. These improve the agent's productivity when working inside the container:
| Package | Binary | Purpose |
|---|---|---|
ripgrep |
rg |
Fast recursive grep — used by Claude Code and rtk |
fd-find |
fd |
Fast file finder (alternative to find) |
bat |
bat |
Syntax-highlighted file viewer (alternative to cat) |
jq |
jq |
JSON processor for API responses and config files |
ruff |
ruff |
Fast Python linter/formatter (installed via pip) |
less |
less |
Pager for browsing large output |
tree |
tree |
Directory structure visualization |
patch |
patch |
Apply unified diffs |
file |
file |
File type identification |
Note: Debian names
batasbatcatandfd-findasfdfind. The Dockerfile creates symlinks so both the Debian and standard names work.
If you need additional packages, add them to the apt-get install line in the
Dockerfile and rebuild (docker compose up --build).
- Claude CLI supports two auth methods:
- API key: Set
ANTHROPIC_API_KEYin.env(for API billing accounts). - OAuth token (subscription): Run
make docker-auth. This callsclaude setup-tokenon the host to generate a token, then saves it to.envasCLAUDE_CODE_OAUTH_TOKEN. Requires Claude CLI installed and authenticated on the host (npm install -g @anthropic-ai/claude-code && claude auth login).
- API key: Set
- GitHub CLI supports two auth methods:
- GH_TOKEN (recommended for Docker): Run
make docker-gh-authto extract the token from the host'sghCLI and save it to.env. Required on macOS where tokens are in the Keychain. - Mounted config:
~/.config/ghis mounted read-only from the host (works when tokens are stored as plain text, not in the system Keychain).
- GH_TOKEN (recommended for Docker): Run
- Git uses a default identity (
Koan <koan@noreply.github.com>), overridable by mounting~/.gitconfig.
| Host | Container | Purpose |
|---|---|---|
./instance/ |
/app/instance/ |
Runtime state (missions, memory, journal) |
instance/missions.docker.md |
/app/instance/missions.md |
Isolated mission queue (see below) |
| (per-project — see below) | /app/workspace/<name> |
Project repositories (one mount per project) |
./logs/ |
/app/logs/ |
Log files |
./claude-auth/ |
/home/koan/.claude/ |
Claude CLI auth state (interactive login) |
projects.docker.yaml |
/app/projects.docker.yaml |
Project config template (copied to projects.yaml on startup) |
~/.config/gh |
/home/koan/.config/gh |
GitHub CLI auth (read-only) |
Persistent project config on managed deployments. Koan resolves
projects.yamlwith priority:instance/projects.yamlfirst, then the repo-rootprojects.yaml. On hosted/containerized deployments (e.g. Railway) only theinstance/volume persists across re-deploys — placing your config atinstance/projects.yamlkeeps it from being wiped when the ephemeral repo root is reset. Startup logs print the resolved path and its origin.
Workspace mounts are per-project and dynamic. The base
docker-compose.ymlcontains no workspace mounts.setup-docker.shresolves eachworkspace/<name>symlink to its real host path and writes individual bind mounts intodocker-compose.override.yml(e.g./real/path/to/myapp:/app/workspace/myapp). Re-run./setup-docker.shafter adding or removing projects.
The container mounts instance/missions.docker.md over
instance/missions.md. This means Docker and a native (non-Docker) Koan
instance each have their own mission queue — they won't interfere if you run
both.
The entrypoint writes a heartbeat timestamp to .koan-heartbeat every 5
seconds. Docker's HEALTHCHECK verifies it's less than 120 seconds old:
docker inspect --format='{{.State.Health.Status}}' koanTo avoid permission issues on bind-mounted volumes, setup-docker.sh detects
your host UID/GID and passes them as build args. The container user runs with
the same IDs as your host user.
| File | Purpose |
|---|---|
.env |
Credentials: ANTHROPIC_API_KEY, messaging tokens, GITHUB_USER |
projects.docker.yaml |
Project paths and per-project overrides (auto-generated) |
instance/config.yaml |
Agent settings (same as native setup) |
instance/soul.md |
Agent personality |
docker-compose.override.yml |
Volume mounts and build args (auto-generated) |
The two auto-generated files (docker-compose.override.yml and
projects.docker.yaml) are gitignored. Re-run ./setup-docker.sh to
regenerate them after changing your workspace layout.
make docker-setup # Run setup-docker.sh
make docker-pull-up # Pull the prebuilt GHCR image and start (recommended)
make docker-up # Build from source and start (detached)
make docker-down # Stop the container
make docker-logs # Tail container logs
make docker-test # Run the test suite inside the container
make docker-auth # Extract Claude OAuth token from host CLI → .env
make docker-gh-auth # Extract GitHub token from host gh CLI → .env
# Pin a specific published version (defaults to :latest):
make docker-pull-up KOAN_IMAGE=ghcr.io/anantys-oss/koan:stable# Pull the prebuilt image and start (what `make docker-pull-up` runs)
KOAN_IMAGE=ghcr.io/anantys-oss/koan:latest docker compose pull
KOAN_IMAGE=ghcr.io/anantys-oss/koan:latest docker compose up -d --no-build
# Build from source and start in foreground (see logs directly)
docker compose up --build
# Generate OAuth token from host CLI (one-time, for subscription users)
make docker-auth
# Interactive shell inside the container
docker compose run --rm koan shell
# Run tests
docker compose run --rm koan test
# Run only the agent loop (no bridge)
docker compose run --rm koan agent
# Run only the bridge (no agent)
docker compose run --rm koan bridgePreview what setup-docker.sh would generate without writing files:
./setup-docker.sh --dry-runThe container needs one of:
- API key: Set
ANTHROPIC_API_KEYin.env(console.anthropic.com) - OAuth token: Run
make docker-authon the HOST (not inside Docker)
UID/GID mismatch between host and container. Re-run ./setup-docker.sh to
regenerate with the correct IDs, then rebuild:
./setup-docker.sh
docker compose up --buildOn macOS, gh stores tokens in the system Keychain, which isn't accessible
inside Docker. The fix is to inject the token as an environment variable:
# Extract token from host and save to .env as GH_TOKEN
make docker-gh-auth
# Restart the container to pick up the new token
docker compose up --build -dThe gh CLI natively uses GH_TOKEN when set — no config file needed.
If you're on Linux (where tokens are stored as plain text in ~/.config/gh),
the mounted config directory may work directly. Re-run ./setup-docker.sh to
ensure the mount is set up.
If Kōan starts but doesn't see any projects, the projects.docker.yaml file
may be missing a projects: section or workspace entries. Re-run
./setup-docker.sh — it automatically adds the projects: section if
absent, and appends any workspace entries that are not yet listed:
./setup-docker.sh
docker compose up --build -dExisting entries and custom settings in projects.docker.yaml are never
overwritten.
Check the logs for the root cause:
docker compose logs --tail=50Common causes: missing API key, invalid messaging credentials, no projects configured.
make docker-auth runs claude setup-token interactively on your host and captures the
resulting 1-year OAuth token from its output. For this to work:
- Claude CLI must be installed on the host:
npm install -g @anthropic-ai/claude-code - Claude CLI must be authenticated on the host:
claude auth login - The
setup-tokencommand requires~/.claude.jsonwith{"hasCompletedOnboarding": true}. If you've runclaudeat least once, this file should already exist.
If the token extraction fails, you can manually copy the token from the setup-token
output and add it to .env:
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-...
Mission queues are isolated by default (missions.docker.md vs
missions.md). However, instance/ state (memory, journal, config) is
shared. If you run both simultaneously, avoid editing instance/config.yaml
from both sides.
Set KOAN_INSTANCE_REPO to your PRIVATE instance repository and, on cold
boot with an empty volume, the entrypoint clones it (including .git) into
instance/ so Kōan boots as your instance: soul, projects, skills, memory,
journal, and any in-flight mission state. Unset → seed from the bundled
instance.example/ template.
| Variable | Purpose |
|---|---|
KOAN_INSTANCE_REPO |
gh slug (owner/koan-instance) or full git URL of your private instance repo. |
KOAN_INSTANCE_REPO_BRANCH |
Optional branch to clone. Default: the repo's default branch. |
KOAN_INSTANCE_SYNC_INTERVAL |
Optional seconds between background git pull --rebase --autostash of instance/. 0 (default) disables it. |
Full-mirror model. Hydration restores the whole tree, not just config,
because the running agent already commits and pushes the entire instance/
directory via mission_runner.commit_instance() on every lifecycle beat. The
clone is the only new piece; the agent keeps the remote in sync from then on,
and a later redeploy re-clones the up-to-date remote and resumes.
Caveats. The gh clone path uses GH_TOKEN directly; the git clone
fallback needs a credential helper (gh auth setup-git runs in the entrypoint).
Any clone error fails open to the template — boot never hard-fails. Don't edit
the remote by hand while the agent runs (the next commit_instance push would
be non-fast-forward and diverge); if you must, set KOAN_INSTANCE_SYNC_INTERVAL
so a throttled pull --rebase --autostash reconciles first. Secrets stay out of
the repo — .env lives at $KOAN_ROOT/.env, outside instance/.