Why use a sandbox
- A clean machine for each job: no cache, file, or process from an earlier job can change the result.
- Isolation for untrusted code: code from a pull request runs in a microVM, away from your runners and their secrets.
- Parallel jobs: start one sandbox per job or per shard, up to your team’s active sandbox limit.
- Fast starts: bake dependencies into a template, or fork a prepared sandbox from a snapshot.
How it works
1
Create a sandbox
Create it from a template that has your toolchain, with a timeout as a safety net.
2
Send the code in
Clone the repository in the sandbox, or send the runner’s checkout in with
tar.3
Run the build and tests
superserve exec returns the command’s exit code, so the CI step fails when a test fails.4
Delete the sandbox
Delete it when the job ends, also when the job fails.
One sandbox per job
A job has three steps: create a sandbox, run commands in it withsuperserve exec, and delete it.
superserve execexits with the command’s own exit code, so a failing test fails the CI step. stdout and stderr stay separate.- A single argument after
--runs as a shell command, so&&andcdwork. - Set
--timeouton everyexecthat runs a build or tests. A command that runs longer stops, andexecexits with124. --timeout 30m --auto-delete 0oncreateis a safety net. If the job dies before the delete step, the sandbox auto-pauses after 30 minutes and then auto-deletes at once. Make the--timeoutlonger than your slowest job.
Sandbox names are not unique. If two sandboxes have the same name,
exec and delete fail with 2 sandboxes are named "..."; use an ID instead. Give each job a name that includes a unique run ID, as the workflow below does.Send the CI checkout to the sandbox
A CI runner usually checks out the code already, sometimes with changes that are not on a remote branch. Send that checkout into the sandbox through stdin withexec -i, instead of cloning again:
--exclude=.git keeps the upload small. Leave it out if your build needs the git history.
Env vars and secrets
Pass plain configuration with-e, on create for every command or on one exec:
-s on create. The sandbox gets a stand-in token, not the real value, and Superserve attaches the real value only to requests to the secret’s hosts:
GitHub Actions
This workflow runs a Node.js project’s tests in a sandbox on every push and pull request.- Create an API key in the console.
- Add the key as a repository secret named
SUPERSERVE_API_KEY(Settings → Secrets and variables → Actions). - Add the workflow file:
.github/workflows/test.yml
- The CLI reads
SUPERSERVE_API_KEYfrom the environment, so no login step is needed. github.run_attemptkeeps the name unique when you re-run a job.if: always()deletes the sandbox even when the tests fail.- The CLI needs Node.js 22.4 or later, so the workflow sets up Node.js 22 on the runner.
- GitHub doesn’t pass repository secrets to
pull_requestworkflows from forks, so those runs can’t create sandboxes. See Run untrusted code before you change that.
Start faster
Installing system packages and dependencies on every job is slow. Do the slow setup once and reuse it.Custom templates
Bake the tools the job needs into a template, then create each job’s sandbox from it:Snapshots and forks
A snapshot saves a sandbox’s memory and disk, including installed dependencies. Install dependencies once per workflow run, take a snapshot, and fork a sandbox from it for each job:/w and node_modules in place. --snapshot takes the snapshot ID, not its name. Delete the snapshot when the run ends: superserve snapshot delete "$SNAPSHOT" --yes.
Shard a test suite
To split a slow suite, run each shard in its own sandbox. This workflow installs dependencies once, forks three sandboxes from a snapshot, and deletes the snapshot at the end. It uses Node.js’s built-in--test-shard; use your test runner’s own shard option.
.github/workflows/test-sharded.yml
sandbox create fails with too_many_sandboxes. Set strategy.max-parallel to stay under the limit when several runs can overlap. Snapshots have their own limits, so delete them when the run ends, as the cleanup job does.
To retry a failed run of this workflow, use Re-run all jobs. Re-run failed jobs reuses the snapshot ID from the first
prepare job, but the cleanup job already deleted that snapshot, so the shards fail when they fork it.Run untrusted code
Pull requests from outside contributors can run any code in their build scripts and tests. Running that code in a sandbox keeps it off the CI runner:- Each sandbox is its own microVM, so the code can’t touch the runner or other jobs.
- The Superserve API key and the runner’s other credentials stay on the runner. Only the files and env vars you send go into the sandbox.
- Bind credentials the tests need as secrets, so the sandbox only holds stand-in tokens. A stand-in token hides the real value, but code in the sandbox can still use it on the secret’s hosts, so bind only narrowly scoped test credentials.
--ignore-scripts: npm still reads the repository’s .npmrc, which can point the git setting at a script in the repository. So install in one sandbox that has a network allowlist, then run the tests in a second sandbox that has no outbound access:
- Treat the install sandbox as untrusted. Code that runs during the install can reach the DNS resolvers and the registry, so don’t bind secrets or send credentials to it.
- Only files go from the install sandbox to the test sandbox. A process that the install starts stays in the install sandbox, and the test sandbox never has outbound access.
- Don’t use
sandbox update --deny-out 0.0.0.0/0on the install sandbox as a replacement. New rules apply only to new connections, and a process that the install starts keeps running and keeps its open connections. - If the tests need a host, such as a test database, add only that host with
--allow-outon the test sandbox. - The network log shows the HTTP and HTTPS connections on ports 80 and 443 that the sandbox made or tried to make, allowed or blocked. It doesn’t record DNS or other ports.
pull_request_target and a checkout of the pull request’s code: that gives the code access to your repository secrets on the runner itself.
Limits and tips
- Timeouts in the SDK:
commands.run()stops a command after 30 seconds by default. SettimeoutMs(TypeScript) ortimeout_seconds(Python) for builds and tests. See Timeouts. - Sandbox size: a sandbox gets the vCPU and memory of its template. The Free plan allows up to 2 vCPU and 4 GiB per sandbox. See resource limits.
- Daily allowance: on the Free plan, running sandboxes pause when the team uses its daily compute allowance, and
createfails withfree_allowance_exhausted. - Exit codes: the CLI’s own errors use exit codes such as
4for not found and6for rate limited.execreturns the command’s exit code.
Related guides
Docker services
Start databases and other services for your tests.
Coding agents
Let an agent change the code, then run the tests.
Evals & RL
Score many runs in parallel sandboxes.
Networking
Control which hosts a job can reach.