# How Do I Give Every Student in a Class Their Own Cloud Sandbox?

> Build the lab once in a sandbox and save it as a named snapshot. Before class, create one small --no-env sandbox per student from it, with a TTL and an idle timeout, and send each student their own private code-server URL or SSH access. Delete them all at term end. A 30-student, 2-hour lab on small costs $1.08.

Source: https://sandboxer.com/guides/sandboxes-for-classrooms-and-teaching. Updated 2026-10-27.

**Short answer:** Build the lab once in a sandbox and save it as a named snapshot. Before class, create one small --no-env sandbox per student from it, with a TTL and an idle timeout, and send each student their own private code-server URL or SSH access. Delete them all at term end. A 30-student, 2-hour lab on small costs $1.08.

This page is for instructors running a course, a bootcamp or a one-day workshop. Students don't need a Sandboxer account, a card or anything installed: you run the sandboxes, and each student gets a link. Everything in the end-to-end section ran on prod on 2026-10-27. For a weekend event with teams, see [Sandboxes for hackathons](/guides/sandboxes-for-hackathons); for a vendor workshop or runnable docs, see [Sandboxes for DevRel](/guides/sandboxes-for-devrel-runnable-docs).

## The decisions, and what to use for each

| Decision | Short answer | Sandboxer feature | Worked guide |
| - | - | - | - |
| One lab image for everyone | Install the tools and the starter files once, then save the result under a versioned name | `sbx snapshot <id> lab01-v1`, `sbx new --from lab01-v1` | [Turn a sandbox into a reusable template](/guides/turn-a-sandbox-into-a-reusable-template) |
| How students get in | A private hosted code-server or JupyterLab URL for browser-only students; SSH with their own key for students with a terminal | `sbx host <id> 8080`, `POST /sandboxes/{id}/sshkey` | [code-server](/guides/run-code-server-vs-code-in-the-browser), [JupyterLab](/guides/run-jupyterlab-on-an-https-url), [SSH from VS Code](/guides/ssh-into-a-cloud-sandbox-from-vs-code-or-cursor) |
| Keeping your account out of student hands | Create every student sandbox with `--no-env`: none of your secrets inside, and its built-in token can only read that sandbox and host its ports | `sbx new --no-env` | [Environment variables inside a sandbox](/guides/environment-variables-inside-a-sandbox) |
| Bounding the cost per student | Small size, a TTL as long as the session, an idle timeout so breaks cost nothing | `--type small --ttl 7200 --idle 15` | [Idle auto-pause and wake on use](/guides/idle-auto-pause-and-wake-on-use), [Cap what sandboxes can cost](/guides/cap-what-agent-sandboxes-can-cost) |
| Starting 30 at once | Your plan's starts-per-minute limit sets the pace; start early | `sbx limits --json` | [Rate limits and concurrency caps](/guides/handle-rate-limits-and-concurrency-caps) |
| End of term | Copy out what you grade, then delete every sandbox and the snapshot | `sbx scp`, `sbx delete <id> --yes`, `sbx snapshot rm` | [Clean up orphaned sandboxes](/guides/clean-up-orphaned-sandboxes) |

## You run the sandboxes, or students join an organization?

There are two ways to set up a class. For most classes, you run the sandboxes.

| | You create every sandbox (recommended) | Students are members of your organization |
| - | - | - |
| Student needs | A link (or an SSH key) | A Sandboxer sign-in |
| Who pays | Your wallet | The organization's wallet, **per seat**: plan price × seats ([Organizations](/docs/organizations#seats-limits-and-caps)) |
| Can students see each other's sandboxes? | No. Each student has only their own URL or key | Yes. Every member sees the organization's sandboxes, and `--no-env` ones are usable by every member from the start |
| Can they start from your lab snapshot? | You do it for them | Not by name: named snapshots are visible only to their creator. You'd give them a share code (`sbx snapshot share`) |
| Per-student cap | Size × TTL bounds each sandbox; there is no per-student dollar setting | An owner can cap a member's machine time with `PATCH /orgs/{orgId}/members/{userId}` and `usageCapSeconds`, API only. Over the cap, new starts get `402 team_member_cap_reached`; running sandboxes keep running |

