Compare commits

...

6 Commits

Author SHA1 Message Date
nick ac1d4470bd updated readme 2026-07-29 13:44:52 -07:00
nick 48adfb9c8e minor tweaks, fixed python in build 2026-07-29 13:44:35 -07:00
nick ee48d50abf Test worked 2026-07-21 02:17:06 -07:00
code 06931ace00 removed tmux as default, back to bash 2026-07-15 19:21:07 +00:00
code c3432b2df4 docs: unconfigured MCP servers still connect; failures show at tool-call time
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:01:46 +00:00
code bcd7e012ba added gitea, n8n and invokeai MCP servers, plus docs/
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:47:29 +00:00
13 changed files with 568 additions and 98 deletions
+4
View File
@@ -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
+76 -39
View File
@@ -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
View File
@@ -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
+34 -1
View File
@@ -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
+44 -31
View File
@@ -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
+14
View File
@@ -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
+14
View File
@@ -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.
+110
View File
@@ -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.
+89
View File
@@ -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
View File
@@ -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
View File
@@ -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'
+20
View File
@@ -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
View File
@@ -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>