Skip to main content
A CI job fits a sandbox well: create a sandbox, run the build and tests in it, then delete it. Each job gets a clean microVM, so jobs can’t leave files, processes or credentials behind for the next job. This guide uses the CLI. The same steps work from the SDK.

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 with superserve exec, and delete it.
  • superserve exec exits 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 && and cd work.
  • Set --timeout on every exec that runs a build or tests. A command that runs longer stops, and exec exits with 124.
  • --timeout 30m --auto-delete 0 on create is 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 --timeout longer 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 with exec -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:
For credentials, bind a secret with -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:
See Environment variables for how env vars behave.

GitHub Actions

This workflow runs a Node.js project’s tests in a sandbox on every push and pull request.
  1. Create an API key in the console.
  2. Add the key as a repository secret named SUPERSERVE_API_KEY (Settings → Secrets and variables → Actions).
  3. Add the workflow file:
.github/workflows/test.yml
  • The CLI reads SUPERSERVE_API_KEY from the environment, so no login step is needed.
  • github.run_attempt keeps 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_request workflows 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:
Rebuild the template when the tools change, not on every job. See Create a template for all the options and resource limits for sizes. For jobs that need containers, bake Docker into the template as shown in Run Docker services in a sandbox.

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:
The fork starts with /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
Each shard is one active sandbox. Past your team’s active sandbox limit, 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.
Also limit what the code can reach on the network. The install needs the package registry, and it can run project code even with --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/0 on 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-out on 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.
Keep the GitHub side safe too. Don’t run untrusted pull requests with 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. Set timeoutMs (TypeScript) or timeout_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 create fails with free_allowance_exhausted.
  • Exit codes: the CLI’s own errors use exit codes such as 4 for not found and 6 for rate limited. exec returns the command’s exit code.

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.