# Run Loopgate on your own box

The operator's reference. The [step-by-step guides](https://loopgate.dev/runner/) (`guides/`) walk the same path in plainer words, per cloud and per address.

Loopgate's runner is the daemon on a machine in your network, opened by your org's members from the portal and driven from Slack if you like. The image is public and prebuilt; code, diffs, prompts, transcripts and every credential stay on the box.

## 1. The command from the portal

In the portal, **Devices → Add a runner** (an owner or admin) gives one line with a single-use token that lasts an hour. Paste it on a Linux box with Docker Compose v2:

```sh
curl -fsSL https://loopgate.dev/runner/install.sh | sh -s -- lgr_…
```

It checks Docker, writes `~/loopgate/compose.yaml` and `runner.env` (mode 600), starts the runner, which redeems the token, and prints `Runner connected` once the runner is on the Devices page. A runner that is signed out takes a fresh line the same way; the rest of `runner.env` is kept.

The runner has an HTTPS address from its first start: the `address` service is a Cloudflare quick tunnel (no account, no token), the runner reads its `…trycloudflare.com` hostname from the tunnel's metrics port, reports it to the portal and logs `Runner ready at https://… (temporary address)`. It changes when the box restarts, links Slack posted before then stop working, and Cloudflare promises it no uptime. Members open the runner from the portal: **Devices → Open** on its row.

## 2. On the runner's page

Each is an owner's or admin's on a runner; a member is told to ask one.

- **Settings → Engines.** pi ships in the image and is what the copilot thinks on: **Sign in with ChatGPT** or **Sign in with Claude** on its row shows an address and a code. **Install Codex** and **Install Claude Code**, then **Sign in**, add the others at the versions Loopgate tested (Codex into the `data` volume, Claude Code into `home`; both survive restarts and updates). A box several people share runs every task on whoever signed in there; per-person routing is not built.
- **Settings → GitHub.** A fine-grained token (Contents and Pull requests: Read and write, best for a separate bot user), checked with GitHub before it is saved on the box (mode 600), and the name and email commits carry. It clones private repositories, pushes and opens pull requests. `GH_TOKEN` and the `GIT_` lines in `runner.env` win over it.
- **Add a workspace.** A repository's address and **Clone**, or a name and **Create** for an empty one, under `/repos` in the `repos` volume.
- **Settings → Slack** ([guide](guides/slack.md)). A worker beside the runner answers one Slack workspace over Socket Mode, so Slack needs no inbound port; it always runs and is idle until connected. **Create the app in Slack** from the page, paste the `xapp-` token (`connections:write`) and the `xoxb-` bot token (each checked with Slack), check the members matched by email, choose the workspace tasks work in and the ceiling in dollars and hours, then **Connect**. The page reads `Connected to <workspace>` or the worker's error in one sentence.

A task starts from Slack's Home, `/loopgate <task>` or `@Loopgate`; one card shows what will run, with Start, Customize and Change…, and becomes the progress card. Gates, replies, Stop and Retry happen in the thread; several questions at once, a whole document, raising a budget after a stop, and Adjust and retry open in the workbench. Only the thread's starter is heard, every task runs with Full access unless Customize lowers it, and no run opens a pull request on its own.

## 3. A permanent address

Put one of these in front of the runner, then add `LOOPGATE_RUNNER_URL=https://…` to `runner.env` (it wins over the quick tunnel) and `docker compose up -d runner`:

- **Cloudflare Tunnel** (first choice; [guide](guides/address-cloudflare.md)): public, with the runner's own door (the portal's Open pass) as the auth, and no inbound port. Put the tunnel's token in `tunnel.env` (from `tunnel.env.example`), start the `tunnel` profile (`COMPOSE_PROFILES=tunnel`) and point a public hostname at `http://runner:4311`.
- **Tailscale Serve** ([guide](guides/address-tailscale.md)): enable HTTPS certificates once in the tailnet's DNS settings, then `tailscale serve --bg 4311` on the box. Reachable only from the tailnet.
- **Your own proxy**, Caddy for example: `loopgate.internal.example { reverse_proxy 127.0.0.1:4311 }`, passing the Host header through.
- **An SSH tunnel** (last resort): `ssh -L 4311:127.0.0.1:4311 <box>` works for one person, but moved the 1.5 MB workbench script at 3.5 KB/s when we measured it.

## Bounds

The runner reports every minute. Revoke it in the portal, or `docker compose exec runner loopgate logout`, and within a minute no member can reach it; runs in flight continue until they need a person or end, within their budgets. `docker compose exec runner loopgate cancel <run>` stops a run at once. A member removed from the org loses access within about two minutes. A browser's sign-in lasts thirty days from its last use and survives a restart, and a page with no sign-in offers **Sign in**, which goes through the portal and comes back to the same page. A restart marks steps in flight as interrupted, with Retry.

## Update, rollback, backup

Each release is a new `compose.yaml` pinning the image by digest; the harness layers are shared between releases, so an update pulls only the app, about 14 MB.

```sh
cp compose.yaml compose.previous.yaml                     # update
curl -fsSLO https://loopgate.dev/runner/compose.yaml
docker compose pull && docker compose up -d

cp compose.previous.yaml compose.yaml                     # rollback
docker compose up -d

docker compose stop                                       # backup: all three volumes
docker run --rm -v loopgate-runner_data:/data -v loopgate-runner_repos:/repos -v loopgate-runner_home:/home/node -v "$PWD":/backup debian:bookworm-slim tar czf /backup/loopgate-runner.tgz /data /repos /home/node
docker compose start
```

## What the box needs

Any Linux machine with Docker Compose v2, `linux/amd64`. Inference is remote, so CPU is light. Measured on 2026-10-10 with `docker stats`, the image emulated as amd64 on an Apple Mac; a native box uses less.

| Piece                                   | Size                                                              |
| --------------------------------------- | ----------------------------------------------------------------- |
| The image (Node 22, Git, gh, pi 0.85.1) | 566 MB on disk, 169 MB to pull (the app layers on top: 14 MB)     |
| Codex 0.154.0, if you install it        | 251 MB, in the `data` volume                                      |
| Claude Code 2.1.274, if you install it  | 220 MB, in the `home` volume                                      |
| The runner idle                         | 145 MB memory                                                     |
| The Slack worker idle                   | 70 MB memory                                                      |
| Each harness session while a step works | 200–500 MB memory                                                 |
| **Free memory**                         | **about 2 GB** for the runner, the worker and one or two sessions |
| **Disk**                                | **5 GB**, plus your repositories                                  |
| **Network**                             | outbound only: model providers, GitHub, Slack, the control plane  |

## What leaves the box

Your model providers get what any harness sends them, GitHub gets your pushes and pull requests, and Slack gets the thread's posts. Loopgate's control plane gets a heartbeat (the runner's URL and version), member and policy reads, and audit as metadata when a run ends. It never receives code, diffs, prompts or transcripts.

## By hand

[By hand](guides/by-hand.md) is every step above from files and a terminal: the three files, `runner.env` line by line, `loopgate login` with a device code, engines from the terminal, a permanent address with its token, and `slack.env`, which overrides the page. A line in `runner.env` or `slack.env` always wins over the page.
