Run the factory on cloud boxes
By default the workbench factory runs every job on one static ssh box
(WORKBENCH_PROVIDER=ssh). The cloud tier lets each job lease its own fresh box from
your cloud account, so many jobs run in parallel and nothing shares a machine. The gate
model is unchanged - the gate still runs inside the container the target repo declares
(see Prepare a repository); the cloud tier only changes where the box comes from.
Turn it on
The factory reads these from the server's environment (the server runs under
doppler run), and crabbox reads its own knobs from the same environment.
Hetzner (a few cents per job)
export WORKBENCH_PROVIDER=hetzner
export HCLOUD_TOKEN=... # a Hetzner Cloud API token (direct mode)
export WORKBENCH_BOX_CLASS=cpx41 # 8 vCPU / 16 GB / 240 GB disk (a Hetzner server type)
export CRABBOX_HETZNER_LOCATION=ash # a location that actually has the type in stockPin CRABBOX_HETZNER_LOCATION - crabbox's default (fsn1, Germany) does not always have
cpx41 in stock, and a leased-in-a-stockless-location request fails fast with a clean
"unsupported location for server type" (no box is created, no cost) rather than provisioning
anything. ash (Ashburn, US-East) and hil (Hillsboro, US-West) are the US locations;
availability moves, so if a location rejects the type, check the Hetzner API
GET /v1/datacenters (server_types.available) for one that has it.
AWS
export WORKBENCH_PROVIDER=aws
# AWS credentials come from the standard SDK chain (env vars, ~/.aws, instance role) -
# the factory holds no AWS secret of its own.
export CRABBOX_AWS_REGION=us-east-1 # default eu-west-1
export CRABBOX_CAPACITY_MARKET=spot # spot (default) or on-demand
export CRABBOX_AWS_ROOT_GB=100 # root disk in GB (crabbox reads it directly)Daytona
Daytona is a managed sandbox, not a bare VM: you lease a container from a snapshot and reach it
over Daytona's ssh gateway. Size lives on the snapshot, not --class (WORKBENCH_BOX_CLASS is
ignored, with a warning). It runs in either provisioning mode:
- Install at lease time (default). Point at a base snapshot with enough resources; the factory
installs docker/gh and starts the daemon (docker-in-docker, which Daytona supports) on it each
run - no baking needed.
export WORKBENCH_PROVIDER=daytona export CRABBOX_DAYTONA_API_KEY=... export CRABBOX_DAYTONA_SNAPSHOT=daytona-large # a base snapshot sized >= 2 CPU / 4 GB export CRABBOX_DAYTONA_TARGET=us # a Daytona compute region, optional - Pre-baked. Bake a snapshot with the tools already in it, from the shared
factory-box.sh(one source of truth) viadevbox/daytona-snapshot.Dockerfile, and setWORKBENCH_PREBAKED=1so the box boots ready with no per-run install:daytona snapshot create factory-box --dockerfile devbox/daytona-snapshot.Dockerfile \ --cpu 4 --memory 8 --disk 10 export WORKBENCH_PROVIDER=daytona WORKBENCH_PREBAKED=1 export CRABBOX_DAYTONA_API_KEY=... CRABBOX_DAYTONA_SNAPSHOT=factory-box
A default Daytona account caps a sandbox at 4 CPU / 8 GB / 10 GB disk (contact Daytona for more); docker-in-docker wants >= 2 CPU / 4 GB.
Two operator notes for daytona:
- crabbox must be >= 0.41. Older crabbox builds over-request sandbox memory at lease time, which trips the quota on a small Daytona org before any box exists - a failure that reads as an account limit when it is really a CLI version floor.
- Snapshots go inactive with disuse. Daytona deactivates a snapshot that has not been used
for a while, and an inactive snapshot fails lease validation until it is activated again
(
POST /api/snapshots/{id}/activate, or thedaytonaCLI).
ssh stays the default: unset WORKBENCH_PROVIDER (or set it to ssh) and
the factory behaves exactly as before, no lease involved.
Box size
export WORKBENCH_BOX_CLASS=c7a.2xlarge # default: 8 vCPU / 16 GB (a raw AWS instance type)
export CRABBOX_AWS_ROOT_GB=100 # default root disk for that boxThe factory leases an 8 vCPU / 16 GB box by default with a 100 GB root disk - enough to
docker build and run a heavy repo's gate without OOMing or filling the disk, while
staying cheap (a few cents per job on spot). crabbox's own default is beast (a 192-vCPU
machine), which the factory overrides.
WORKBENCH_BOX_CLASS is passed to crabbox as --class, which on both cloud providers
accepts a raw provider type name:
- aws: a raw EC2 instance type (the named ladder -
standard= c7a.8xlarge,beast= c7a.48xlarge - jumps fromsmall(t3.small) straight to 32 vCPU, so the default pins the exactc7a.2xlarge). Disk is a separate knob (CRABBOX_AWS_ROOT_GB). - hetzner: a Hetzner server-type name (
cpx41= 8 vCPU / 16 GB / 240 GB). Disk is bundled with the type - there is no separate disk knob. crabbox'sbeastdefault is not a valid Hetzner type, so a missing class errors rather than making a giant box.
Raise it (c7a.4xlarge / cpx51 / a named class) for a repo whose build or tests need
more memory or CPU.
Provisioning mode
By default every cloud box installs the factory's tools at lease time (factory-box.sh), but a
per-run install is wasted work at high volume. Set WORKBENCH_PREBAKED=1
to boot from a pre-baked image that already carries the tools and skip the install. It applies to
any provider given a matching baked image (CRABBOX_AWS_AMI on aws, a baked
CRABBOX_DAYTONA_SNAPSHOT on daytona); pointed at an unprepared image it fails fast at the gate's
docker check. ssh is unaffected (its static box is prepared out of band).
The lease lifecycle
workbench_cloneleases a box (crabbox warmup) under a slug derived from the run id:wb-<hash(run_id)>. Because the slug is derived, every step of the run targets the same box, and a retried clone re-derives the same slug rather than leasing a second box.workbench_clonethen provisions the fresh box before the clone lands: it runs the samedevbox/factory-box.shthat prepares the static ssh box (installing docker, git, gh, and the coding agent, and creating the workbench workdir), so a bare cloud image becomes a ready factory box. Provisioning is bounded (a wedged install times out) and, if it fails, the box is released so the clone retry starts on a clean box. A cloud provider image that blocks egress toget.docker.com/cli.github.com/claude.aifails here with a legible error rather than later in the gate.workbench_attemptandworkbench_submitreuse that box (crabbox run --id <slug>).workbench_submitreleases the box (crabbox stop --id <slug>) once the PR is opened.- The clone output surfaces the box -
box: { slug, provider, id }- so every run's box is visible in its run doc, and submit reportsbox_released.
Cost and safety
- Bounded on a crash. A run that errors before submit never releases its box, but
crabbox's idle-timeout (
CRABBOX_IDLE_TIMEOUT, default 30m) and TTL (CRABBOX_TTL, default 90m) terminate an idle box on their own. Worst case is roughly 30 minutes of one box. Until the timeout fires, that box also holds a provider quota/concurrency slot - on a small Daytona quota enough to block the next lease - so clear stale sandboxes before retrying a failed run. - A capacity/credential failure is legible. If a box cannot be leased (no spot
capacity, a bad token, a down region), the run reports a clean "could not lease a cloud
box: ..." error and the workflow's
clone retry 2gives it a couple more tries before it drafts out - it does not crash or scatter across machines. - Concurrency is isolated. Each run has a distinct id, so a distinct slug, box, workdir, and compose project; one run's box release can never touch another's.
Recovering a leaked box
Every box's slug is wb-<hash(run_id)>, printed in the run's clone output. To reclaim one
by hand:
crabbox stop --provider <hetzner|aws> --id <slug> # stop one box by its slug (the wb-... value)
crabbox cleanup --provider <hetzner|aws> # sweep expired direct-provider boxesNot built (deliberate)
- Per-run region/market/size overrides. Provider selection and sizing are process-env globals today (one operator, one config); a per-run override is a future add.
- An explicit run-error finalizer. A crashed run leans on the idle-timeout floor above rather than eagerly stopping the box; a finalizer is a possible later addition.
- A max-concurrent-lease cap. Twenty queued jobs lease twenty boxes; the blast radius
is operator-driven. A
CRABBOX_MAX_LEASES-style cap is a future knob.
Next
- Make a repo factory-ready - what runs on the box once it exists.
- Run coding work in an isolated factory - the overall run shape.
- - every provider variable.