An organization fits the staff, not the students. Put yourself and your teaching assistants in one: every member can see the class sandboxes your organization pays for, and can open and exec into the `--no-env` ones to help a stuck student. Use the owner-only member caps for the TAs who start their own sandboxes. We didn't create an organization or set a cap for this guide; that column is from the [organization docs](/docs/organizations) and the API code.

## Getting students in

| Route | Student needs | What we measured on prod |
| - | - | - |
| Private code-server URL | A browser | `401` without the token. The token URL sets a cookie and redirects, then the editor page loads with `200`. One student's token on another student's URL got `401` |
| Private JupyterLab URL | A browser | Not run here; the [JupyterLab guide](/guides/run-jupyterlab-on-an-https-url) runs the same pattern on port 8888 |
| SSH with the student's own key | An SSH client (or VS Code Remote-SSH) | The student's public key is authorized through the API, then plain `ssh` to the sandbox's IPv4 `sshEndpoint` connects in 1.78 s. The same key against the other student's sandbox got `Permission denied (publickey)` |
| `sbx ssh` with a per-student API key | The `sbx` CLI | Not run. [API keys](/docs/api-keys) can be limited to one sandbox, a list of actions and an expiry, for example `--sandbox <id> --actions sandbox.read,ssh --ttl 120d`. Never give students your own key |

`sbx handoff` isn't a student-access tool. It moves your own Claude Code or Codex session and repo from your laptop into a sandbox ([MCP docs](/docs/mcp)).

## One lab, two students, end to end

We built a lab image with code-server and a Python exercise, saved it, started two student sandboxes from it, checked each one and deleted everything. Sizes were all `small`. Private URLs and tokens are replaced with `<token>`.

### 1. Build the lab image

```bash
sbx new --name classroom-1 --type small --no-env --ttl 3600
id=sb_11ub6acn7l

sbx exec $id --timeout 300 "curl -fsSL https://code-server.dev/install.sh | sh >/tmp/cs.log 2>&1; code-server --version"
```

```text
4.143.1 22ddf60dbc52f164cff13620ab515086dd08c90a with Code 1.143.1
```

