#Sandbox Services
Sandbox Services expose public HTTP entrypoints for workloads inside a sandbox. A service can point to a long-running listener on a sandbox port, start a command on first public access, or route to a Sandbox Function.
Use services for:
- public HTTP routes into a sandbox
- route-level method filtering, auth, CORS, rate limits, timeout, and path rewrite
- public URL auto-resume for paused sandboxes
- public entrypoints for listeners, command-backed services, and sandbox functions
Mental Model#
A sandbox service has three layers:
| Layer | Purpose |
|---|---|
| Service | Names the public entrypoint and, for listener-backed services, the sandbox port |
| Runtime | Describes who owns the backing process: user, command, or function |
| Route | Matches public HTTP requests and applies route policy before proxying or executing |
The service response includes a public_url when ingress.public is true and the deployment has an exposure domain. Treat public_url as the service base URL. Append a route path when calling it.
bashcurl "$PUBLIC_URL/api/health"
Runtime Behavior#
When runtime is omitted, Sandbox0 treats the service as manual.
| Runtime | port | Public access behavior |
|---|---|---|
manual | Required | Sandbox0 proxies traffic to port. The template, user, or agent must keep the listener running |
cmd | Required | On public access, Sandbox0 creates the configured procd cmd context when it is missing, waits for readiness, then proxies traffic to port |
function | Omit | Sandbox0 routes the request to procd and executes the configured inline function source. No user-managed listener is required |
For cmd services, health_check.path controls readiness. If it is omitted, Sandbox0 falls back to a TCP connect check on the service port.
Public URL Shape#
For listener-backed services, the public URL is derived from the sandbox ID, service port, region ID, and root domain:
texthttps://<sandbox_id>--p<port>.<region_id>.<root_domain>
Function services also return a service-level public_url, but they use Sandbox0's reserved function routing label. See Sandbox Functions for function-specific URL and config details.
The URL itself does not include a route path. If a route has path_prefix: /webhook, call:
bashcurl -X POST "$PUBLIC_URL/webhook/github"
Routes And Paths#
Routes are evaluated before proxying or function execution.
| Field | Behavior |
|---|---|
path_prefix | Matches request paths. Defaults to / |
methods | Allows only listed HTTP methods. Empty allows all methods |
rewrite_prefix | Rewrites the matched path prefix before proxying or function execution |
auth | Applies bearer or header authentication before the request reaches the sandbox |
cors | Handles CORS preflight and response headers at the gateway |
rate_limit | Applies per-route rate limiting at the gateway |
timeout_seconds | Sets the upstream or function execution timeout |
resume | Allows this route to resume a paused sandbox when sandbox auto_resume is also true. Resume-enabled public routes require runtime.type: cmd or runtime.type: function |
path_prefix is not stripped automatically. Without rewrite_prefix, the upstream service or function handler sees the original request path.
yamlroutes: - id: webhook path_prefix: /webhook resume: true
A request to $PUBLIC_URL/webhook/github reaches the backing service or function as /webhook/github.
To strip /webhook, set rewrite_prefix: /:
yamlroutes: - id: webhook path_prefix: /webhook rewrite_prefix: / resume: true
The same request reaches the backing service or function as /github.
Sandbox Functions#
Use Sandbox Functions when you want a public HTTP handler without running a long-lived listener inside the sandbox. A function is still configured as a service with runtime.type: function, so it uses the same public URL, route policy, auth, CORS, rate limit, timeout, rewrite, and auto-resume model.
Function services omit port, execute inline source through procd, and can return normal HTTP responses, SSE streams, or WebSocket messages. Set runtime.function.max_concurrency when one function service must limit simultaneous handler executions inside a sandbox runtime.
Cmd Services#
A cmd service starts a procd command on first matching public access when the service command is not already running. Sandbox0 injects these environment variables into the command:
| Environment variable | Value |
|---|---|
SANDBOX0_SERVICE_ID | Service ID |
SANDBOX0_SERVICE_PORT | Configured service port |
SANDBOX0_SERVICE_RUNTIME | cmd |
yamlservices: - id: api port: 8080 runtime: type: cmd command: - python3 - -c - | import http.server import json import os import socketserver port = int(os.environ["SANDBOX0_SERVICE_PORT"]) class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): if self.path == "/healthz": self.send_response(204) self.end_headers() return self.send_response(200) self.send_header("content-type", "application/json") self.end_headers() self.wfile.write(json.dumps({ "service_id": os.environ["SANDBOX0_SERVICE_ID"], "path": self.path, }).encode()) socketserver.TCPServer(("", port), Handler).serve_forever() health_check: path: /healthz ingress: public: true routes: - id: api path_prefix: /api rewrite_prefix: / methods: [GET] timeout_seconds: 30 resume: true
After applying this service, the first request starts the command, waits for /healthz, rewrites /api/hello to /hello, and proxies to the command.
bashcurl "$PUBLIC_URL/api/hello"
Manual Services#
Use manual when another actor starts and owns the listener.
yamlservices: - id: web port: 3000 runtime: type: manual ingress: public: true routes: - id: web path_prefix: / resume: false
Manual services can expose a listener while the sandbox is running, but they cannot be restarted by public auto-resume. Use a cmd service or a function service when a public route needs resume: true.
API Reference#
List Services#
/api/v1/sandboxes/{id}/services
The response includes the normalized services, publishable, publish_blockers, public_url, and exposure_domain.
goresp, err := sandbox.GetServices(ctx) if err != nil { log.Fatal(err) } for _, service := range resp.Services { fmt.Printf("%s url=%s port=%d\n", service.ID, service.PublicURL.Or(""), service.Port.Or(0)) }
Replace Services#
/api/v1/sandboxes/{id}/services
PUT replaces the full service list. Include every service that should remain active.
gotokenHash := sha256.Sum256([]byte(os.Getenv("PUBLIC_API_TOKEN"))) resp, err := sandbox.UpdateServices(ctx, []apispec.SandboxAppService{ { ID: "api", Port: apispec.NewOptInt32(8080), Runtime: apispec.NewOptSandboxAppServiceRuntime(apispec.SandboxAppServiceRuntime{ Type: apispec.SandboxAppServiceRuntimeTypeCmd, Command: []string{"python3", "-m", "http.server", "8080"}, Cwd: apispec.NewOptString("/workspace"), }), Ingress: apispec.SandboxAppServiceIngress{ Public: true, Routes: []apispec.SandboxAppServiceRoute{ { ID: "api", PathPrefix: apispec.NewOptString("/api"), Methods: []string{"GET", "POST"}, Auth: apispec.NewOptSandboxAppServiceRouteAuth(apispec.SandboxAppServiceRouteAuth{ Mode: apispec.SandboxAppServiceRouteAuthModeBearer, BearerTokenSHA256: apispec.NewOptString(hex.EncodeToString(tokenHash[:])), }), TimeoutSeconds: apispec.NewOptInt32(30), Resume: true, }, }, }, }, }) if err != nil { log.Fatal(err) } fmt.Printf("services: %d\n", len(resp.Services))
Delete One Service#
The public API replaces the full service list; the CLI wraps that flow for deleting one service by ID.
bashs0 sandbox service delete api -s sb_abc123
Clear Services#
Clearing services removes public HTTP exposure from the sandbox.
go_, err := sandbox.ClearServices(ctx) if err != nil { log.Fatal(err) }
Set Services At Claim Time#
You can attach services when claiming a sandbox. The same service schema is accepted at claim time and by PUT /services.
yamltemplate: default config: auto_resume: true services: - id: api port: 8080 runtime: type: cmd command: - python3 - -m - http.server - "8080" cwd: /workspace ingress: public: true routes: - id: api path_prefix: /api rewrite_prefix: / methods: [GET, POST] resume: true
gosandbox, err := client.ClaimSandbox(ctx, "default", sandbox0.WithSandboxAutoResume(true), sandbox0.WithSandboxServices([]apispec.SandboxAppService{ { ID: "api", Port: apispec.NewOptInt32(8080), Runtime: apispec.NewOptSandboxAppServiceRuntime(apispec.SandboxAppServiceRuntime{ Type: apispec.SandboxAppServiceRuntimeTypeCmd, Command: []string{"python3", "-m", "http.server", "8080"}, Cwd: apispec.NewOptString("/workspace"), }), Ingress: apispec.SandboxAppServiceIngress{ Public: true, Routes: []apispec.SandboxAppServiceRoute{ { ID: "api", PathPrefix: apispec.NewOptString("/api"), RewritePrefix: apispec.NewOptString("/"), Methods: []string{"GET", "POST"}, Resume: true, }, }, }, }, }), ) if err != nil { log.Fatal(err) }
Auto-Resume#
Public service auto-resume has three requirements:
- sandbox
auto_resumemust be true - matched route
resumemust be true - the matched service must be restartable with
runtime.type: cmdorruntime.type: function
All three requirements are needed for public URL requests to wake a paused sandbox and make the backing service available.
For a paused sandbox, Sandbox0 creates a new runtime pod for the same sandbox identity and restores the latest rootfs checkpoint. The sandbox ID and service public_url stay unchanged until the sandbox is deleted or hard_ttl expires. For cmd services, the command is started again in the new runtime pod. For function services, there is still no long-running listener; each request executes the handler after the sandbox runtime is available.
Notes#
PUT /servicesreplaces the entire service list.- Public traffic must match a route on a public service.
- If
methodsis empty, all methods are allowed. - Route auth stores SHA-256 digests, not plaintext tokens.
- Route rate limits use the gateway rate limit backend. Self-hosted deployments default to process-local memory; configure
Sandbox0Infra.spec.redisfor shared Redis-backed limits across gateway replicas.
Next Steps#
Sandbox Functions
Expose public handlers without running long-lived HTTP listeners.
Contexts
Create and inspect the command processes that can back services.