139 lines
8.3 KiB
Markdown
139 lines
8.3 KiB
Markdown
# AGENTS.md
|
|
|
|
## Purpose
|
|
Docker scaffold for running Claude Code in an isolated container (see README.md).
|
|
|
|
## Structure
|
|
- `.env` — gitignored, per-user, written by `claudaris config`. Holds `NAME`
|
|
(image/container name — the default `claudaris` is meant to be overridden
|
|
per person, e.g. `chris-claude`, so multiple users can each run their own
|
|
container off this same repo), `DATA_DIR`, and `WORKSPACE_DIR`. `claudaris`
|
|
sources it if present; `docker-compose.yml` reads the same vars, and
|
|
`docker compose` picks up `.env` on its own since it lives in the project
|
|
directory compose runs from — so `claudaris` and plain `docker compose`
|
|
commands agree without any extra wiring.
|
|
- `docker-compose.yml` — the actual container definition (build context,
|
|
image/container/hostname name, volumes); `claudaris` shells out to
|
|
`docker compose` rather than raw `docker build`/`run`/`exec`. The one
|
|
mount it can't express is the opt-in SSH mount (see `claudaris start`
|
|
below), which needs a host path to exist first — that's added via a
|
|
gitignored `docker-compose.override.yml` that `claudaris start` writes or
|
|
removes as needed; compose merges it automatically when present.
|
|
- `Dockerfile` — archlinux base image with bash, git, nodejs/npm, tmux, vim,
|
|
fastfetch, etc. `files/bash_aliases` is baked in at `/opt/dotfiles/bash_aliases`
|
|
and `files/bashrc` becomes the image's default `/root/.bashrc` — both are
|
|
rebuild-time defaults, but the host shadows both with its own single-file
|
|
bind mounts at runtime (see `claudaris start` below). Claude
|
|
Code is installed at build time straight into `/root` (`$HOME`), so a
|
|
rebuild always picks up the latest release. Staying logged in across
|
|
rebuilds is handled entirely by `claudaris start`, which bind-mounts two
|
|
individual host files onto `~/.claude/.credentials.json` (the OAuth token)
|
|
and `~/.claude.json` (account/onboarding state) — see `claudaris start`
|
|
below for why these are per-file mounts rather than a directory volume.
|
|
Three MCP servers for Claude Code are also installed at build time:
|
|
`n8n-mcp` (npm, `/usr/local/bin/n8n-mcp`), the official `gitea-mcp`
|
|
(latest release binary, `/usr/local/bin/gitea-mcp`), and
|
|
`invokeai-mcp-server` (PyPI via `uv tool install`; upstream hardcodes the
|
|
InvokeAI URL, so the Dockerfile sed-patches it to honor
|
|
`$INVOKEAI_BASE_URL`, with a `grep` that fails the build if upstream
|
|
changes and the patch stops landing). They're only *installed* by the
|
|
Dockerfile — *registration* happens in `files/entrypoint.sh` on every
|
|
container start (`claude mcp add --scope user`, skipped per-server if
|
|
already present), because user-scope MCP config lives in `~/.claude.json`,
|
|
which is bind-mounted from the host and would shadow anything registered
|
|
at build time. Consequence of the "add if missing" guard: hand edits to a
|
|
server's config survive restarts, but a server removed with
|
|
`claude mcp remove` is re-added on the next container start. The servers
|
|
read their instance URLs/tokens (`GITEA_HOST`, `GITEA_ACCESS_TOKEN`,
|
|
`N8N_API_URL`, `N8N_API_KEY`, `INVOKEAI_BASE_URL`) from the environment
|
|
Claude Code runs in — commented `export` templates live in
|
|
`files/bash_aliases`, which is host-persisted, so tokens stay out of the
|
|
image and the repo.
|
|
`git config --system --add safe.directory '*'` is set so git works on
|
|
`/projects` repos bind-mounted from the host (which the container, always
|
|
running as root, would otherwise treat as untrusted). `files/entrypoint.sh`
|
|
starts the tmux server and creates a `main` session, then polls and exits
|
|
once the session ends (tmux's defaults apply: `remain-on-exit` off,
|
|
`exit-empty` on, so a shell exiting — Ctrl+D, `exit`, crash — tears the
|
|
session down) rather than running forever; there's no `--restart` policy,
|
|
so a stopped container needs `claudaris connect` or `claudaris start` to
|
|
bring it back up (both do this automatically). `/etc/profile.d/nix_path.sh`
|
|
points `PATH` at `/root/.local/bin`. `files/tmux` is copied in as a
|
|
`/usr/local/bin/tmux` wrapper that shadows the real `tmux` binary so that
|
|
plain `docker exec -it "$NAME" tmux` runs `tmux new-session -A -s main`
|
|
(attaches to `main` if it exists, creates it otherwise) instead of always
|
|
starting a new session.
|
|
- `claudaris` — single entry point with subcommands:
|
|
- `config` (alias `configure`) — interactive wizard prompting for `NAME`,
|
|
`DATA_DIR`, `WORKSPACE_DIR` (showing current/default values, enter to
|
|
keep) and writing them to `.env`. Re-run any time to update it.
|
|
- `build` — `docker compose build --no-cache --pull` (see `.env` above for
|
|
`$NAME`).
|
|
- `start` — `docker compose up -d`, so the shell prompt reads
|
|
`root@$NAME`, e.g. `root@claudaris`, instead of a random container ID
|
|
(`hostname:` in `docker-compose.yml`). `/root/.bashrc`,
|
|
`/opt/dotfiles/bash_aliases`, and the auth files
|
|
(`~/.claude/.credentials.json`, `~/.claude.json`,
|
|
`~/.local/share/opencode/auth.json`) are each bind-mounted individually
|
|
in `docker-compose.yml` — seeded once by `start` (empty, for the auth
|
|
files; from `files/bashrc`/`files/bash_aliases`, for the dotfiles) into
|
|
`$DATA_DIR/home/{.bashrc,bash_aliases}`,
|
|
`$DATA_DIR/claude/{credentials,claude}.json`, and
|
|
`$DATA_DIR/opencode/auth.json` on the host, so they can be edited/persist
|
|
without a rebuild — including a coworker dropping in their own aliases.
|
|
Individual-file mounts matter for the auth files specifically: Claude
|
|
Code and OpenCode likely save them atomically (write a temp file, then
|
|
`rename()` over the target), and `rename()` onto a symlink replaces the
|
|
symlink instead of writing through it — silently breaking persistence
|
|
after the first write. A bind mount doesn't have that failure mode,
|
|
which is why these aren't just symlinked from a directory volume the way
|
|
an earlier version of this setup did it. `$WORKSPACE_DIR` is mounted at
|
|
`/projects`. Also offers an opt-in `/root/.ssh` mount (read-only) for
|
|
reaching other nodes: create `$DATA_DIR/ssh` on the host and populate it
|
|
before starting the container to enable it — `start` writes or removes a
|
|
gitignored `docker-compose.override.yml` for this mount, since compose
|
|
can't make a volume conditional on a host path existing the way the
|
|
script itself can. `DATA_DIR` defaults to `/data/$NAME`; `WORKSPACE_DIR`
|
|
defaults to `/home/$USER/projects` (override via `claudaris config`).
|
|
- `connect` — `docker compose up -d` (a no-op if already running) then
|
|
`docker compose exec claudaris bash`, so it also works right after the
|
|
container has auto-exited (see `Dockerfile`/entrypoint above).
|
|
- `remove` (aliases `stop`, `rm`) — `docker compose down`, so a subsequent
|
|
`start` recreates the container fresh.
|
|
- `help` — usage.
|
|
- `files/` — plain (non-dot) source files `COPY`'d into the image at build
|
|
time: `bashrc`/`bash_aliases` (dotfiles — see `Dockerfile` above),
|
|
`entrypoint.sh`, and the `tmux` wrapper. Kept dotless so they're easy to
|
|
see/edit directly in a normal directory listing; the `Dockerfile` adds the
|
|
leading dot back on for `bashrc`'s destination. Both `bashrc` and
|
|
`bash_aliases` are also seeded onto the host by `claudaris start` so their
|
|
bind-mounted copies can diverge without a rebuild.
|
|
- `README.md` — usage instructions.
|
|
- `docs/` — user documentation in markdown: `README.md` (index),
|
|
`getting-started.md` (setup and daily use), `claudaris.md` (the executable,
|
|
subcommand by subcommand, including the mount table), `mcp.md` (the baked-in
|
|
MCP servers and their env vars). Keep these in sync when changing the
|
|
script, Dockerfile, or entrypoint.
|
|
|
|
## Common commands
|
|
```bash
|
|
# build the image
|
|
docker compose build --no-cache --pull
|
|
|
|
# start the container
|
|
docker compose up -d
|
|
|
|
# attach to the running container
|
|
docker compose exec claudaris bash
|
|
```
|
|
`docker compose` reads `NAME`/`DATA_DIR`/`WORKSPACE_DIR` from `.env`
|
|
automatically (no `claudaris` involvement needed), but the host-side seeding
|
|
`claudaris start` does (dotfiles, auth files, the SSH override) isn't
|
|
replicated by compose itself — run `claudaris start` at least once per host
|
|
before using raw compose commands.
|
|
|
|
## Notes
|
|
No test suite or CI — this is infra/config, not application code. Verify changes
|
|
by rebuilding the image (`./claudaris build`) and exec'ing in
|
|
(`./claudaris connect`) to confirm the container behaves as expected.
|