Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

JupyterHub Spawner: per-user notebook servers as Sandstorm sandboxes

Open
#21 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
25/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Quiet
Tech stack
python, rust

Research direction

Start with recipes/jupyter-kernel/ and the forkd-controller sandbox endpoints named in the issue; map the existing recipe and API before deciding how the Spawner will start, poll, stop, persist state, and expose kernel ports. The kernel-readiness, resource-limit, and authentication questions are unresolved, so settle those design choices first. Done means the package installs and the stated JupyterHub integration test spawns five kernels that each execute a numpy expression.

Written by the indexing model from the issue text.

Description

backend:gvisor enhancement help wanted

Goal

A JupyterHub Spawner that runs each user's single-user notebook server as a Sandstorm sandbox. Hub admins would get per-user servers that cost almost nothing while idle and come back with their kernel state intact, instead of being culled.

Why this fits Sandstorm now

The case for this changed with v0.6. It is no longer about cold-start time. It is about idle time, which is what Sandstorm is built for.

  • A JupyterHub user's server is idle most of the day. Today an admin either keeps it running and paying for memory, or lets jupyterhub-idle-culler kill it, and the user's variables are lost.
  • On Sandstorm an idle server drops through frozen (about 10 ms back) to snapshot (about 0.2 s back, no memory held), and the first request wakes it with the kernel's state as it was. That is the four tiers in the README, applied to notebooks.

What already exists (v0.6)

  • Lifecycle: POST /v1/sandboxes with template (or snapshot_tag to fork a warmed image) and memory_limit_mib. Also GET /v1/sandboxes/:id, DELETE /v1/sandboxes/:id, and POST /v1/sandboxes/:id/tier to put one away explicitly. Calls carry the master's bearer token. The master places sandboxes across nodes, so multi-host is no longer out of scope.
  • Reaching the server: each sandbox has a gateway_port on its node and a per-sandbox token. The gateway streams responses and splices WebSockets (#365), which is what notebook kernels use. It also wakes a cold sandbox when a request reaches it (ADR 7). Kernels' ZMQ ports never leave the sandbox: jupyter-server speaks HTTP and WebSocket to the browser, so no port forwarding beyond the gateway is needed.

The first thing to settle: the token on the browser's path

The gateway only accepts the sandbox token as the x-sandstorm-sandbox-token header. A browser going through JupyterHub's proxy (configurable-http-proxy) cannot add a per-user header, and CHP has no per-route header injection. So the proposal has to pick one of these before any Spawner code is written:

  1. A small proxy step on the hub side that adds the header per route (a CHP replacement such as Traefik with a per-route header middleware).
  2. A second credential the gateway accepts, such as a cookie or a query parameter scoped to one sandbox, designed against RFC 16.5.
  3. Something else, with the reasons.

Whichever it is must keep the property the gateway exists for: no request reaches a sandbox without that sandbox's own token.

Scope

  • In scope: a sandstorm-jupyter-spawner package (in sdk/ here or its own repo) subclassing jupyterhub.spawner.Spawner:
    • start() creates the sandbox and returns the URL the hub's proxy should route to.
    • poll() reads the sandbox. A frozen or snapshot tier counts as running, because it wakes on the next request.
    • stop() deletes it. An idle-culler hook should move it to snapshot instead of deleting it.
    • get_state / load_state keep the sandbox id and token across hub restarts.
  • A template for the single-user server (jupyter-server and ipykernel, with the SciPy stack pre-imported and warmed), built for v0.6 templates. The old recipes/jupyter-kernel is a v0.5 rootfs.
  • Out of scope: a custom Jupyter UI, replacing JupyterHub, and CPU or IO quotas. The master takes a memory ceiling today and nothing else, so CPU limits are a separate feature.

Acceptance criteria

  • c.JupyterHub.spawner_class = "sandstorm_jupyter_spawner.SandstormSpawner" with a master URL, token and template starts a working notebook server.
  • An integration test brings up a local JupyterHub against a one-node fleet and starts 5 users in parallel. Each runs a numpy cell, is moved to the snapshot tier, then runs a second cell that reads a variable from the first.
  • The time from a click on a snapshot-tier user's notebook to the cell running is reported, next to the hot-tier time.

Still help wanted. If you want to pick it up, start with a short proposal for the token question above.

Dominant language
Rust
Stars
2.9k
Forks
227
PR merge metrics
No merged PRs in 30d

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from deeplethe/forkd

All issues in deeplethe/forkd

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.