8.3 KiB
8.3 KiB
AGENTS.md
Purpose
Docker scaffold for running Claude Code in an isolated container (see README.md).
Structure
.env— gitignored, per-user, written byclaudaris config. HoldsNAME(image/container name — the defaultclaudarisis 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, andWORKSPACE_DIR.claudarissources it if present;docker-compose.ymlreads the same vars, anddocker composepicks up.envon its own since it lives in the project directory compose runs from — soclaudarisand plaindocker composecommands agree without any extra wiring.docker-compose.yml— the actual container definition (build context, image/container/hostname name, volumes);claudarisshells out todocker composerather than rawdocker build/run/exec. The one mount it can't express is the opt-in SSH mount (seeclaudaris startbelow), which needs a host path to exist first — that's added via a gitignoreddocker-compose.override.ymlthatclaudaris startwrites 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_aliasesis baked in at/opt/dotfiles/bash_aliasesandfiles/bashrcbecomes 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 (seeclaudaris startbelow). 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 byclaudaris start, which bind-mounts two individual host files onto~/.claude/.credentials.json(the OAuth token) and~/.claude.json(account/onboarding state) — seeclaudaris startbelow 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 officialgitea-mcp(latest release binary,/usr/local/bin/gitea-mcp), andinvokeai-mcp-server(PyPI viauv tool install; upstream hardcodes the InvokeAI URL, so the Dockerfile sed-patches it to honor$INVOKEAI_BASE_URL, with agrepthat fails the build if upstream changes and the patch stops landing). They're only installed by the Dockerfile — registration happens infiles/entrypoint.shon 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 withclaude mcp removeis 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 — commentedexporttemplates live infiles/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/projectsrepos bind-mounted from the host (which the container, always running as root, would otherwise treat as untrusted).files/entrypoint.shstarts the tmux server and creates amainsession, then polls and exits once the session ends (tmux's defaults apply:remain-on-exitoff,exit-emptyon, so a shell exiting — Ctrl+D,exit, crash — tears the session down) rather than running forever; there's no--restartpolicy, so a stopped container needsclaudaris connectorclaudaris startto bring it back up (both do this automatically)./etc/profile.d/nix_path.shpointsPATHat/root/.local/bin.files/tmuxis copied in as a/usr/local/bin/tmuxwrapper that shadows the realtmuxbinary so that plaindocker exec -it "$NAME" tmuxrunstmux new-session -A -s main(attaches tomainif it exists, creates it otherwise) instead of always starting a new session.claudaris— single entry point with subcommands:config(aliasconfigure) — interactive wizard prompting forNAME,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.envabove for$NAME).start—docker compose up -d, so the shell prompt readsroot@$NAME, e.g.root@claudaris, instead of a random container ID (hostname:indocker-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 indocker-compose.yml— seeded once bystart(empty, for the auth files; fromfiles/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.jsonon 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, thenrename()over the target), andrename()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_DIRis mounted at/projects. Also offers an opt-in/root/.sshmount (read-only) for reaching other nodes: create$DATA_DIR/sshon the host and populate it before starting the container to enable it —startwrites or removes a gitignoreddocker-compose.override.ymlfor this mount, since compose can't make a volume conditional on a host path existing the way the script itself can.DATA_DIRdefaults to/data/$NAME;WORKSPACE_DIRdefaults to/home/$USER/projects(override viaclaudaris config).connect—docker compose up -d(a no-op if already running) thendocker compose exec claudaris bash, so it also works right after the container has auto-exited (seeDockerfile/entrypoint above).remove(aliasesstop,rm) —docker compose down, so a subsequentstartrecreates the container fresh.help— usage.
files/— plain (non-dot) source filesCOPY'd into the image at build time:bashrc/bash_aliases(dotfiles — seeDockerfileabove),entrypoint.sh, and thetmuxwrapper. Kept dotless so they're easy to see/edit directly in a normal directory listing; theDockerfileadds the leading dot back on forbashrc's destination. Bothbashrcandbash_aliasesare also seeded onto the host byclaudaris startso 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
# 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.