Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 38 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,26 @@ cd <example> && rig deploy
| [`url-shortener/`](./url-shortener/) | Python · Django | the **full validated param set** (url/string/number/boolean/select/email/secret/textarea) + SQLite migrations |
| [`markdown-notes/`](./markdown-notes/) | Python · Flask | **workspace volume-backed SQLite persistence** + Markdown rendering |

Every example deploys with the same command — `rig deploy`. Most rsync code and
run `install:` on the VM. Several declare a `Dockerfile`, which makes
`rig deploy` **freeze the environment into an image** and boot from it (see
**Docker builds & the hybrid deploy** below). No flag: the Dockerfile is the
signal.
Every example deploys with the same command — `rig deploy`. All of them rsync
code and describe their environment with `install:`. Several also set
`reproducible: true`, which makes `rig deploy` **freeze the result of `install:`
into an image** and boot from it (see **Reproducible deploys & the hybrid model**
below). No Dockerfile anywhere — the flag is the signal.

### Established products, run reproducibly

These run real, recognizable self-hosted products on Rigbox via a
`FROM rigbox-base` Dockerfile that installs the product on top of the base. The
image freezes the install; `rig deploy` builds it once, then reuses it. (Upstream
images like `postgres:16` can't be booted directly — they lack the rigbox agent +
init — so each installs the product on the rigbox base instead.)
`reproducible: true` `install:` script that installs the product on top of the
Rigbox base. `rig deploy` runs the script once in a builder VM, freezes the
result as an image, then reuses it. (Upstream Docker images like `postgres:16`
can't be booted — they lack the rigbox agent + init — so each installs the
product on the rigbox base instead.)

| Example | Product | What it shows |
|---|---|---|
| [`code-server/`](./code-server/) | **code-server** (VS Code) | run an established product via a reproducible Dockerfile; settings/extensions persist under `$DATA_DIR` |
| [`code-server/`](./code-server/) | **code-server** (VS Code) | run an established product via a reproducible `install:`; settings/extensions persist under `$DATA_DIR` |
| [`gitea/`](./gitea/) | **Gitea** (Git hosting) | a headless single-binary service (install wizard locked) with SQLite + repos under `$DATA_DIR` |
| [`n8n/`](./n8n/) | **n8n** (workflow automation) | freeze a heavy `npm install` into the image (`sizeMb` bump); workflows persist under `$DATA_DIR` |
| [`n8n/`](./n8n/) | **n8n** (workflow automation) | freeze a heavy `npm install` into the image; workflows persist under `$DATA_DIR` |

## Catalog apps

Expand Down Expand Up @@ -86,23 +87,36 @@ The point of the suite is to model the *right* primitive for each job:
`{ emails: [...] }`) so a redeploy keeps it — only an app's front door is public;
siblings reach private apps over loopback via `dependsOn`.

## Docker builds & the hybrid deploy
## Reproducible deploys & the hybrid model

Most examples install their runtime on the VM with `install:`. The established
products — **`code-server`**, **`gitea`**, and **`n8n`** — instead **freeze their
environment into an image** with a `Dockerfile` (`FROM rigbox-base`). The command
is the same — `rig deploy` — and a Dockerfile in `rig.yaml` is all it takes to
switch on the image build (no flag):
Every example installs its runtime with `install:`. By default that script runs
on the workspace VM on each deploy. The established products —
**`code-server`**, **`gitea`**, **`n8n`**, and every [`catalog-apps/`](./catalog-apps/)
example — add one line, `reproducible: true`, which makes the same `install:`
**freeze into an image** instead. The command is the same — `rig deploy`:

- the **first** deploy builds the image from the local Dockerfile (the CLI uploads
the project dir as the build context — no git repo needed), boots from that
frozen image, and rsyncs the code;
- **later** deploys reuse the cached image when the Dockerfile/deps are unchanged
and **only rsync the changed code** — no rebuild, no re-install.
- the **first** deploy boots a throwaway builder VM from the `base` image, runs
`install:` inside it, snapshots the rootfs as a content-addressed image, boots
the workspace from that frozen image, and rsyncs the code;
- **later** deploys reuse the cached image when the build inputs (`install:`
script, base image, lockfiles) are unchanged and **only rsync the changed
code** — no rebuild, no re-install.

