Cluster runners deployment guide
π° Camelot AI runs every agent CLI inside an isolated container scheduled by Docker Swarm. This guide covers what an operator needs to set up.
TL;DR
For a hosted CapRover deployment:
- Run
tecnativa/docker-socket-proxyas a Swarm service on the manager, exposing onlySERVICES,TASKS,NETWORKS,NODES,SECRETS. - Label worker nodes with
camelot-home=node-Xfor every X you want to host user runner containers on. - Set π° Camelot AI's env vars (CapRover app config):
RUNNER_BACKEND=swarmDOCKER_HOST=tcp://docker-socket-proxy:2375ENCRYPTION_KEY=<32-byte base64>RUNNER_GLOBAL_MAX=20(or whatever your cluster can handle)RUNNER_PER_USER_MAX=2(default tier)RUNNER_NETWORKSβ optional; defaults toauto(see Runner networking). Setnoneto keep runners isolated.
- Build and push the runner images
(
.github/workflows/runner-images.yml). Reference them fromAgent.runner_image(the Agent CLI's field). A single project can pin a different image via itsrunner_image_overridefield (set in the project's edit form) β when non-nil, it wins over the Agent CLI'sAgent.runner_imagefor every task in that project. - For each user, in π° Camelot AI's profile UI:
- Add their API keys / GitHub PAT as Credentials.
SecretSyncwill populatecamelot_user_<id>_<kind>Swarm secrets automatically.
- Add their API keys / GitHub PAT as Credentials.
- Optionally pin runners to a node label (see Node labels below).
Deployment topologies
π° Camelot AI does not require running on a manager. Three supported shapes:
A. Local socket (smallest installs)
manager + camelot
/var/run/docker.sock βbind-mountββΊ camelot
π° Camelot AI scheduled onto a manager, the manager's socket mounted in.
DOCKER_HOST=unix:///var/run/docker.sock. Simplest, but π° Camelot AI can
only ever run on a manager.
B. Remote TCP + TLS (max flexibility)
manager camelot (anywhere)
dockerd ββtcp/2376ββΊ REQ
(mTLS)
Daemon configured with tcp://0.0.0.0:2376 + client cert auth.
π° Camelot AI env: DOCKER_HOST=tcp://<manager>:2376 plus DOCKER_CERT_PATH
mounted as a Swarm secret. Lets π° Camelot AI live anywhere reachable.
C. Socket proxy as a Swarm service (recommended)
manager (proxy) camelot (anywhere)
socket βββΊ docker-socket-proxy ββtcp/2375ββΊ Camelot
Proxy service constrained to managers; π° Camelot AI reaches it via the in-cluster overlay network. Survives π° Camelot AI getting rescheduled to a worker, restricts the API surface, and Swarm handles proxy failover.
Example proxy service:
docker service create \
--name docker-socket-proxy \
--network camelot \
--constraint node.role==manager \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
--env SERVICES=1 --env TASKS=1 --env NODES=1 \
--env NETWORKS=1 --env SECRETS=1 \
tecnativa/docker-socket-proxy:latest
π° Camelot AI env: DOCKER_HOST=tcp://docker-socket-proxy:2375.
Node labels
Runners can be pinned to a node so a user's profile volume stays local (no NFS required). Label each candidate worker:
docker node update --label-add camelot-home=node-a worker-1
docker node update --label-add camelot-home=node-b worker-2
Then set the same label string as a pin, at whichever level fits
(admin-only, in π° Camelot AI's UI). Each pin control shows a dropdown of
labels discovered live from GET /nodes when the Docker/Swarm API
is reachable, falling back to a free-text field otherwise (e.g. a
non-Swarm RUNNER_BACKEND, or no node labelled yet):
- Project β
/projects/:id, applies to that project only. - User β
/admin/users, applies to all of that user's projects. - Instance default β
/admin/settings, used when neither of the above is set.
Precedence when more than one applies to a runner: project pin > project owner's user pin > instance default. Changing a pin does not restart already-running containers β it takes effect for runners started or adopted from that point forward.
Pool capacity
| Env var | Default | What it controls |
|---|---|---|
RUNNER_GLOBAL_MAX |
20 | Cluster-wide concurrent runner count. |
RUNNER_PER_USER_MAX |
2 | Free-tier cap per user. Override per user via RunnerPool.set_user_cap/2. |
Sessions never get refused β they queue. Wait time surfaces in the UI; a future paid tier will let users buy higher per-user caps.
Recovering interrupted runs
A run cut short by infrastructure β a deploy replacing the runner container, a Swarm service that vanished β is re-queued, not errored: the every-minute dispatcher runs the task again from its current stage.
| Env var | Default | What it controls |
|---|---|---|
RUNNER_MAX_INTERRUPT_REQUEUES |
3 | Consecutive automatic re-queues before a task is errored instead. Reset on any forward progress, so only a task that can never run exhausts it. |
RUNNER_REDEPLOY_WAIT_MS |
60000 | How long a force-redeployed service has to produce a runnable replica before its runner is treated as lost. Raise it if your runner images are large and cold pulls are slow. |
Re-running a task reuses its service name, and Swarm will not delete a
secret that any service still references. Because a secret can only be
rotated by deleting and recreating it, π° Camelot AI removes the previous
run's service before refreshing the task's secrets β otherwise the new
runner would mount the old GitHub App installation token, which expires
an hour after it is minted and shows up as
remote: Invalid username or token from git clone at container boot.
If you delete runner services by hand, delete the matching
camelot_task_<task-id>_gh_token secret too.
Runner networking
A task-runner container joins the Swarm bridge network for outbound
internet, but Swarm's service-discovery DNS (e.g. a CapRover
srv-captain--db) only resolves between containers on the same
overlay. A runner that runs the project's test suite against a shared
database β DATABASE_URL=ecto://β¦@srv-captain--db:5432/β¦ β needs to be
on that overlay, or it fails with "Name or service not known" /
nxdomain.
RUNNER_NETWORKS controls which overlays runners join.
| Value | What happens |
|---|---|
(unset) / auto |
Default. π° Camelot AI copies the overlay network(s) its own service is on onto each runner. |
net-a,net-b |
Explicit network names/IDs. Combine with auto (auto,net-a) to add to the discovered set. |
none |
Keep runners isolated β bridge only, no overlay. |
auto (the default) β a runner reaches exactly what π° Camelot AI
reaches, with nothing to hardcode. Discovery reads the app's own task and
service via the Docker API (TASKS + SERVICES, already in the
socket-proxy allow-list) and memoizes the result. If it can't complete
(e.g. plain-Docker / non-Swarm self-hosting, where there is no overlay to
discover), runners start with no extra network and a warning is logged β
never a hard failure.
Security note: auto places runners on the same overlay as π° Camelot AI,
so they can reach the control-plane services on it (including π° Camelot AI's
own DB). Set RUNNER_NETWORKS=none β or an explicit, dedicated overlay β
if you need runners isolated from the control plane.
Backups & disaster recovery
- DB is the source of truth. Back up PostgreSQL.
- Profile volumes are caches. Losing one only costs the next session's bootstrap time (asdf re-install, CLI caches rebuilt). The entrypoint re-materialises credentials from secrets on every spawn.
- Swarm secrets: rotate via π° Camelot AI UI (or
SecretSync.reconcile/2); no manualdocker secretcalls needed.
Local development
Drop RUNNER_BACKEND=local (the default in dev/test). π° Camelot AI runs
the CLI directly on your host via Port.open β identical to the
pre-runner behaviour. No Docker required.
To test the containerised path on a dev VM without Swarm:
RUNNER_BACKEND=docker. Set up a Docker daemon and bind-mount the
socket (or expose it over TCP).