Files
ai-agent/docker-compose.standalone.yml
Gabriel Vidal 27b3caa0f9 feat(ai-agent): fold the runner into the container — standalone spawns sessions
GOAL.md's first gap: without a runner a standalone image is a read-only
viewer, because launching `claude -p` needed a host-side process.

The Claude Code CLI is a self-contained native binary, so the image can just
carry it — and the *same* sidecar/sidecar.py the homelab runs on its host runs
in-container against it. With RUNNER_IN_CONTAINER=1 (the standalone default)
docker-entrypoint.sh starts the runner on the loopback, mints a SIDECAR_TOKEN
if none was given, overrides SIDECAR_URL to point at it, and gives the CLI a
writable $HOME on the data volume. The transcripts it writes there become a
third live source (RUNNER_TRANSCRIPTS_DIR → SOURCE_DIRS), so an in-container
session streams into the viewer like any other.

Verified end to end: a standalone container (workspace + data, no homelab, no
host sidecar) spawns a session, the CLI runs it, and the turn renders in the
conversation list with its model tag. The CLI authenticates from
ANTHROPIC_API_KEY or a Claude home mounted at RUNNER_HOME.

The homelab is unchanged: it leaves the flag off and keeps its host sidecar,
which is what lets a run use the host's own hooks, skills and credentials.
standalone-smoke.sh now also asserts the CLI is on PATH and the runner is
healthy — the packaging property that would otherwise regress silently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 10:08:54 +02:00

76 lines
3.6 KiB
YAML

# Standalone ai-agent — the viewer against any repo, with no homelab around it.
#
# This is the generic counterpart of the homelab `docker-compose.yml`: same image,
# but nothing homelab-specific is required. Every mount below is either generic
# (a repo to look at, the machine's own Claude Code transcripts) or optional, and
# the app degrades to an empty catalog for whatever isn't there.
#
# AI_AGENT_WORKSPACE=/path/to/your/repo \
# docker compose -f docker-compose.standalone.yml up -d --build
#
# Then open http://localhost:8096. `scripts/standalone-smoke.sh` runs exactly this
# shape against a throwaway workspace and asserts the API comes up clean.
#
# Sessions are spawned by the **in-image runner** (RUNNER_IN_CONTAINER=1): the
# Claude Code CLI is baked into the image and driven by the same sidecar the
# homelab runs on its host — no host process, no systemd unit. It needs the CLI to
# be able to authenticate, which is either:
# • ANTHROPIC_API_KEY (below), or
# • an already-logged-in Claude home mounted at RUNNER_HOME (its OAuth
# credentials come with it) — e.g. `-v ~/.claude:/data/home/.claude`.
# With neither, everything still works except spawning.
#
# One caveat remains, tracked in GOAL.md: building needs the homelab's private npm
# registry (frontend/.npmrc pulls @gabvdl/ui from verdaccio on localhost:4873), so
# today a standalone *build* only works on the homelab — a prebuilt image runs
# anywhere. Publishing @gabvdl/ui to public npm is what closes this.
#
# On the homelab itself the default container name collides with the live one, so
# docker refuses the run — set AI_AGENT_NAME (and AI_AGENT_PORT) to try it here.
services:
ai-agent:
container_name: ${AI_AGENT_NAME:-ai-agent}
build:
context: .
# Only needed while @gabvdl/ui comes from the host-bound private registry.
network: host
image: ${AI_AGENT_IMAGE:-ai-agent:standalone}
restart: unless-stopped
# Match the owner of the workspace you mount, so edits keep their ownership.
user: "${AI_AGENT_UID:-1000}:${AI_AGENT_GID:-1000}"
environment:
TZ: ${TZ:-UTC}
WORKSPACE: /workspace
STATIC_DIR: /app/static
DB_PATH: /data/ai-agent.db
REPO_DIR: /workspace
PROJECTS_DIR: /workspace/projects
TEMPLATES_DIR: /workspace/projects/templates
TRANSCRIPTS_DIR: /transcripts
# Real token counts + dollar costs (Anthropic count_tokens), and the CLI's
# auth for the in-image runner. Optional: without it files carry no
# token/cost figures and spawning needs a logged-in Claude home instead.
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
# Spawn sessions from inside the container: the entrypoint runs the bundled
# runner (the Claude Code CLI + sidecar.py) on the loopback and points the
# backend at it. Set to 0 to fall back to a host sidecar (SIDECAR_URL).
RUNNER_IN_CONTAINER: ${RUNNER_IN_CONTAINER:-1}
# The CLI's HOME — config, credentials and the transcripts it writes. It
# lives on the data volume, so sessions and logins survive a restart.
RUNNER_HOME: /data/home
SIDECAR_MODEL: ${SIDECAR_MODEL:-opus}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
# The repo to look at: its CLAUDE.md, .claude/ tree and projects/.
- ${AI_AGENT_WORKSPACE:-./workspace}:/workspace
# This machine's Claude Code transcripts — the same path on any machine.
- ${AI_AGENT_TRANSCRIPTS:-${HOME}/.claude/projects}:/transcripts:ro
# SQLite store, transcript archive, conversation metadata, uploads.
- ai-agent-data:/data
ports:
- "127.0.0.1:${AI_AGENT_PORT:-8096}:8080"
volumes:
ai-agent-data: