> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superserve.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Run CI jobs in sandboxes

> Run each CI job in its own sandbox with the Superserve CLI. Includes a GitHub Actions workflow, sharding and network isolation for untrusted code.

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](https://docs.superserve.ai/cli.md). 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](https://docs.superserve.ai/templates/overview.md), or fork a prepared sandbox from a [snapshot](https://docs.superserve.ai/sandbox/snapshots.md).

## 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.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
superserve sandbox create --name ci-1234 --template superserve/node-22 --timeout 30m --auto-delete 0

superserve exec ci-1234 --timeout 20m -- \
  'git clone --depth 1 https://github.com/your-org/your-repo /w && cd /w && npm ci && npm test'

superserve sandbox delete ci-1234 --yes
```

* `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](https://docs.superserve.ai/sandbox/lifecycle.md#auto-pause) after 30 minutes and then [auto-deletes](https://docs.superserve.ai/sandbox/lifecycle.md#auto-delete) at once. Make the `--timeout` longer than your slowest job.

> [!NOTE]
>
> 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:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
superserve exec ci-1234 -- mkdir -p /w
tar cz --exclude=.git . | superserve exec -i ci-1234 -- tar xz -C /w
```

`--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`:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
superserve exec ci-1234 -e NODE_ENV=test -e CI=true -- 'cd /w && npm test'
```

For credentials, bind a [secret](https://docs.superserve.ai/secrets/overview.md) 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:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
printf %s "$NPM_TOKEN" | superserve secret create npm-ci --host registry.npmjs.org --auth bearer
superserve sandbox create --name ci-1234 --template superserve/node-22 -s NPM_TOKEN=npm-ci
```

See [Environment variables](https://docs.superserve.ai/sandbox/environment-variables.md) 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](https://docs.superserve.ai/api-key.md) 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:

```yaml .github/workflows/test.yml theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      SUPERSERVE_API_KEY: ${{ secrets.SUPERSERVE_API_KEY }}
      BOX: ci-${{ github.run_id }}-${{ github.run_attempt }}
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
      - name: Install the Superserve CLI
        run: npm install -g @superserve/cli
      - name: Create a sandbox
        run: superserve sandbox create --name "$BOX" --template superserve/node-22 --timeout 30m --auto-delete 0
      - name: Send the checkout to the sandbox
        run: |
          superserve exec "$BOX" -- mkdir -p /w
          tar cz --exclude=.git . | superserve exec -i "$BOX" -- tar xz -C /w
      - name: Install and test
        run: superserve exec "$BOX" --timeout 20m -- 'cd /w && npm ci && npm test'
      - name: Delete the sandbox
        if: always()
        run: superserve sandbox delete "$BOX" --yes
```

* 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](#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](https://docs.superserve.ai/templates/overview.md), then create each job's sandbox from it:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
superserve template create ci-node --from node:22 --run "npm install -g pnpm@10" --vcpu 2 --memory 4G
superserve sandbox create --name ci-1234 --template ci-node --timeout 30m --auto-delete 0
```

Rebuild the template when the tools change, not on every job. See [Create a template](https://docs.superserve.ai/templates/create.md) for all the options and [resource limits](https://docs.superserve.ai/templates/build-spec.md#resource-limits) for sizes. For jobs that need containers, bake Docker into the template as shown in [Run Docker services in a sandbox](https://docs.superserve.ai/sandbox/docker.md).

### Snapshots and forks

A [snapshot](https://docs.superserve.ai/sandbox/snapshots.md) 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:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
superserve sandbox create --name ci-1234-base --template superserve/node-22 --timeout 30m --auto-delete 0
# send the checkout to /w as above, then:
superserve exec ci-1234-base --timeout 10m -- 'cd /w && npm ci'
SNAPSHOT=$(superserve snapshot create ci-1234-base --name ci-1234 --json | jq -er .id)
superserve sandbox delete ci-1234-base --yes

superserve sandbox create --name ci-1234-job --snapshot "$SNAPSHOT" --timeout 30m --auto-delete 0
superserve exec ci-1234-job --timeout 20m -- 'cd /w && npm test'
```

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.

```yaml .github/workflows/test-sharded.yml theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
name: Test (sharded)
on: [push, pull_request]

env:
  SUPERSERVE_API_KEY: ${{ secrets.SUPERSERVE_API_KEY }}
  RUN: ci-${{ github.run_id }}-${{ github.run_attempt }}

jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      snapshot: ${{ steps.snap.outputs.id }}
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
      - run: npm install -g @superserve/cli
      - name: Install dependencies in a sandbox
        run: |
          superserve sandbox create --name "$RUN-base" --template superserve/node-22 --timeout 30m --auto-delete 0
          superserve exec "$RUN-base" -- mkdir -p /w
          tar cz --exclude=.git . | superserve exec -i "$RUN-base" -- tar xz -C /w
          superserve exec "$RUN-base" --timeout 10m -- 'cd /w && npm ci'
      - name: Take a snapshot
        id: snap
        run: |
          SNAPSHOT=$(superserve snapshot create "$RUN-base" --name "$RUN" --json | jq -er .id)
          echo "id=$SNAPSHOT" >> "$GITHUB_OUTPUT"
      - name: Delete the sandbox
        if: always()
        run: superserve sandbox delete "$RUN-base" --yes

  shard:
    needs: prepare
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3]
    env:
      BOX: ci-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.shard }}
    steps:
      - uses: actions/setup-node@v7
        with:
          node-version: 22
      - run: npm install -g @superserve/cli
      - name: Fork the snapshot
        run: superserve sandbox create --name "$BOX" --snapshot "${{ needs.prepare.outputs.snapshot }}" --timeout 30m --auto-delete 0
      - name: Run one shard
        run: superserve exec "$BOX" --timeout 20m -- 'cd /w && npm test -- --test-shard=${{ matrix.shard }}/3'
      - name: Delete the sandbox
        if: always()
        run: superserve sandbox delete "$BOX" --yes

  cleanup:
    needs: [prepare, shard]
    if: always() && needs.prepare.outputs.snapshot != ''
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v7
        with:
          node-version: 22
      - run: npm install -g @superserve/cli
      - run: superserve snapshot delete "${{ needs.prepare.outputs.snapshot }}" --yes
```

Each shard is one active sandbox. Past your team's active sandbox limit, `sandbox create` fails with [`too_many_sandboxes`](https://docs.superserve.ai/errors#ratelimiterror). Set `strategy.max-parallel` to stay under the limit when several runs can overlap. Snapshots have their own [limits](https://docs.superserve.ai/sandbox/snapshots.md#limits), so delete them when the run ends, as the `cleanup` job does.

> [!NOTE]
>
> 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](https://docs.superserve.ai/secrets/overview.md), 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](https://docs.superserve.ai/sandbox/networking.md), then run the tests in a second sandbox that has no outbound access:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
# Install sandbox: DNS and the registry only
superserve sandbox create --name ci-1234-install --template superserve/node-22 --timeout 30m --auto-delete 0 \
  --deny-out 0.0.0.0/0 \
  --allow-out 1.1.1.1/32 --allow-out 8.8.8.8/32 \
  --allow-out registry.npmjs.org

# send the checkout to /w as above, then:
superserve exec ci-1234-install --timeout 10m -- 'cd /w && npm ci --ignore-scripts'

# Test sandbox: no outbound access
superserve sandbox create --name ci-1234-test --template superserve/node-22 --timeout 30m --auto-delete 0 \
  --deny-out 0.0.0.0/0
superserve exec ci-1234-test -- mkdir -p /w
superserve exec ci-1234-install -- tar cz -C /w . | superserve exec -i ci-1234-test -- tar xz -C /w
superserve sandbox delete ci-1234-install --yes

superserve exec ci-1234-test --timeout 20m -- 'cd /w && npm test'
```

* 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](https://docs.superserve.ai/sandbox/network-log.md) 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](https://docs.superserve.ai/commands/overview.md#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](https://docs.superserve.ai/templates/build-spec.md#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`](https://docs.superserve.ai/errors#billing-errors-402).
* **Exit codes:** the CLI's own errors use [exit codes](https://docs.superserve.ai/cli.md#exit-codes) such as `4` for not found and `6` for rate limited. `exec` returns the command's exit code.

## Related guides

> [card] [Docker services](https://docs.superserve.ai/sandbox/docker.md)
>
> Start databases and other services for your tests.

> [card] [Coding agents](https://docs.superserve.ai/guides/coding-agents.md)
>
> Let an agent change the code, then run the tests.

> [card] [Evals & RL](https://docs.superserve.ai/guides/evals-and-rl.md)
>
> Score many runs in parallel sandboxes.

> [card] [Networking](https://docs.superserve.ai/sandbox/networking.md)
>
> Control which hosts a job can reach.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.