That's the hybrid: build the slow, stable environment once; ride fast-changing
code over it with rsync. See [`design/CONTRACT.md`](./design/CONTRACT.md) →
*Reproducible builds* for the full rules and when to pick which.
code over it with rsync. `install:` runs as `developer` (with passwordless
`sudo`) in an **empty** deploy dir inside the builder — so it must be
self-contained (inline any config it needs via heredocs) and idempotent, since
the exact same script runs on the workspace VM when `reproducible` is off.
Runtime wrappers (`start.sh`) still rsync in with the code: `start: bash start.sh`.

> **Builder sizing.** The builder VM boots with 1GB RAM / 1 vCPU and inherits
> the app's `workspace.resources.diskSizeMb` (3GB default, 16GB ceiling), so a
> heavy install just needs that value set high enough to hold it. The heavier
> examples (`n8n`, `firecrawl`, `open-webui`, `hermes-agent`, `excalidraw`) size
> themselves in `rig.yaml` and note their footprint in their README.

See [`design/CONTRACT.md`](./design/CONTRACT.md) → *Reproducible builds* for the
full rules and when to pick which.

## Layout convention

Expand Down
6 changes: 3 additions & 3 deletions catalog-apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Standalone reproductions of every app in the [Rigbox catalog](https://docs.rigbox.dev/) — same product the catalog installs, but shipped as a regular `rig deploy`-able example you can fork, modify, and run on its own.

These live in their own subtree so they don't crowd the [curated example suite](../README.md) at the repo root (todo-app, quickstart, ai-chat, bluegreen-blog, …). Each catalog app uses the same `FROM rigbox-base` Dockerfile pattern as `gitea/`, `n8n/`, `code-server/`.
These live in their own subtree so they don't crowd the [curated example suite](../README.md) at the repo root (todo-app, quickstart, ai-chat, bluegreen-blog, …). Each catalog app uses the same `reproducible: true` + `install:` pattern as `gitea/`, `n8n/`, `code-server/` — the install script is frozen into an image once and reused by later deploys.

## Service apps — [`service/`](./service/)

Expand All @@ -17,7 +17,7 @@ Apps that expose a port + health probe, accessed in a browser at the workspace's
| [`service/excalidraw/`](./service/excalidraw/) | **Excalidraw** | virtual whiteboard, vite-built static SPA frozen into the image |
| [`service/filebrowser/`](./service/filebrowser/) | **File Browser** | pinned single Go binary serving `$DATA_DIR` over a web UI |
| [`service/openterminal/`](./service/openterminal/) | **Open Terminal** | sandboxed REST API for shell + filesystem, designed for AI agents; key materialised on boot |
| [`service/firecrawl/`](./service/firecrawl/) | **Firecrawl** | self-hosted web-scraping API — Postgres-17 + Redis + RabbitMQ + Chromium frozen in a 12GB ext4 image |
| [`service/firecrawl/`](./service/firecrawl/) | **Firecrawl** | self-hosted web-scraping API — Postgres-17 + Redis + RabbitMQ + Chromium frozen into one reproducible image |
| [`service/hermes-agent/`](./service/hermes-agent/) | **Hermes Agent** | self-improving agent + a separate user-bus messaging gateway bridging Telegram/Discord/Slack/WhatsApp/Signal |
| [`service/t3code/`](./service/t3code/) | **T3 Code** | multiple browser-tabbed coding agents (Claude Code, Codex, …) behind one web UI |
| [`service/open-webui/`](./service/open-webui/) | **Open WebUI** | full-featured chat UI fronted by the Rigbox managed AI proxy — every chat metered against workspace credits |
Expand All @@ -33,7 +33,7 @@ AI coding CLIs/TUIs accessed by SSHing into the workspace. Each declares `kind:
| [`cli/codex/`](./cli/codex/) | **Codex CLI** | OpenAI's Codex CLI with one-rename `OPENROUTER_* → OPENAI_*` shim (OpenRouter is OpenAI-compatible at `/v1`) |
| [`cli/opencode/`](./cli/opencode/) | **OpenCode** | OSS Go-based TUI from sst/opencode with **native** `OPENROUTER_API_KEY` (no translation shim) |
| [`cli/junie/`](./cli/junie/) | **Junie** | JetBrains' AI agent with a heavy postinstall archive frozen into the image once |
| [`cli/kilocode/`](./cli/kilocode/) | **Kilo Code** | multi-provider CLI (100+ providers) pinned at image-build time to OpenRouter |
| [`cli/kilocode/`](./cli/kilocode/) | **Kilo Code** | multi-provider CLI (100+ providers) pinned at install time to the managed AI proxy |
| [`cli/pi/`](./cli/pi/) | **Pi** | OpenRouter-native CLI coding agent (`@mariozechner/pi-coding-agent`); SSH in and run `pi` |

## Deploy
Expand Down
30 changes: 0 additions & 30 deletions catalog-apps/cli/claude/Dockerfile

This file was deleted.

42 changes: 22 additions & 20 deletions catalog-apps/cli/claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,29 @@ codebase from a single terminal session, with model routing through Rigbox's
**managed AI proxy** — no API key to set, so deploying is never blocked on a
local secret.

## The single capability: a long-lived AI-agent workspace, baked into an image
## The single capability: a long-lived AI-agent workspace, frozen into an image

The whole point here is running an **AI coding CLI on a persistent VM** instead
of locally — your repo, history, and `.claude/` config survive across sessions
and across deploys. The `Dockerfile` is `FROM rigbox-base` (the required base —
the platform asserts the rigbox agent + systemd are present and rejects any
other base at build time) and bakes the upstream installer's binary into the
image once:

```dockerfile
FROM rigbox-base
RUN su - developer -s /bin/bash -c '...curl -fsSL https://claude.ai/install.sh | bash...'
RUN ln -sfn /home/developer/.local/bin/claude /usr/local/bin/claude
```

`rig.yaml` points at it with a `build:` block (no `install:`):
and across deploys. `rig.yaml` sets `reproducible: true`, so `rig deploy` runs
the `install:` script once in a builder VM, freezes the result as an image, and
later deploys boot from it instead of re-running the upstream installer:

```yaml
build:
dockerfile: Dockerfile
reproducible: true
install: |
set -euo pipefail
curl -fsSL https://claude.ai/install.sh | bash # → ~/.local/bin/claude
sudo ln -sfn "$HOME/.local/bin/claude" /usr/local/bin/claude
sudo tee /etc/profile.d/claude-routing.sh <<'EOF'
… # managed-AI routing, below
EOF
```

No Dockerfile — `install:` is the same script a plain deploy would run on the
VM (as `developer`, with passwordless `sudo` for the system-path steps);
`reproducible: true` is what makes `rig deploy` freeze its result.

## SSH-in to use it

Claude Code is a TUI — there is **no web UI**. The app is declared with
Expand All @@ -40,7 +41,7 @@ ssh "$(rig workspace ssh-info --workspace <name-or-id> --output json | jq -r .ss
claude
```

The onboarding wizard is pre-accepted at image-build time
The onboarding wizard is pre-accepted at install time
(`~/.claude.json` + `~/.claude/settings.json`), so `claude` drops you straight
into a session — no first-launch prompts.

Expand All @@ -54,11 +55,12 @@ ai:
```

Claude Code reads `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`. The managed
proxy serves the Anthropic `/v1/messages` shape, and the image's
`/etc/profile.d/claude-routing.sh` points Claude at it on every shell start:
proxy serves the Anthropic `/v1/messages` shape, and the
`/etc/profile.d/claude-routing.sh` that `install:` writes points Claude at it on
every shell start:

```sh
# baked into the image, sourced by every login shell
# written by install:, sourced by every login shell
. ~/.rigbox/proxy.env # OPENAI_BASE_URL=<proxy>/v1, OPENAI_API_KEY=<placeholder>
export ANTHROPIC_BASE_URL="${OPENAI_BASE_URL%/v1}" # Claude appends /v1/messages itself
export ANTHROPIC_AUTH_TOKEN="${OPENAI_API_KEY}"
Expand Down Expand Up @@ -87,4 +89,4 @@ default).
- **No public UI.** `kind: cli` means there's no HTTP front door at all — the
workspace is reachable only via SSH on the rigbox gateway.
- **Onboarding bypass.** `hasCompletedOnboarding` + `bypassPermissionsModeAccepted`
are written at image-build time so `claude` is non-interactive on first launch.
are written by `install:` so `claude` is non-interactive on first launch.
31 changes: 0 additions & 31 deletions catalog-apps/cli/claude/claude-routing.sh

This file was deleted.

62 changes: 59 additions & 3 deletions catalog-apps/cli/claude/rig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,67 @@ kind: cli
# the agent has comfortable headroom for repo-wide edits and node_modules.
workspace:
resources: { ramMb: 1024, vcpuCount: 1, diskSizeMb: 3072 }
build:
dockerfile: Dockerfile
# Bake the CLI into an image: `rig deploy` runs `install:` once in a builder VM
# and later deploys boot from the cached image instead of re-running the
# upstream installer. The script runs as `developer` with passwordless sudo.
reproducible: true
install: |
set -euo pipefail
# 1. Install the CLI as the developer user (no secrets baked in; the installer
# fetches the latest published version). It drops the binary at
# ~/.local/bin/claude; symlink to /usr/local/bin so it's on the standard
# SSH PATH (login shells, ForceCommand, scripted execs). Pre-accept the
# onboarding wizard so the first `claude` is non-interactive — mirrors the
# catalog's claude/install.sh so both shapes stay byte-compatible.
curl -fsSL https://claude.ai/install.sh | bash
test -x "$HOME/.local/bin/claude"
mkdir -p "$HOME/.claude"
printf '%s\n' '{"hasCompletedOnboarding":true,"bypassPermissionsModeAccepted":true}' > "$HOME/.claude.json"
printf '%s\n' '{"theme":"dark","permissions":{"defaultMode":"bypassPermissions","dangerouslySkipPermissions":true}}' > "$HOME/.claude/settings.json"
sudo ln -sfn "$HOME/.local/bin/claude" /usr/local/bin/claude

# 2. Wire Claude Code to an AI backend at every interactive login shell. The
# routing script points ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN at the
# workspace's managed AI proxy (Anthropic /v1/messages shape) by default,
# or at OpenRouter if the operator brings their own OPENROUTER_API_KEY.
# SSHing in is enough to activate it — no per-session `export` dance.
sudo tee /etc/profile.d/claude-routing.sh >/dev/null <<'EOF'
# Managed by rigbox-examples (claude). Wires Claude Code to an AI backend at
# every interactive login shell, so SSHing in is enough to activate routing.
# Claude Code reads ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN.
case ":${PATH}:" in
*":${HOME:-/home/developer}/.local/bin:"*) ;;
*) export PATH="${HOME:-/home/developer}/.local/bin:${PATH}" ;;
esac
if [ -n "${OPENROUTER_API_KEY:-}" ]; then
# Bring-your-own-key: route straight at OpenRouter (Anthropic /v1/messages
# shape). ANTHROPIC_BASE_URL must omit the trailing /v1 — Claude Code appends
# /v1/messages itself.
_rb_base="${OPENROUTER_BASE_URL:-https://openrouter.ai/api}"
_rb_base="${_rb_base%/v1}"
export ANTHROPIC_BASE_URL="${_rb_base%/}"
export ANTHROPIC_AUTH_TOKEN="${OPENROUTER_API_KEY}"
unset _rb_base
elif [ -r "${HOME:-/home/developer}/.rigbox/proxy.env" ]; then
# Managed AI: every managed workspace gets ~/.rigbox/proxy.env exporting
# OPENAI_BASE_URL=<proxy>/v1 and OPENAI_API_KEY=<placeholder>. Strip the /v1
# to get the proxy root Claude Code's Anthropic client expects.
. "${HOME:-/home/developer}/.rigbox/proxy.env"
export ANTHROPIC_BASE_URL="${OPENAI_BASE_URL%/v1}"
export ANTHROPIC_AUTH_TOKEN="${OPENAI_API_KEY}"
fi
if [ -n "${ANTHROPIC_AUTH_TOKEN:-}" ]; then
# When both AUTH_TOKEN and API_KEY are set Claude Code prefers API_KEY, which
# would silently bypass our routing — clear it explicitly.
export ANTHROPIC_API_KEY=""
fi
export CLAUDE_CODE_SKIP_ONBOARDING=1
export CLAUDE_CODE_ENABLE_TELEMETRY=0
EOF
sudo chmod 644 /etc/profile.d/claude-routing.sh
# Opt into the Rigbox managed AI proxy — no OpenRouter key to set, so deploying
# this example is never blocked on a local secret. Claude Code reads
# ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN; the image's
# ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN; the baked
# /etc/profile.d/claude-routing.sh points those at the workspace's managed
# proxy (which serves the Anthropic /v1/messages shape) at every shell start,
# so an SSH session sees `claude` wired up. Bring-your-own-key still works:
Expand Down
Loading