Compare commits

...

4 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
9 changed files with 221 additions and 128 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
+52 -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
@@ -57,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),
@@ -102,22 +118,19 @@ Docker scaffold for running Claude Code in an isolated container (see README.md)
## 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
+3 -2
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 \
@@ -77,8 +79,7 @@ RUN tag="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/gitea-mcp/releases/la
# hardcodes the InvokeAI URL, so patch it to honor $INVOKEAI_BASE_URL; the # 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. # grep fails the build if upstream changes shape and the patch stops landing.
RUN uv tool install invokeai-mcp-server && \ RUN uv tool install invokeai-mcp-server && \
sed -i 's|^INVOKEAI_BASE_URL = .*|import os\nINVOKEAI_BASE_URL = os.environ.get("INVOKEAI_BASE_URL", "http://127.0.0.1:9090")|' \ 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)' && \
/root/.local/share/uv/tools/invokeai-mcp-server/lib/python*/site-packages/invokeai_mcp_server.py && \
grep -q 'INVOKEAI_BASE_URL = os.environ.get' \ grep -q 'INVOKEAI_BASE_URL = os.environ.get' \
/root/.local/share/uv/tools/invokeai-mcp-server/lib/python*/site-packages/invokeai_mcp_server.py /root/.local/share/uv/tools/invokeai-mcp-server/lib/python*/site-packages/invokeai_mcp_server.py
+16 -1
View File
@@ -1,6 +1,11 @@
# 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), 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). the [`claudaris` executable](docs/claudaris.md), and [MCP servers](docs/mcp.md).
@@ -25,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`)
+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
+43 -29
View File
@@ -1,8 +1,11 @@
# The `claudaris` executable # The `claudaris` executable
`./claudaris` is a single bash script at the repo root that wraps every `./claudaris` is a single bash script at the repo root that wraps
Docker operation. It `cd`s to its own directory first, so it can be invoked `docker compose` (see `docker-compose.yml`). It `cd`s to its own directory
from anywhere. Run it with no arguments (or `help`) for usage. 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> Usage: ./claudaris <command>
@@ -33,43 +36,53 @@ means.
sudo ./claudaris build sudo ./claudaris build
``` ```
Runs `docker build --no-cache --pull -t "$NAME" .`. Always uncached so a Runs `docker compose build --no-cache --pull`. Always uncached so a
rebuild picks up the latest Arch packages, the latest Claude Code release rebuild picks up the latest Arch packages, the latest Claude Code release
(installed straight into `/root` in the image), and the latest (installed straight into `/root` in the image), and the latest
[MCP server](mcp.md) releases. [MCP server](mcp.md) releases.
## `start` ## `start`
Creates and starts the container — or just `docker start`s it if a container Runs `docker compose up -d`, which creates the container if it doesn't exist
named `$NAME` already exists (in that case none of the mount setup below is and (re)starts it otherwise, per `docker-compose.yml`. Container/image name,
re-evaluated; use `remove` first to pick up mount changes). 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.
On the host side it first seeds, without overwriting anything that already Before calling compose, `start` seeds the host side, without overwriting
exists: anything that already exists:
- `$DATA_DIR/home/.bashrc` and `$DATA_DIR/home/bash_aliases` — copied from - `$DATA_DIR/home/.bashrc` and `$DATA_DIR/home/bash_aliases` — copied from
`files/bashrc` and `files/bash_aliases`. `files/bashrc` and `files/bash_aliases`.
- `$DATA_DIR/claude/credentials.json` and `$DATA_DIR/claude/claude.json` - `$DATA_DIR/claude/credentials.json`, `$DATA_DIR/claude/claude.json`, and
created empty. Seeding them as files matters: if Docker had to create the `$DATA_DIR/opencode/auth.json` created empty. Seeding them as files
mount targets itself it would make directories, breaking the login mounts. 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.
Then it runs the container with: The mounts, all declared in `docker-compose.yml`:
| Mount / flag | Purpose | | Mount | Purpose |
| --- | --- | | --- | --- |
| `--name "$NAME" --hostname "$NAME"` | Prompt reads `root@$NAME` instead of a random container ID. | | `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/.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/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/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/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. | | `$WORKSPACE_DIR``/projects` | Your project workspace. |
| `$DATA_DIR/ssh``/root/.ssh` (read-only, only if the host dir exists) | Opt-in SSH access to other machines. | | `$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 The login files are individual file mounts rather than a directory volume on
purpose: Claude Code saves them atomically (write a temp file, then purpose: both Claude Code and OpenCode likely save them atomically (write a
`rename()` over the target), and `rename()` onto a symlink replaces the temp file, then `rename()` over the target), and `rename()` onto a symlink
symlink instead of writing through it — which would silently break replaces the symlink instead of writing through it — which would silently
persistence. A bind mount doesn't have that failure mode. break persistence. A bind mount doesn't have that failure mode.
## `connect` ## `connect`
@@ -77,20 +90,21 @@ persistence. A bind mount doesn't have that failure mode.
sudo ./claudaris connect sudo ./claudaris connect
``` ```
Runs `docker start "$NAME"` (a no-op if already running) and then Runs `docker compose up -d` (a no-op if already running) and then
`docker exec -it "$NAME" tmux`. The in-container `tmux` is a wrapper that `docker compose exec claudaris bash`.
attaches to the `main` session if it exists and creates it otherwise, so
`connect` always lands you in the same session.
The explicit `docker start` matters because the container stops itself: the The explicit `up -d` matters because the container stops itself: the
entrypoint creates the `main` tmux session and exits once that session ends entrypoint creates the `main` tmux session and exits once that session ends
(tmux defaults — a window closes when its shell exits, the session closes (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 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. 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`) ## `remove` (aliases: `stop`, `rm`)
Stops (ignoring errors if already stopped) and removes the container, so the Runs `docker compose down`, stopping and removing the container so the next
next `start` recreates it fresh — needed after a `build` to actually run the `start` recreates it fresh — needed after a `build` to actually run the new
new image, or after changing mounts. Nothing under `$DATA_DIR` or image, or after changing mounts. Nothing under `$DATA_DIR` or
`$WORKSPACE_DIR` is touched. `$WORKSPACE_DIR` is touched.
-1
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() {
+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>