The [code-server install script](https://coder.com/docs/code-server/install) took 6.8 s. Then the lab itself: a starter file with a TODO, the tests that grade it, a venv with pinned pytest, and code-server as a systemd service that starts on every boot. We piped this script in with `sbx ssh $id -- bash -s < lab.sh`:

```bash
set -e
mkdir -p ~/lab01 && cd ~/lab01
cat > stats.py <<'P'
def mean(xs):
    """Return the arithmetic mean of xs. TODO: handle an empty list."""
    raise NotImplementedError
P
cat > test_stats.py <<'P'
import pytest
from stats import mean

def test_mean():
    assert mean([1, 2, 3, 4]) == 2.5

def test_empty():
    with pytest.raises(ValueError):
        mean([])
P
uv venv -q --seed ~/lab01/.venv --python 3.12
VIRTUAL_ENV=~/lab01/.venv uv pip install -q pytest==8.3.3
echo 'source ~/lab01/.venv/bin/activate' >> ~/.bashrc
cat > ~/.config/code-server/config.yaml <<C
bind-addr: 0.0.0.0:8080
auth: none
cert: false
C
sudo systemctl enable --now code-server@user
sleep 3
systemctl is-active code-server@user
curl -s -o /dev/null -w "local %{http_code}\n" localhost:8080/
cd ~/lab01 && ~/lab01/.venv/bin/python -m pytest -q -p no:cacheprovider --tb=no | tail -1
```

```text
active
local 200
2 failed in 0.01s
```

Both tests fail before any student touches the code, so the grader works. `auth: none` is safe only because the hosted URL's token guards the editor; never host this port with `--public`.

### 2. Save it and delete the builder

```bash
sbx --json snapshot $id classroom-lab01-v1
sbx delete $id --yes
```

```json
{"ok":true,"type":"snapshot.named.info","snapshot":{"name":"classroom-lab01-v1","snapshotId":"69994b2c-…","sourceSandboxId":"sb_11ub6acn7l","status":"ready","type":"small",…}}
```

The builder lived about 40 seconds, create to delete.

### 3. Start a student's sandbox and hand it over

These are the commands we ran for each student. For a class, run them in a loop over your roster and write each student's id and URL to a file you keep private.

```bash
sbx --json new --from classroom-lab01-v1 --name classroom-2 --type small --no-env --ttl 7200 --idle 15
sbx host sb_4ac1yp0yd9 8080 --title "Lab 1"
```

```json
{"id":"sb_4ac1yp0yd9","name":"classroom-2","type":"small","noEnv":true,"ttlSeconds":7200,"idleStopMinutes":15,"sshEndpoint":"62.238.91.250:22021","subdomain":"exact-epic-salmon"}
```

```text
https://exact-epic-salmon-8080.box.sandboxer.com?_token=<token>
```

That URL is what the student gets. Checking it from outside:

```bash
curl -s -o /dev/null -w '%{http_code}\n' https://exact-epic-salmon-8080.box.sandboxer.com/
curl -s -c jar -o /dev/null -w '%{http_code} %{redirect_url}\n' "https://exact-epic-salmon-8080.box.sandboxer.com?_token=<token>"
curl -s -b jar -w '%{http_code}\n' -o page.html https://exact-epic-salmon-8080.box.sandboxer.com/
```

```text
401
302 https://exact-epic-salmon-8080.box.sandboxer.com/
200
```

The page that came back was code-server's workbench. The service started by itself in the new sandbox, because the snapshot holds the enabled systemd unit, and the lab files and venv were there:

```bash
sbx exec sb_4ac1yp0yd9 "systemctl is-active code-server@user; ls ~/lab01; cd ~/lab01 && .venv/bin/python -m pytest -q -p no:cacheprovider --tb=no | tail -1"
```

```text
active
__pycache__
README.md
stats.py
test_stats.py
2 failed in 0.00s
```

For a student who wants a terminal instead, authorize their public key (never a private key) and give them the `sshEndpoint`:

```bash
curl -sS -X POST https://sandboxer.com/api/v1/sandboxes/sb_4ac1yp0yd9/sshkey \
  -H "Authorization: Bearer $SANDBOXER_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"key\":\"$(cat student1_key.pub)\"}"
# the student runs:
ssh -i student1_key -p 22021 user@62.238.91.250 'hostname; cd lab01 && .venv/bin/python -m pytest -q -p no:cacheprovider --tb=no | tail -1'
```

```text
exact-epic-salmon
2 failed in 0.00s
```

### 4. The second student

The second sandbox, `sb_15e0iaw16a`, came from the same snapshot and checked out the same way: its own subdomain (`tiny-cyan-sierra`), `401` without its token, `200` with it, code-server active, the same two failing tests. Two isolation checks:

- The first student's token on the second student's URL: `401`.
- The first student's SSH key against the second student's sandbox: `Permission denied (publickey)`. Each sandbox also had its own SSH host key.

We ran the two students one after another, not side by side, to keep our test small. In class they run side by side.

### 5. Clean up

```bash
sbx delete sb_4ac1yp0yd9 --yes
sbx delete sb_15e0iaw16a --yes
sbx snapshot rm classroom-lab01-v1
sbx --json info sb_4ac1yp0yd9
```

```text
{"event":"error","error":"Sandbox not found","code":"not_found","status":404,…}
```

After the delete, the first student's old private URL answered `404`. All three sandbox ids returned `404`, and `sbx list` showed no `classroom` sandboxes.

### Timings

Single runs, timed from our client, so they include CLI and network time:

| Step | Time |
| - | - |
| Create the builder (`small`) | 3.20 s to ready |
| Install code-server | 6.8 s |
| Lab files, venv, pytest, service | 8.6 s (3 s of it a deliberate wait) |
| Save the named snapshot | 2.13 s |
| Student sandbox from the snapshot | 3.20 s to ready, both times |
| `sbx host` | 0.30 s |
| Editor page through the private URL, after `host` | 0.67 s and 0.72 s, first try |
| SSH with the student's own key | 1.78 s |
| Delete a sandbox | 2.13 s |

## What a 30-student, 2-hour lab costs

From [pricing](/pricing): `small` (2 vCPU, 4 GB) costs $0.018/hour and `default` (4 vCPU, 8 GB) $0.036/hour, billed per second while a sandbox runs.

| Scenario | Arithmetic | Cost |
| - | - | - |
| 30 students, 2 hours, `small`, all running the whole time | 30 × 2 h × $0.018 | $1.08 |
| Same on `default` | 30 × 2 h × $0.036 | $2.16 |
| 12 weekly labs of that size in a term, `small` | 12 × $1.08 | $12.96 |
| Building the image | About 40 s of `small` | Under $0.001 |

`--ttl 7200` makes each run stop itself 2 hours after it starts. In the API code, a wake from idle pause sets a new deadline 2 hours from the wake, so a sandbox a student opens again after class runs again. To make $1.08 a real ceiling, delete the sandboxes when the session ends, or use `--idle 0` so they don't wake. Idle pause lowers the bill during class, because a paused sandbox bills no compute. The $20 plan includes $20 of machine time a month, which covers the 12-lab term. Check each student's real number with `sbx usage <id>`: two seconds into its life, our second student's sandbox showed `"seconds":2,"dollars":0.00002`.

## Limits that matter for a class

| Limit | Free trial | $20 plan | $100 plan |
| - | - | - | - |
| Sandboxes running at once | 2 | 100 | 300 |
| Starts per minute / hour / day | | 12 / 60 / 200 | 30 / 210 / 840 |
| Longest TTL | 2 hours | 30 days | 30 days |

- **The trial can't run a class.** It allows 2 sandboxes at once.
- **Start early.** On the $20 plan, 30 creates spread over three minutes (12, 12, then 6). Creates, resumes and forks each count as one start, and in the API code a wake from idle pause is recorded as a start too. If all 30 sandboxes pause over a break, the 30 wakes after it are another 30 starts, which reaches the 60-an-hour limit. For a 2-hour session, either create the sandboxes more than an hour ahead, set `--idle` longer than your breaks, or use `--idle 0` and let the TTL stop them.
- **Storage.** Stopped sandboxes and named snapshots count toward your plan's included storage (50 GB on $20). At the limit, new sandboxes get `402 storage_limit_reached`. Delete last term's sandboxes and old lab versions.

## Honest limits

- **No per-student dollar cap when you run the sandboxes.** Size × TTL bounds each one; the member usage cap applies only to organization members starting their own sandboxes, and only through the API.
- **The private URL is a password.** Anyone a student forwards it to gets in. To revoke one, the documented path is switching the port to `--public` and back to `--private`, which mints a new token but leaves it open for that moment; replacing the sandbox is the safer fix.
- **Students have root in their VM.** That's the point of a VM per student, and the isolation is between VMs. Inside their own sandbox they can install anything and reach the internet.
- **SSH endpoint ports get reused.** Our second student's sandbox got the same IPv4 port as the first, deleted one, with a different host key. A student whose old sandbox was replaced sees a host-key warning until they remove the old `known_hosts` line.
- **Where it runs.** The sandbox we checked reported `region: "eu-central"`. Sandboxer has two regions, us-east (Ashburn, Virginia) and eu-central (Helsinki, Finland); see [regions](/docs/regions). Students far from both will feel the round trip in a terminal more than in a browser editor.
- **No GPU.** Sizes are CPU-only, up to `xlarge` (16 vCPU, 32 GB, $100 plan or higher). A machine-learning class can teach on small models, not train large ones.
- **Moving to a smaller size isn't supported yet.** Pick the size in the snapshot you build; a new, smaller sandbox means copying the student's work over.

## FAQ

### Do students need a Sandboxer account?

No, not when you run the sandboxes. A student needs only their private URL, or an SSH client and the public key you authorized. They need an account only if they join your organization or start their own copy from a share code.

### What happens to a student's work if their sandbox stops?

Nothing is lost. Auto-stop and idle pause keep the disk and memory, and an idle-paused sandbox wakes on the next request to its private URL. A sandbox stopped by its TTL stays stopped until you run `sbx resume <id>`. Copy work you grade out with `sbx scp <id>:/home/user/lab01 .` before you delete.

### How do I update the lab mid-term?

Change a sandbox built from the current image, save it under a new name such as `lab01-v2`, and create new students' sandboxes from that. Sandboxes already running keep the version they started from; reusing a name replaces the snapshot.

### Can teaching assistants get into a student's sandbox?

Yes, if they are members of the organization that pays for the sandboxes: members can open and exec into `--no-env` organization sandboxes. Otherwise you exec into it yourself with `sbx exec` or `sbx ssh`.

## Related

- [Turn a sandbox into a reusable template](/guides/turn-a-sandbox-into-a-reusable-template)
- [Run code-server (VS Code in the browser)](/guides/run-code-server-vs-code-in-the-browser)
- [Give every user of your product their own sandbox](/guides/give-every-user-their-own-sandbox)
- [Organizations](/docs/organizations)
