Compare commits
6 Commits
06fcf6bab7
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ac1d4470bd | |||
| 48adfb9c8e | |||
| ee48d50abf | |||
| 06931ace00 | |||
| c3432b2df4 | |||
| bcd7e012ba |
@@ -1,5 +1,9 @@
|
|||||||
# Per-user container name/settings — run `./claudaris config` to generate.
|
# Per-user container name/settings — run `./claudaris config` to generate.
|
||||||
/.env
|
/.env
|
||||||
|
|
||||||
|
# Generated by `claudaris start` when $DATA_DIR/ssh exists, to opt the
|
||||||
|
# compose service into the read-only SSH mount. Absent otherwise.
|
||||||
|
/docker-compose.override.yml
|
||||||
|
|
||||||
# Claude Code's auto-generated per-user permission cache.
|
# Claude Code's auto-generated per-user permission cache.
|
||||||
/.claude/settings.local.json
|
/.claude/settings.local.json
|
||||||
|
|||||||
@@ -8,7 +8,17 @@ Docker scaffold for running Claude Code in an isolated container (see README.md)
|
|||||||
(image/container name — the default `claudaris` is meant to be overridden
|
(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
|
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`
|
container off this same repo), `DATA_DIR`, and `WORKSPACE_DIR`. `claudaris`
|
||||||
sources it if present.
|
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,
|
- `Dockerfile` — archlinux base image with bash, git, nodejs/npm, tmux, vim,
|
||||||
fastfetch, etc. `files/bash_aliases` is baked in at `/opt/dotfiles/bash_aliases`
|
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
|
and `files/bashrc` becomes the image's default `/root/.bashrc` — both are
|
||||||
@@ -20,6 +30,25 @@ Docker scaffold for running Claude Code in an isolated container (see README.md)
|
|||||||
individual host files onto `~/.claude/.credentials.json` (the OAuth token)
|
individual host files onto `~/.claude/.credentials.json` (the OAuth token)
|
||||||
and `~/.claude.json` (account/onboarding state) — see `claudaris start`
|
and `~/.claude.json` (account/onboarding state) — see `claudaris start`
|
||||||
below for why these are per-file mounts rather than a directory volume.
|
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
|
`git config --system --add safe.directory '*'` is set so git works on
|
||||||
`/projects` repos bind-mounted from the host (which the container, always
|
`/projects` repos bind-mounted from the host (which the container, always
|
||||||
running as root, would otherwise treat as untrusted). `files/entrypoint.sh`
|
running as root, would otherwise treat as untrusted). `files/entrypoint.sh`
|
||||||
@@ -38,33 +67,39 @@ Docker scaffold for running Claude Code in an isolated container (see README.md)
|
|||||||
- `config` (alias `configure`) — interactive wizard prompting for `NAME`,
|
- `config` (alias `configure`) — interactive wizard prompting for `NAME`,
|
||||||
`DATA_DIR`, `WORKSPACE_DIR` (showing current/default values, enter to
|
`DATA_DIR`, `WORKSPACE_DIR` (showing current/default values, enter to
|
||||||
keep) and writing them to `.env`. Re-run any time to update it.
|
keep) and writing them to `.env`. Re-run any time to update it.
|
||||||
- `build` — builds the image, tagged `$NAME` (see `.env` above).
|
- `build` — `docker compose build --no-cache --pull` (see `.env` above for
|
||||||
- `start` — runs the container as `$NAME` with `--hostname "$NAME"` (so the
|
`$NAME`).
|
||||||
shell prompt reads `root@$NAME`, e.g. `root@claudaris`, instead of a
|
- `start` — `docker compose up -d`, so the shell prompt reads
|
||||||
random container ID). `/root/.bashrc`, `/opt/dotfiles/bash_aliases`, and
|
`root@$NAME`, e.g. `root@claudaris`, instead of a random container ID
|
||||||
the two auth files (`~/.claude/.credentials.json`, `~/.claude.json`) are
|
(`hostname:` in `docker-compose.yml`). `/root/.bashrc`,
|
||||||
each bind-mounted individually — seeded once (empty, for the auth files;
|
`/opt/dotfiles/bash_aliases`, and the auth files
|
||||||
from `files/bashrc`/`files/bash_aliases`, for the dotfiles) into
|
(`~/.claude/.credentials.json`, `~/.claude.json`,
|
||||||
`$DATA_DIR/home/{.bashrc,bash_aliases}` and
|
`~/.local/share/opencode/auth.json`) are each bind-mounted individually
|
||||||
`$DATA_DIR/claude/{credentials,claude}.json` on the host, so they can be
|
in `docker-compose.yml` — seeded once by `start` (empty, for the auth
|
||||||
edited/persist without a rebuild — including a coworker dropping in their
|
files; from `files/bashrc`/`files/bash_aliases`, for the dotfiles) into
|
||||||
own aliases. Individual-file mounts matter for the auth files
|
`$DATA_DIR/home/{.bashrc,bash_aliases}`,
|
||||||
specifically: Claude Code likely saves them atomically (write a temp
|
`$DATA_DIR/claude/{credentials,claude}.json`, and
|
||||||
file, then `rename()` over the target), and `rename()` onto a symlink
|
`$DATA_DIR/opencode/auth.json` on the host, so they can be edited/persist
|
||||||
replaces the symlink instead of writing through it — silently breaking
|
without a rebuild — including a coworker dropping in their own aliases.
|
||||||
persistence after the first write. A bind mount doesn't have that
|
Individual-file mounts matter for the auth files specifically: Claude
|
||||||
failure mode, which is why these aren't just symlinked from a directory
|
Code and OpenCode likely save them atomically (write a temp file, then
|
||||||
volume the way an earlier version of this setup did it. `$WORKSPACE_DIR`
|
`rename()` over the target), and `rename()` onto a symlink replaces the
|
||||||
is mounted at `/projects`. Also offers an opt-in `/root/.ssh` mount
|
symlink instead of writing through it — silently breaking persistence
|
||||||
(read-only) for reaching other nodes: create `$DATA_DIR/ssh` on the host
|
after the first write. A bind mount doesn't have that failure mode,
|
||||||
and populate it before starting the container to enable it. `DATA_DIR`
|
which is why these aren't just symlinked from a directory volume the way
|
||||||
defaults to `/data/$NAME`; `WORKSPACE_DIR` defaults to
|
an earlier version of this setup did it. `$WORKSPACE_DIR` is mounted at
|
||||||
`/home/$USER/projects` (override via `claudaris config`).
|
`/projects`. Also offers an opt-in `/root/.ssh` mount (read-only) for
|
||||||
- `connect` — `docker start`s `$NAME` (a no-op if it's already running)
|
reaching other nodes: create `$DATA_DIR/ssh` on the host and populate it
|
||||||
then attaches to its tmux session, so it also works right after the
|
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).
|
container has auto-exited (see `Dockerfile`/entrypoint above).
|
||||||
- `remove` (aliases `stop`, `rm`) — stops and removes the container so
|
- `remove` (aliases `stop`, `rm`) — `docker compose down`, so a subsequent
|
||||||
a subsequent `start` recreates it fresh.
|
`start` recreates the container fresh.
|
||||||
- `help` — usage.
|
- `help` — usage.
|
||||||
- `files/` — plain (non-dot) source files `COPY`'d into the image at build
|
- `files/` — plain (non-dot) source files `COPY`'d into the image at build
|
||||||
time: `bashrc`/`bash_aliases` (dotfiles — see `Dockerfile` above),
|
time: `bashrc`/`bash_aliases` (dotfiles — see `Dockerfile` above),
|
||||||
@@ -74,26 +109,28 @@ Docker scaffold for running Claude Code in an isolated container (see README.md)
|
|||||||
`bash_aliases` are also seeded onto the host by `claudaris start` so their
|
`bash_aliases` are also seeded onto the host by `claudaris start` so their
|
||||||
bind-mounted copies can diverge without a rebuild.
|
bind-mounted copies can diverge without a rebuild.
|
||||||
- `README.md` — usage instructions.
|
- `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
|
## Common commands
|
||||||
```bash
|
```bash
|
||||||
# build the image
|
# build the image
|
||||||
docker build -t "$NAME" .
|
docker compose build --no-cache --pull
|
||||||
|
|
||||||
# start the container
|
# start the container
|
||||||
docker run -d \
|
docker compose up -d
|
||||||
--name "$NAME" \
|
|
||||||
--hostname "$NAME" \
|
|
||||||
-v "$DATA_DIR/home/.bashrc:/root/.bashrc" \
|
|
||||||
-v "$DATA_DIR/home/bash_aliases:/opt/dotfiles/bash_aliases" \
|
|
||||||
-v "$DATA_DIR/claude/credentials.json:/root/.claude/.credentials.json" \
|
|
||||||
-v "$DATA_DIR/claude/claude.json:/root/.claude.json" \
|
|
||||||
-v "$WORKSPACE_DIR:/projects" \
|
|
||||||
"$NAME"
|
|
||||||
|
|
||||||
# attach to the running container's tmux session
|
# attach to the running container
|
||||||
docker exec -it "$NAME" tmux
|
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
|
## Notes
|
||||||
No test suite or CI — this is infra/config, not application code. Verify changes
|
No test suite or CI — this is infra/config, not application code. Verify changes
|
||||||
|
|||||||
+27
@@ -15,8 +15,10 @@ RUN pacman -Sy --noconfirm archlinux-keyring && \
|
|||||||
mariadb-clients \
|
mariadb-clients \
|
||||||
nodejs \
|
nodejs \
|
||||||
npm \
|
npm \
|
||||||
|
opencode \
|
||||||
openssh \
|
openssh \
|
||||||
php \
|
php \
|
||||||
|
php-sqlite \
|
||||||
pwgen \
|
pwgen \
|
||||||
python \
|
python \
|
||||||
redis \
|
redis \
|
||||||
@@ -56,6 +58,31 @@ RUN curl -fsSL https://claude.ai/install.sh | bash
|
|||||||
RUN PATH="/root/.local/bin:$PATH" bash -c \
|
RUN PATH="/root/.local/bin:$PATH" bash -c \
|
||||||
'curl -fsSL https://raw.githubusercontent.com/leeguooooo/claude-code-usage-bar/main/web-install.sh | bash'
|
'curl -fsSL https://raw.githubusercontent.com/leeguooooo/claude-code-usage-bar/main/web-install.sh | bash'
|
||||||
|
|
||||||
|
# MCP servers for Claude Code. Only installed here — they're registered at
|
||||||
|
# user scope by entrypoint.sh on container start, because user-scope MCP
|
||||||
|
# config lives in ~/.claude.json, which is bind-mounted from the host and
|
||||||
|
# would shadow anything written to it at build time. Each server picks up
|
||||||
|
# its instance URL/token from the environment (see files/bash_aliases).
|
||||||
|
|
||||||
|
# n8n — https://github.com/czlonkowski/n8n-mcp
|
||||||
|
RUN npm install -g --prefix /usr/local n8n-mcp
|
||||||
|
|
||||||
|
# Gitea — official server, latest release binary
|
||||||
|
# https://gitea.com/gitea/gitea-mcp
|
||||||
|
RUN tag="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/gitea-mcp/releases/latest \
|
||||||
|
| python -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')" && \
|
||||||
|
curl -fsSL "https://gitea.com/gitea/gitea-mcp/releases/download/${tag}/gitea-mcp_Linux_x86_64.tar.gz" \
|
||||||
|
| tar -xz -C /usr/local/bin gitea-mcp && \
|
||||||
|
chmod 755 /usr/local/bin/gitea-mcp
|
||||||
|
|
||||||
|
# InvokeAI — https://github.com/coinstax/invokeai-mcp-server. Upstream
|
||||||
|
# hardcodes the InvokeAI URL, so patch it to honor $INVOKEAI_BASE_URL; the
|
||||||
|
# grep fails the build if upstream changes shape and the patch stops landing.
|
||||||
|
RUN uv tool install invokeai-mcp-server && \
|
||||||
|
python -c 'import glob; path=glob.glob("/root/.local/share/uv/tools/invokeai-mcp-server/lib/python*/site-packages/invokeai_mcp_server.py")[0]; c=open(path).read(); c="import os\n"+c; c=c.replace("INVOKEAI_BASE_URL = \"http://127.0.0.1:9090\"", "INVOKEAI_BASE_URL = os.environ.get(\"INVOKEAI_BASE_URL\", \"http://127.0.0.1:9090\")"); open(path,"w").write(c)' && \
|
||||||
|
grep -q 'INVOKEAI_BASE_URL = os.environ.get' \
|
||||||
|
/root/.local/share/uv/tools/invokeai-mcp-server/lib/python*/site-packages/invokeai_mcp_server.py
|
||||||
|
|
||||||
# tmux wrapper
|
# tmux wrapper
|
||||||
COPY files/tmux /usr/local/bin/tmux
|
COPY files/tmux /usr/local/bin/tmux
|
||||||
COPY files/tmux.conf /etc/tmux.conf
|
COPY files/tmux.conf /etc/tmux.conf
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
# claudaris
|
# claudaris
|
||||||
|
|
||||||
Claudaris (klaw-DAR-iss): A moveable docker container for Claude code. using Arch btw.
|
Claudaris (klaw-DAR-iss): A moveable docker container for Claude and OpenCode. using Arch btw.
|
||||||
|
|
||||||
|
`./claudaris` is a thin wrapper around `docker compose` (see
|
||||||
|
`docker-compose.yml`) — `build`/`start`/`connect`/`remove` still work exactly
|
||||||
|
as below, and `docker compose` commands work directly too if you'd rather use
|
||||||
|
those.
|
||||||
|
|
||||||
|
Full documentation lives in [`docs/`](docs/README.md): [getting started](docs/getting-started.md),
|
||||||
|
the [`claudaris` executable](docs/claudaris.md), and [MCP servers](docs/mcp.md).
|
||||||
|
|
||||||
## Configure
|
## Configure
|
||||||
|
|
||||||
@@ -22,24 +30,34 @@ defaults to `/data/$NAME`; `WORKSPACE_DIR` (the host dir mounted as
|
|||||||
As root run build (always `--no-cache --pull`, so it picks up the latest Claude Code release):
|
As root run build (always `--no-cache --pull`, so it picks up the latest Claude Code release):
|
||||||
```
|
```
|
||||||
sudo ./claudaris build
|
sudo ./claudaris build
|
||||||
|
# or
|
||||||
|
docker buildx build --no-cache -t 4lights/claudaris:latest .
|
||||||
```
|
```
|
||||||
## Start the container
|
## Start the container
|
||||||
|
|
||||||
Make sure the volumes are as you like and start the container.
|
Make sure the volumes are as you like and start the container.
|
||||||
```
|
```
|
||||||
sudo ./claudaris start
|
sudo ./claudaris start
|
||||||
|
or
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Connect to the container
|
## Connect to the container
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo ./claudaris connect
|
sudo ./claudaris connect
|
||||||
|
# or
|
||||||
|
docker compose exec claudaris bash
|
||||||
|
# or
|
||||||
|
docker compose exec claudaris tmux
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stop and remove the container
|
## Stop and remove the container
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo ./claudaris remove
|
sudo ./claudaris remove
|
||||||
|
# or
|
||||||
|
docker compose down
|
||||||
```
|
```
|
||||||
(aliases: `stop`, `rm`)
|
(aliases: `stop`, `rm`)
|
||||||
|
|
||||||
@@ -49,6 +67,21 @@ To let the container SSH out to other machines, create `$DATA_DIR/ssh`
|
|||||||
on the host and populate it with keys/config *before* running `./claudaris start`. If
|
on the host and populate it with keys/config *before* running `./claudaris start`. If
|
||||||
present, it's bind-mounted read-only to `/root/.ssh`. It's off by default.
|
present, it's bind-mounted read-only to `/root/.ssh`. It's off by default.
|
||||||
|
|
||||||
|
## MCP servers
|
||||||
|
|
||||||
|
MCP servers for [Gitea](https://gitea.com/gitea/gitea-mcp),
|
||||||
|
[n8n](https://github.com/czlonkowski/n8n-mcp), and
|
||||||
|
[InvokeAI](https://github.com/coinstax/invokeai-mcp-server) are baked into
|
||||||
|
the image and registered with Claude Code (user scope) automatically on
|
||||||
|
container start. To point them at your instances, uncomment and fill in the
|
||||||
|
`export` lines at the bottom of your host-persisted
|
||||||
|
`$DATA_DIR/home/bash_aliases` (`GITEA_HOST`, `GITEA_ACCESS_TOKEN`,
|
||||||
|
`N8N_API_URL`, `N8N_API_KEY`, `INVOKEAI_BASE_URL`) — if yours was seeded
|
||||||
|
before this feature existed, copy the block from `files/bash_aliases`.
|
||||||
|
Tokens stay on the host, out of the image and repo. Note the services run
|
||||||
|
elsewhere (not in this container), so use URLs reachable from inside it —
|
||||||
|
`127.0.0.1` here means the container itself.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
Claude Code is installed at build time under `/root`, so a rebuild always
|
Claude Code is installed at build time under `/root`, so a rebuild always
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ REPO_DIR="$(pwd)"
|
|||||||
# people can each run their own container from this repo) and other options.
|
# people can each run their own container from this repo) and other options.
|
||||||
# It writes them to .env, which is gitignored and sourced here if present.
|
# It writes them to .env, which is gitignored and sourced here if present.
|
||||||
NAME=claudaris
|
NAME=claudaris
|
||||||
|
CLAUDARIS_IMAGE=4lights/claudaris:latest
|
||||||
[ -f .env ] && set -a && . .env && set +a
|
[ -f .env ] && set -a && . .env && set +a
|
||||||
|
|
||||||
|
# docker-compose.yml reads NAME/DATA_DIR/WORKSPACE_DIR for variable
|
||||||
|
# substitution, so every subcommand that shells out to `docker compose`
|
||||||
|
# needs them exported, with the same defaults `cmd_start` seeds on disk.
|
||||||
|
export NAME
|
||||||
|
export CLAUDARIS_IMAGE
|
||||||
|
export DATA_DIR="${DATA_DIR:-/data/$NAME}"
|
||||||
|
export WORKSPACE_DIR="${WORKSPACE_DIR:-/home/${USER:-$(id -un)}/projects}"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: ./claudaris <command>
|
Usage: ./claudaris <command>
|
||||||
@@ -18,7 +27,7 @@ Commands:
|
|||||||
config, configure Interactive wizard to write .env with your settings
|
config, configure Interactive wizard to write .env with your settings
|
||||||
build Build the container image (always --no-cache --pull)
|
build Build the container image (always --no-cache --pull)
|
||||||
start Start (or create) the container
|
start Start (or create) the container
|
||||||
connect Start the container if needed, then attach a tmux session
|
connect Start the container if needed, then attach a bash shell
|
||||||
remove, stop, rm Stop and remove the container
|
remove, stop, rm Stop and remove the container
|
||||||
help Show this message
|
help Show this message
|
||||||
EOF
|
EOF
|
||||||
@@ -39,9 +48,14 @@ cmd_config() {
|
|||||||
read -r -p "WORKSPACE_DIR [$workspace_dir_default]: " input
|
read -r -p "WORKSPACE_DIR [$workspace_dir_default]: " input
|
||||||
WORKSPACE_DIR="${input:-$workspace_dir_default}"
|
WORKSPACE_DIR="${input:-$workspace_dir_default}"
|
||||||
|
|
||||||
|
local claudaris_image_default="${CLAUDARIS_IMAGE:-4lights/claudaris:latest}"
|
||||||
|
read -r -p "CLAUDARIS_IMAGE [$claudaris_image_default]: " input
|
||||||
|
CLAUDARIS_IMAGE="${input:-$claudaris_image_default}"
|
||||||
|
|
||||||
cat > .env <<EOF
|
cat > .env <<EOF
|
||||||
# Written by \`./claudaris config\` — gitignored, per-user.
|
# Written by \`./claudaris config\` — gitignored, per-user.
|
||||||
NAME=$NAME
|
NAME=$NAME
|
||||||
|
CLAUDARIS_IMAGE=$CLAUDARIS_IMAGE
|
||||||
DATA_DIR=$DATA_DIR
|
DATA_DIR=$DATA_DIR
|
||||||
WORKSPACE_DIR=$WORKSPACE_DIR
|
WORKSPACE_DIR=$WORKSPACE_DIR
|
||||||
EOF
|
EOF
|
||||||
@@ -53,17 +67,10 @@ EOF
|
|||||||
|
|
||||||
cmd_build() {
|
cmd_build() {
|
||||||
export BUILDX_NO_DEFAULT_ATTESTATIONS=1
|
export BUILDX_NO_DEFAULT_ATTESTATIONS=1
|
||||||
docker build --no-cache --pull -t "$NAME" .
|
docker compose build --no-cache --pull
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_start() {
|
cmd_start() {
|
||||||
# Host directory for volume mount data, separate from the repo checkout.
|
|
||||||
DATA_DIR="${DATA_DIR:-/data/$NAME}"
|
|
||||||
|
|
||||||
# Host directory mounted as /projects (the project workspace) inside the
|
|
||||||
# container. Override via `./claudaris config` if it's not right for you.
|
|
||||||
WORKSPACE_DIR="${WORKSPACE_DIR:-/home/${USER:-$(id -un)}/projects}"
|
|
||||||
|
|
||||||
# Seed host copies of .bashrc and bash_aliases, without clobbering any
|
# Seed host copies of .bashrc and bash_aliases, without clobbering any
|
||||||
# customization already made on this host. Both are bind-mounted as
|
# customization already made on this host. Both are bind-mounted as
|
||||||
# single files below so they can be tweaked per-host (e.g. a coworker
|
# single files below so they can be tweaked per-host (e.g. a coworker
|
||||||
@@ -87,41 +94,47 @@ cmd_start() {
|
|||||||
[ -f "$DATA_DIR/claude/credentials.json" ] || touch "$DATA_DIR/claude/credentials.json"
|
[ -f "$DATA_DIR/claude/credentials.json" ] || touch "$DATA_DIR/claude/credentials.json"
|
||||||
[ -f "$DATA_DIR/claude/claude.json" ] || touch "$DATA_DIR/claude/claude.json"
|
[ -f "$DATA_DIR/claude/claude.json" ] || touch "$DATA_DIR/claude/claude.json"
|
||||||
|
|
||||||
|
# OpenCode stores provider credentials (API keys/OAuth tokens) in a single
|
||||||
|
# auth.json, same atomic-write concern as Claude Code above, so it gets the
|
||||||
|
# same individual-file bind mount treatment.
|
||||||
|
mkdir -p "$DATA_DIR/opencode"
|
||||||
|
[ -f "$DATA_DIR/opencode/auth.json" ] || touch "$DATA_DIR/opencode/auth.json"
|
||||||
|
|
||||||
# Optional: mount ~/.ssh into the container (read-only) for connecting out to
|
# Optional: mount ~/.ssh into the container (read-only) for connecting out to
|
||||||
# other nodes. Opt in by creating $DATA_DIR/ssh and populating it with
|
# other nodes. Opt in by creating $DATA_DIR/ssh and populating it with
|
||||||
# keys/config before starting the container.
|
# keys/config before starting the container. docker-compose.yml doesn't
|
||||||
ssh_mount=()
|
# list this mount (compose can't make a volume conditional on a host path
|
||||||
|
# existing), so it's added via a gitignored override file that compose
|
||||||
|
# picks up automatically when present, and removed when the opt-in host
|
||||||
|
# dir isn't there.
|
||||||
if [ -d "$DATA_DIR/ssh" ]; then
|
if [ -d "$DATA_DIR/ssh" ]; then
|
||||||
ssh_mount=(-v "$DATA_DIR/ssh:/root/.ssh:ro")
|
cat > "$REPO_DIR/docker-compose.override.yml" <<EOF
|
||||||
|
services:
|
||||||
|
claudaris:
|
||||||
|
volumes:
|
||||||
|
- $DATA_DIR/ssh:/root/.ssh:ro
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
rm -f "$REPO_DIR/docker-compose.override.yml"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if docker ps -aq -f name="^${NAME}\$" | grep -q .; then
|
docker compose up -d
|
||||||
docker start "$NAME"
|
|
||||||
else
|
|
||||||
docker run -d \
|
|
||||||
--name "$NAME" \
|
|
||||||
--hostname "$NAME" \
|
|
||||||
-v "$DATA_DIR/home/.bashrc:/root/.bashrc" \
|
|
||||||
-v "$DATA_DIR/home/bash_aliases:/opt/dotfiles/bash_aliases" \
|
|
||||||
-v "$DATA_DIR/claude/credentials.json:/root/.claude/.credentials.json" \
|
|
||||||
-v "$DATA_DIR/claude/claude.json:/root/.claude.json" \
|
|
||||||
-v "$WORKSPACE_DIR:/projects" \
|
|
||||||
"${ssh_mount[@]}" \
|
|
||||||
"$NAME"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_connect() {
|
cmd_connect() {
|
||||||
# The container exits on its own once the main tmux session ends (see
|
# The container exits on its own once the main tmux session ends (see
|
||||||
# entrypoint.sh) and isn't auto-restarted, so make sure it's up before
|
# entrypoint.sh) and isn't auto-restarted, so make sure it's up before
|
||||||
# exec'ing in — a no-op if it's already running.
|
# exec'ing in — a no-op if it's already running. Plain bash rather than
|
||||||
docker start "$NAME" >/dev/null
|
# tmux here: when this container runs on a remote host, the caller is
|
||||||
docker exec -it "$NAME" tmux
|
# typically already inside a tmux session locally, and nesting tmux in
|
||||||
|
# tmux is annoying. tmux itself stays installed and available — run
|
||||||
|
# `tmux` manually inside the container if you want it.
|
||||||
|
docker compose up -d >/dev/null
|
||||||
|
docker compose exec claudaris bash
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_remove() {
|
cmd_remove() {
|
||||||
docker stop "$NAME" >/dev/null 2>&1 || true
|
docker compose down
|
||||||
docker rm "$NAME"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-help}" in
|
case "${1:-help}" in
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
claudaris:
|
||||||
|
image: ${CLAUDARIS_IMAGE:-4lights/claudaris:latest}
|
||||||
|
container_name: ${NAME:-claudaris}
|
||||||
|
hostname: ${NAME:-claudaris}
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- ${DATA_DIR:-/data/claudaris}/home/.bashrc:/root/.bashrc
|
||||||
|
- ${DATA_DIR:-/data/claudaris}/home/bash_aliases:/opt/dotfiles/bash_aliases
|
||||||
|
- ${DATA_DIR:-/data/claudaris}/claude/credentials.json:/root/.claude/.credentials.json
|
||||||
|
- ${DATA_DIR:-/data/claudaris}/claude/claude.json:/root/.claude.json
|
||||||
|
- ${DATA_DIR:-/data/claudaris}/opencode/auth.json:/root/.local/share/opencode/auth.json
|
||||||
|
- ${WORKSPACE_DIR:-./}:/projects
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# claudaris docs
|
||||||
|
|
||||||
|
Claudaris (klaw-DAR-iss) is a movable Docker container for running
|
||||||
|
[Claude Code](https://code.claude.com/docs) in isolation, built on Arch Linux.
|
||||||
|
Your login, dotfiles, and MCP configuration live on the host, so the image can
|
||||||
|
be rebuilt at any time (always pulling the latest Claude Code release) without
|
||||||
|
losing anything.
|
||||||
|
|
||||||
|
- **[Getting started](getting-started.md)** — prerequisites, first-time setup,
|
||||||
|
and daily use.
|
||||||
|
- **[The `claudaris` executable](claudaris.md)** — every subcommand, what it
|
||||||
|
does, and the files and mounts behind it.
|
||||||
|
- **[MCP servers](mcp.md)** — the Gitea, n8n, and InvokeAI MCP servers baked
|
||||||
|
into the image, and how to point them at your instances.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# The `claudaris` executable
|
||||||
|
|
||||||
|
`./claudaris` is a single bash script at the repo root that wraps
|
||||||
|
`docker compose` (see `docker-compose.yml`). It `cd`s to its own directory
|
||||||
|
first, so it can be invoked from anywhere. Run it with no arguments (or
|
||||||
|
`help`) for usage. Plain `docker compose` subcommands work directly too, as
|
||||||
|
long as `.env` is present (compose reads it automatically) — `claudaris`
|
||||||
|
just adds the `config` wizard and the host-side seeding/mount logic below.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: ./claudaris <command>
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
config, configure Interactive wizard to write .env with your settings
|
||||||
|
build Build the container image (always --no-cache --pull)
|
||||||
|
start Start (or create) the container
|
||||||
|
connect Start the container if needed, then attach a tmux session
|
||||||
|
remove, stop, rm Stop and remove the container
|
||||||
|
help Show this message
|
||||||
|
```
|
||||||
|
|
||||||
|
All commands source `.env` if present (gitignored, written by `config`),
|
||||||
|
falling back to `NAME=claudaris` and the defaults listed below.
|
||||||
|
|
||||||
|
## `config` (alias: `configure`)
|
||||||
|
|
||||||
|
Interactive wizard: prompts for `NAME`, `DATA_DIR`, and `WORKSPACE_DIR`,
|
||||||
|
showing the current/default value for each (enter keeps it), then writes all
|
||||||
|
three to `.env`. Safe to re-run any time. See
|
||||||
|
[Getting started](getting-started.md#1-configure) for what each variable
|
||||||
|
means.
|
||||||
|
|
||||||
|
## `build`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./claudaris build
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs `docker compose build --no-cache --pull`. Always uncached so a
|
||||||
|
rebuild picks up the latest Arch packages, the latest Claude Code release
|
||||||
|
(installed straight into `/root` in the image), and the latest
|
||||||
|
[MCP server](mcp.md) releases.
|
||||||
|
|
||||||
|
## `start`
|
||||||
|
|
||||||
|
Runs `docker compose up -d`, which creates the container if it doesn't exist
|
||||||
|
and (re)starts it otherwise, per `docker-compose.yml`. Container/image name,
|
||||||
|
volumes, and workspace mount come from `NAME`/`DATA_DIR`/`WORKSPACE_DIR`,
|
||||||
|
which compose reads from `.env` the same way `claudaris` does — mount
|
||||||
|
changes in `docker-compose.yml` take effect on the next `up -d` without
|
||||||
|
needing `remove` first.
|
||||||
|
|
||||||
|
Before calling compose, `start` seeds the host side, without overwriting
|
||||||
|
anything that already exists:
|
||||||
|
|
||||||
|
- `$DATA_DIR/home/.bashrc` and `$DATA_DIR/home/bash_aliases` — copied from
|
||||||
|
`files/bashrc` and `files/bash_aliases`.
|
||||||
|
- `$DATA_DIR/claude/credentials.json`, `$DATA_DIR/claude/claude.json`, and
|
||||||
|
`$DATA_DIR/opencode/auth.json` — created empty. Seeding them as files
|
||||||
|
matters: if Docker had to create the mount targets itself it would make
|
||||||
|
directories, breaking the login mounts.
|
||||||
|
- `docker-compose.override.yml` (gitignored) — written to add the SSH mount
|
||||||
|
below if `$DATA_DIR/ssh` exists, removed otherwise. Compose merges this
|
||||||
|
file automatically when present; it's the only mount that can't live in
|
||||||
|
`docker-compose.yml` directly, since compose has no way to make a volume
|
||||||
|
conditional on a host path existing.
|
||||||
|
|
||||||
|
The mounts, all declared in `docker-compose.yml`:
|
||||||
|
|
||||||
|
| Mount | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `hostname: ${NAME}` | Prompt reads `root@$NAME` instead of a random container ID. |
|
||||||
|
| `$DATA_DIR/home/.bashrc` → `/root/.bashrc` | Per-host shell config, editable without a rebuild. |
|
||||||
|
| `$DATA_DIR/home/bash_aliases` → `/opt/dotfiles/bash_aliases` | Per-host aliases and [MCP env vars](mcp.md); sourced by `.bashrc`. |
|
||||||
|
| `$DATA_DIR/claude/credentials.json` → `/root/.claude/.credentials.json` | Claude Code OAuth token. |
|
||||||
|
| `$DATA_DIR/claude/claude.json` → `/root/.claude.json` | Claude Code account/onboarding state and user-scope MCP config. |
|
||||||
|
| `$DATA_DIR/opencode/auth.json` → `/root/.local/share/opencode/auth.json` | OpenCode provider credentials (API keys/OAuth tokens). |
|
||||||
|
| `$WORKSPACE_DIR` → `/projects` | Your project workspace. |
|
||||||
|
| `$DATA_DIR/ssh` → `/root/.ssh` (read-only, override file, only if the host dir exists) | Opt-in SSH access to other machines. |
|
||||||
|
|
||||||
|
The login files are individual file mounts rather than a directory volume on
|
||||||
|
purpose: both 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 — which would silently
|
||||||
|
break persistence. A bind mount doesn't have that failure mode.
|
||||||
|
|
||||||
|
## `connect`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./claudaris connect
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs `docker compose up -d` (a no-op if already running) and then
|
||||||
|
`docker compose exec claudaris bash`.
|
||||||
|
|
||||||
|
The explicit `up -d` matters because the container stops itself: the
|
||||||
|
entrypoint creates the `main` tmux session and exits once that session ends
|
||||||
|
(tmux defaults — a window closes when its shell exits, the session closes
|
||||||
|
with its last window). There's no `--restart` policy, so after a Ctrl+D the
|
||||||
|
container sits stopped until `connect` (or `start`) brings it back. `tmux`
|
||||||
|
itself stays installed and available — run it manually inside the container
|
||||||
|
(the in-container `tmux` wrapper attaches to the `main` session if it exists
|
||||||
|
and creates it otherwise) if you want it.
|
||||||
|
|
||||||
|
## `remove` (aliases: `stop`, `rm`)
|
||||||
|
|
||||||
|
Runs `docker compose down`, stopping and removing the container so the next
|
||||||
|
`start` recreates it fresh — needed after a `build` to actually run the new
|
||||||
|
image, or after changing mounts. Nothing under `$DATA_DIR` or
|
||||||
|
`$WORKSPACE_DIR` is touched.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Getting started
|
||||||
|
|
||||||
|
Claudaris runs Claude Code inside an Arch Linux container, with your project
|
||||||
|
workspace bind-mounted in and your login persisted on the host. Everything is
|
||||||
|
driven by the [`./claudaris` executable](claudaris.md) at the repo root.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker installed and running on the host.
|
||||||
|
- Root (or equivalent Docker) access — the examples below use `sudo`.
|
||||||
|
- A Claude account to log in with on first run.
|
||||||
|
|
||||||
|
## 1. Configure
|
||||||
|
|
||||||
|
From the repo root, run the interactive wizard and answer its prompts
|
||||||
|
(press enter to keep a default):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./claudaris config
|
||||||
|
```
|
||||||
|
|
||||||
|
It asks for three values and writes them to `.env` (gitignored — every user
|
||||||
|
of a shared checkout keeps their own):
|
||||||
|
|
||||||
|
| Variable | Default | Meaning |
|
||||||
|
| --------------- | ---------------------- | -------------------------------------------------------------- |
|
||||||
|
| `NAME` | `claudaris` | Image *and* container name. Pick something unique to you (e.g. `chris-claude`) if several people run containers from the same repo checkout. |
|
||||||
|
| `DATA_DIR` | `/data/$NAME` | Host directory for everything that must survive rebuilds: dotfiles, Claude login, optional SSH keys. |
|
||||||
|
| `WORKSPACE_DIR` | `/home/$USER/projects` | Host directory mounted as `/projects` inside the container — this is where Claude Code works. |
|
||||||
|
|
||||||
|
Re-run `config` any time to change these.
|
||||||
|
|
||||||
|
## 2. Build the image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./claudaris build
|
||||||
|
```
|
||||||
|
|
||||||
|
The build always runs with `--no-cache --pull`, so every rebuild picks up the
|
||||||
|
latest Arch packages and the latest Claude Code release. Claude Code is
|
||||||
|
installed into the image itself (not a volume) — rebuilding is how you
|
||||||
|
upgrade it.
|
||||||
|
|
||||||
|
## 3. Start and connect
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./claudaris start
|
||||||
|
sudo ./claudaris connect
|
||||||
|
```
|
||||||
|
|
||||||
|
`start` creates the container (seeding host-side copies of the dotfiles and
|
||||||
|
login files on first run); `connect` attaches you to its tmux session. You
|
||||||
|
land in a shell at `/projects` as `root@$NAME`.
|
||||||
|
|
||||||
|
On first connect, run `claude` and log in. The two files that carry the login
|
||||||
|
(`~/.claude/.credentials.json` and `~/.claude.json`) are bind-mounted from
|
||||||
|
`$DATA_DIR/claude/` on the host, so you stay logged in across container
|
||||||
|
rebuilds and re-creations.
|
||||||
|
|
||||||
|
## 4. Daily use
|
||||||
|
|
||||||
|
- **Attach:** `sudo ./claudaris connect` — also restarts the container if it
|
||||||
|
stopped. The container exits on its own when the tmux session ends (last
|
||||||
|
shell exits via Ctrl+D, `exit`, or a crash); there is no Docker restart
|
||||||
|
policy, so `connect` bringing it back up is the normal flow.
|
||||||
|
- **Detach without stopping anything:** standard tmux detach
|
||||||
|
(`Ctrl+b d`).
|
||||||
|
- **Upgrade Claude Code / packages:** `sudo ./claudaris remove`, then
|
||||||
|
`sudo ./claudaris build` and `sudo ./claudaris start`. Login, dotfiles, and
|
||||||
|
MCP registration all survive this.
|
||||||
|
|
||||||
|
## Customizing your shell
|
||||||
|
|
||||||
|
`~/.bashrc` and the aliases file are bind-mounted single files living at
|
||||||
|
`$DATA_DIR/home/.bashrc` and `$DATA_DIR/home/bash_aliases` on the host. Edit
|
||||||
|
them there (or inside the container) and the changes persist across rebuilds —
|
||||||
|
no image change needed. They're seeded from `files/bashrc` and
|
||||||
|
`files/bash_aliases` the first time `start` runs and never overwritten after
|
||||||
|
that.
|
||||||
|
|
||||||
|
This is also where you configure the [MCP servers](mcp.md): the bottom of
|
||||||
|
`bash_aliases` has commented `export` lines for the Gitea/n8n/InvokeAI URLs
|
||||||
|
and tokens.
|
||||||
|
|
||||||
|
## Optional: SSH access to other machines
|
||||||
|
|
||||||
|
To let the container SSH out, create `$DATA_DIR/ssh` on the host and populate
|
||||||
|
it with keys/config *before* running `start`. If the directory exists, it's
|
||||||
|
bind-mounted read-only at `/root/.ssh`. It's off by default.
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
# MCP servers
|
||||||
|
|
||||||
|
The image ships three [MCP](https://modelcontextprotocol.io/) servers for
|
||||||
|
Claude Code, all speaking stdio:
|
||||||
|
|
||||||
|
| Server | Name in `/mcp` | What it gives Claude | Source |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Gitea | `gitea` | Repos, issues, PRs, releases on your Gitea instance | [gitea/gitea-mcp](https://gitea.com/gitea/gitea-mcp) (official, latest release binary) |
|
||||||
|
| n8n | `n8n` | n8n node documentation, plus workflow creation/management when the API env vars are set | [czlonkowski/n8n-mcp](https://github.com/czlonkowski/n8n-mcp) (npm) |
|
||||||
|
| InvokeAI | `invokeai` | Text-to-image, img2img, and upscaling against your InvokeAI instance | [coinstax/invokeai-mcp-server](https://github.com/coinstax/invokeai-mcp-server) (PyPI) |
|
||||||
|
|
||||||
|
## How install and registration are split
|
||||||
|
|
||||||
|
The Dockerfile only *installs* the servers (`/usr/local/bin/n8n-mcp`,
|
||||||
|
`/usr/local/bin/gitea-mcp`, and a `uv` tool venv for InvokeAI).
|
||||||
|
*Registration* happens in `files/entrypoint.sh` on every container start,
|
||||||
|
via `claude mcp add --scope user`.
|
||||||
|
|
||||||
|
It has to work that way: user-scope MCP config lives in `~/.claude.json`,
|
||||||
|
which [`claudaris start`](claudaris.md#start) bind-mounts from the host — so
|
||||||
|
anything registered at build time would be shadowed by the mounted file. The
|
||||||
|
runtime registration writes into the mounted file instead, which also means
|
||||||
|
it persists across rebuilds like the login does.
|
||||||
|
|
||||||
|
Registration is guarded per server: a server that's already present is left
|
||||||
|
alone, so hand edits to its config survive restarts. The flip side is that a
|
||||||
|
server deleted with `claude mcp remove` comes back on the next container
|
||||||
|
start — to change one permanently, edit its entry rather than removing it.
|
||||||
|
|
||||||
|
## Pointing the servers at your instances
|
||||||
|
|
||||||
|
The servers read their URLs and tokens from the environment Claude Code runs
|
||||||
|
in (Claude Code passes its environment through to stdio MCP servers). Set
|
||||||
|
them in your host-persisted aliases file, `$DATA_DIR/home/bash_aliases`,
|
||||||
|
where a commented template already exists at the bottom:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GITEA_HOST='https://gitea.example.com'
|
||||||
|
export GITEA_ACCESS_TOKEN='...'
|
||||||
|
export N8N_API_URL='https://n8n.example.com' # optional: enables workflow management tools
|
||||||
|
export N8N_API_KEY='...'
|
||||||
|
export INVOKEAI_BASE_URL='http://invokeai.example.com:9090'
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start a new shell (or `source ~/.bashrc`) and launch `claude`. Because
|
||||||
|
the file lives on the host, tokens never end up in the image or the repo. If
|
||||||
|
your `bash_aliases` was seeded before the MCP servers existed, copy the
|
||||||
|
template block from `files/bash_aliases`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- **URLs must be reachable from inside the container.** `127.0.0.1` means
|
||||||
|
the container itself, not the host. Use the host's LAN address or service
|
||||||
|
hostname.
|
||||||
|
- **Gitea:** create the token in Gitea under *Settings → Applications →
|
||||||
|
Generate New Token*.
|
||||||
|
- **n8n:** without `N8N_API_URL`/`N8N_API_KEY` the server still works as a
|
||||||
|
node-documentation reference; the API pair unlocks creating and managing
|
||||||
|
workflows. Create the key in n8n under *Settings → n8n API*.
|
||||||
|
- **InvokeAI:** defaults to `http://127.0.0.1:9090` if unset. Upstream
|
||||||
|
hardcodes that URL; the Dockerfile patches the package so
|
||||||
|
`INVOKEAI_BASE_URL` is honored (and the build fails loudly if a future
|
||||||
|
upstream release breaks the patch). Registration uses
|
||||||
|
`python -m invokeai_mcp_server` because the package's console script is
|
||||||
|
broken upstream.
|
||||||
|
|
||||||
|
## Checking it works
|
||||||
|
|
||||||
|
Inside the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp list # all three should show as configured
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in a Claude Code session, run `/mcp` to see connection status, and try
|
||||||
|
something like *"list my Gitea repos"*. Note that a server whose env vars
|
||||||
|
are unset still connects — the servers only read them when a tool is
|
||||||
|
actually called — so a green `/mcp` status doesn't prove the configuration
|
||||||
|
is right. Tool calls failing (or Gitea answering about gitea.com instead of
|
||||||
|
your instance, its built-in default) is the symptom of missing/wrong env
|
||||||
|
vars.
|
||||||
+10
-2
@@ -4,7 +4,6 @@
|
|||||||
# ls long hidden and human readable
|
# ls long hidden and human readable
|
||||||
alias ll='ls -lah'
|
alias ll='ls -lah'
|
||||||
alias l1='ls -1'
|
alias l1='ls -1'
|
||||||
alias ls='ls --color=auto'
|
|
||||||
|
|
||||||
# up levels (up #_of_dir_up)
|
# up levels (up #_of_dir_up)
|
||||||
function cd_up() {
|
function cd_up() {
|
||||||
@@ -23,4 +22,13 @@ alias tl='tmux list-sessions'
|
|||||||
alias gs='git status'
|
alias gs='git status'
|
||||||
|
|
||||||
alias btw='fastfetch'
|
alias btw='fastfetch'
|
||||||
alias cd='z'
|
alias cd='z'
|
||||||
|
|
||||||
|
# MCP servers (see Dockerfile / entrypoint.sh). Uncomment and point at your
|
||||||
|
# instances — Claude Code passes its environment through to the stdio
|
||||||
|
# servers, so exporting these here is all the configuration they need.
|
||||||
|
#export GITEA_HOST='https://gitea.example.com'
|
||||||
|
#export GITEA_ACCESS_TOKEN='changeme'
|
||||||
|
#export N8N_API_URL='https://n8n.example.com' # optional: enables workflow management tools
|
||||||
|
#export N8N_API_KEY='changeme'
|
||||||
|
#export INVOKEAI_BASE_URL='http://127.0.0.1:9090'
|
||||||
@@ -5,6 +5,26 @@ set -euo pipefail
|
|||||||
# which bind-mounts ~/.claude/.credentials.json and ~/.claude.json individually
|
# which bind-mounts ~/.claude/.credentials.json and ~/.claude.json individually
|
||||||
# from the host (see claudaris) — nothing to seed or symlink here.
|
# from the host (see claudaris) — nothing to seed or symlink here.
|
||||||
|
|
||||||
|
# Register the MCP servers baked into the image (see Dockerfile) at user
|
||||||
|
# scope. Done here rather than at build time because user-scope MCP config
|
||||||
|
# lives in ~/.claude.json, which is bind-mounted from the host. Each server
|
||||||
|
# is only added if missing, so hand edits survive restarts — but a server
|
||||||
|
# removed via `claude mcp remove` comes back on the next container start.
|
||||||
|
[ -s /root/.claude.json ] || echo '{}' > /root/.claude.json
|
||||||
|
register_mcp() {
|
||||||
|
local name="$1"; shift
|
||||||
|
/root/.local/bin/claude mcp get "$name" >/dev/null 2>&1 ||
|
||||||
|
/root/.local/bin/claude mcp add --scope user "$name" "$@" || true
|
||||||
|
}
|
||||||
|
register_mcp gitea -- /usr/local/bin/gitea-mcp -t stdio
|
||||||
|
register_mcp n8n --env MCP_MODE=stdio --env LOG_LEVEL=error \
|
||||||
|
--env DISABLE_CONSOLE_OUTPUT=true -- /usr/local/bin/n8n-mcp
|
||||||
|
# Not the `invokeai-mcp-server` console script — it's broken upstream
|
||||||
|
# (calls the async main() without asyncio.run); `python -m` is the
|
||||||
|
# invocation upstream documents.
|
||||||
|
register_mcp invokeai -- \
|
||||||
|
/root/.local/share/uv/tools/invokeai-mcp-server/bin/python -m invokeai_mcp_server
|
||||||
|
|
||||||
# Create the main tmux session if it does not exist.
|
# Create the main tmux session if it does not exist.
|
||||||
/usr/bin/tmux new-session -d -s main 2>/dev/null || true
|
/usr/bin/tmux new-session -d -s main 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
+45
-25
@@ -387,10 +387,11 @@
|
|||||||
<h1 class="glitch-title" data-text="CLAUDARIS">CLAUDARIS</h1>
|
<h1 class="glitch-title" data-text="CLAUDARIS">CLAUDARIS</h1>
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
/klaw-<span class="accent">DAR</span>-iss/ — a moveable docker container for
|
/klaw-<span class="accent">DAR</span>-iss/ — a moveable docker container for
|
||||||
<span class="accent2">Claude Code</span>. using Arch btw.
|
<span class="accent2">Claude Code</span> and <span class="accent2">OpenCode</span>. using Arch btw.
|
||||||
</div>
|
</div>
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<span class="tag">ARCHLINUX</span>
|
<span class="tag">ARCHLINUX</span>
|
||||||
|
<span class="tag">DOCKER COMPOSE</span>
|
||||||
<span class="tag">TMUX</span>
|
<span class="tag">TMUX</span>
|
||||||
<span class="tag">SELF-HOSTED</span>
|
<span class="tag">SELF-HOSTED</span>
|
||||||
<span class="tag">NO-RESTART-POLICY</span>
|
<span class="tag">NO-RESTART-POLICY</span>
|
||||||
@@ -414,11 +415,14 @@
|
|||||||
<pre><code>./claudaris config</code></pre>
|
<pre><code>./claudaris config</code></pre>
|
||||||
<p>
|
<p>
|
||||||
It writes your answers to <code>.env</code> (gitignored — every user
|
It writes your answers to <code>.env</code> (gitignored — every user
|
||||||
keeps their own), which every other command sources. Set
|
keeps their own). <code>claudaris</code> sources it, and
|
||||||
<code>NAME</code> to something unique to you (e.g. <code>claude-ten</code>)
|
<code>docker compose</code> reads the same file automatically since it
|
||||||
if more than one person is running a container from this same repo
|
lives in the project directory compose runs from — so
|
||||||
checkout. Re-run <code>config</code> / <code>configure</code> any time
|
<code>claudaris</code> and plain <code>docker compose</code> commands
|
||||||
to update it.
|
always agree. Set <code>NAME</code> to something unique to you (e.g.
|
||||||
|
<code>claude-ten</code>) if more than one person is running a container
|
||||||
|
from this same repo checkout. Re-run <code>config</code> /
|
||||||
|
<code>configure</code> any time to update it.
|
||||||
</p>
|
</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>variable</th><th>purpose</th><th>default</th></tr>
|
<tr><th>variable</th><th>purpose</th><th>default</th></tr>
|
||||||
@@ -429,7 +433,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="name">DATA_DIR</td>
|
<td class="name">DATA_DIR</td>
|
||||||
<td>Host directory for volume mount data — bashrc, aliases, Claude auth, ssh keys.</td>
|
<td>Host directory for volume mount data — bashrc, aliases, Claude Code / OpenCode auth, ssh keys.</td>
|
||||||
<td class="default">/data/$NAME</td>
|
<td class="default">/data/$NAME</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -442,7 +446,14 @@
|
|||||||
|
|
||||||
<section id="commands">
|
<section id="commands">
|
||||||
<h2>Commands</h2>
|
<h2>Commands</h2>
|
||||||
<p class="dim">Everything runs through the single <code>claudaris</code> entry point.</p>
|
<p class="dim">
|
||||||
|
<code>claudaris</code> is a thin wrapper around
|
||||||
|
<code>docker compose</code> (see <code>docker-compose.yml</code>) — it
|
||||||
|
adds the config wizard and host-side seeding, then shells out to
|
||||||
|
compose for the actual container lifecycle. Plain <code>docker
|
||||||
|
compose</code> commands work directly too, once <code>start</code> has
|
||||||
|
seeded the host once.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="cmd-grid">
|
<div class="cmd-grid">
|
||||||
<div class="cmd-card">
|
<div class="cmd-card">
|
||||||
@@ -452,22 +463,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="cmd-card">
|
<div class="cmd-card">
|
||||||
<div><span class="cmd">build</span></div>
|
<div><span class="cmd">build</span></div>
|
||||||
<div class="desc">Builds the image (always <code>--no-cache --pull</code>, so it picks up the latest Claude Code release).</div>
|
<div class="desc"><code>docker compose build --no-cache --pull</code> — always uncached, so it picks up the latest Claude Code / OpenCode releases.</div>
|
||||||
<pre><code>sudo ./claudaris build</code></pre>
|
<pre><code>sudo ./claudaris build</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="cmd-card">
|
<div class="cmd-card">
|
||||||
<div><span class="cmd">start</span></div>
|
<div><span class="cmd">start</span></div>
|
||||||
<div class="desc">Creates or restarts the container with all volumes wired up.</div>
|
<div class="desc">Seeds host files, then <code>docker compose up -d</code> with all volumes wired up.</div>
|
||||||
<pre><code>sudo ./claudaris start</code></pre>
|
<pre><code>sudo ./claudaris start</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="cmd-card">
|
<div class="cmd-card">
|
||||||
<div><span class="cmd">connect</span></div>
|
<div><span class="cmd">connect</span></div>
|
||||||
<div class="desc">Starts the container if needed, then attaches your tmux session.</div>
|
<div class="desc">Starts the container if needed, then <code>docker compose exec</code>s a shell.</div>
|
||||||
<pre><code>sudo ./claudaris connect</code></pre>
|
<pre><code>sudo ./claudaris connect</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="cmd-card">
|
<div class="cmd-card">
|
||||||
<div><span class="cmd">remove</span><span class="alias">stop · rm</span></div>
|
<div><span class="cmd">remove</span><span class="alias">stop · rm</span></div>
|
||||||
<div class="desc">Stops and removes the container so the next <code>start</code> recreates it fresh.</div>
|
<div class="desc"><code>docker compose down</code> — stops and removes the container so the next <code>start</code> recreates it fresh.</div>
|
||||||
<pre><code>sudo ./claudaris remove</code></pre>
|
<pre><code>sudo ./claudaris remove</code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -481,21 +492,28 @@
|
|||||||
<em>before</em> running <code>./claudaris start</code>. If present, it's
|
<em>before</em> running <code>./claudaris start</code>. If present, it's
|
||||||
bind-mounted read-only to <code>/root/.ssh</code>.
|
bind-mounted read-only to <code>/root/.ssh</code>.
|
||||||
</p>
|
</p>
|
||||||
<div class="note"><strong>off by default</strong> — nothing is mounted unless the directory exists.</div>
|
<div class="note"><strong>off by default</strong> — nothing is mounted unless the directory exists. Since compose can't make a volume conditional on a host path existing, <code>start</code> writes (or removes) a gitignored <code>docker-compose.override.yml</code> to add this mount, which compose merges automatically when present.</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="internals">
|
<section id="internals">
|
||||||
<h2>Internals</h2>
|
<h2>Internals</h2>
|
||||||
|
|
||||||
<p><strong style="color:var(--green)">Persistent login.</strong>
|
<p><strong style="color:var(--green)">Persistent login.</strong>
|
||||||
Claude Code is installed at build time under <code>/root</code>, so a
|
Claude Code and OpenCode are both installed at build time (Claude Code
|
||||||
rebuild always picks up the latest release. Two files are bind-mounted
|
via installer straight into <code>/root</code>; OpenCode via
|
||||||
individually to survive that: <code>~/.claude/.credentials.json</code>
|
<code>pacman</code>), so a rebuild always picks up the latest release.
|
||||||
(the OAuth token) and <code>~/.claude.json</code> (account/onboarding
|
Three files are bind-mounted individually to survive that:
|
||||||
state — Claude Code checks this too, so persisting the token alone
|
<code>~/.claude/.credentials.json</code> (the OAuth token),
|
||||||
isn't enough to avoid a re-login prompt after a rebuild).
|
<code>~/.claude.json</code> (account/onboarding state — Claude Code
|
||||||
<code>./claudaris start</code> seeds both from empty on the host the
|
checks this too, so persisting the token alone isn't enough to avoid a
|
||||||
first time it runs.
|
re-login prompt after a rebuild), and
|
||||||
|
<code>~/.local/share/opencode/auth.json</code> (OpenCode's provider
|
||||||
|
credentials). <code>./claudaris start</code> seeds all three from empty
|
||||||
|
on the host the first time it runs. All three are individual-file
|
||||||
|
mounts rather than a directory volume, since both tools likely save
|
||||||
|
atomically (write a temp file, then <code>rename()</code> over the
|
||||||
|
target) — <code>rename()</code> onto a symlink replaces the symlink
|
||||||
|
instead of writing through it, silently breaking persistence.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><strong style="color:var(--green)">Self-stopping.</strong>
|
<p><strong style="color:var(--green)">Self-stopping.</strong>
|
||||||
@@ -518,8 +536,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><strong style="color:var(--green)">Hostname.</strong>
|
<p><strong style="color:var(--green)">Hostname.</strong>
|
||||||
The container is started with <code>--hostname "$NAME"</code>, so the
|
<code>docker-compose.yml</code> sets <code>hostname: ${NAME}</code>, so
|
||||||
prompt reads <code>root@claudaris</code> (or whatever you set
|
the prompt reads <code>root@claudaris</code> (or whatever you set
|
||||||
<code>NAME</code> to) instead of a random container ID.
|
<code>NAME</code> to) instead of a random container ID.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -528,7 +546,9 @@
|
|||||||
<h2>Repo layout</h2>
|
<h2>Repo layout</h2>
|
||||||
<ul class="check">
|
<ul class="check">
|
||||||
<li><code>claudaris</code> — the entry point: build / start / connect / remove / help.</li>
|
<li><code>claudaris</code> — the entry point: build / start / connect / remove / help.</li>
|
||||||
<li><code>Dockerfile</code> — Arch base image, dotfiles, Claude Code install, entrypoint.</li>
|
<li><code>docker-compose.yml</code> — the container definition (image, volumes) that <code>claudaris</code> wraps.</li>
|
||||||
|
<li><code>docker-compose.override.yml</code> — gitignored, written by <code>start</code> for the opt-in SSH mount.</li>
|
||||||
|
<li><code>Dockerfile</code> — Arch base image, dotfiles, Claude Code / OpenCode install, entrypoint.</li>
|
||||||
<li><code>.env</code> — written by <code>claudaris config</code>, per-user, gitignored.</li>
|
<li><code>.env</code> — written by <code>claudaris config</code>, per-user, gitignored.</li>
|
||||||
<li><code>files/</code> — <code>bashrc</code>, <code>bash_aliases</code>, <code>entrypoint.sh</code>, <code>tmux</code> wrapper, <code>tmux.conf</code>.</li>
|
<li><code>files/</code> — <code>bashrc</code>, <code>bash_aliases</code>, <code>entrypoint.sh</code>, <code>tmux</code> wrapper, <code>tmux.conf</code>.</li>
|
||||||
<li><code>AGENTS.md</code> — deep-dive notes for anyone (human or agent) hacking on this repo.</li>
|
<li><code>AGENTS.md</code> — deep-dive notes for anyone (human or agent) hacking on this repo.</li>
|
||||||
@@ -536,7 +556,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
claudaris <span class="blink">█</span> moveable · self-hosted · always running the latest Claude Code
|
claudaris <span class="blink">█</span> moveable · self-hosted · always running the latest Claude Code + OpenCode
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user