diff --git a/src/content/docs/containers/get-started.mdx b/src/content/docs/containers/get-started.mdx
index d8f679e99e80e42..aa6cb2521ceba1e 100644
--- a/src/content/docs/containers/get-started.mdx
+++ b/src/content/docs/containers/get-started.mdx
@@ -5,7 +5,7 @@ sidebar:
order: 2
---
-import { WranglerConfig, PackageManagers } from "~/components";
+import { Render, PackageManagers, WranglerConfig } from "~/components";
In this guide, you will deploy a Worker that can make requests to one or more Containers in response to end-user requests.
In this example, each container runs a small webserver written in Go.
@@ -17,10 +17,9 @@ This example Worker should give you a sense for simple Container use, and provid
### Ensure Docker is running locally
In this guide, we will build and push a container image alongside your Worker code. By default, this process uses
-[Docker](https://www.docker.com/) to do so. You must have Docker running locally when you run `wrangler deploy`. For most people, the best way to install Docker is to follow the [docs for installing Docker Desktop](https://docs.docker.com/desktop/). Other tools like [Colima](https://github.com/abiosoft/colima) may also work.
+[Docker](https://www.docker.com/) to do so.
-You can check that Docker is running properly by running the `docker info` command in your terminal. If Docker is running, the command will succeed. If Docker is not running,
-the `docker info` command will hang or return an error including the message "Cannot connect to the Docker daemon".
+
{/* FUTURE CHANGE: Add some image you can use if you don't have Docker running. */}
{/* FUTURE CHANGE: Link to docs on alternative build/push options */}
diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx
index 6799a789554b406..dc4db5d1a5e2d9a 100644
--- a/src/content/docs/sandbox/api/index.mdx
+++ b/src/content/docs/sandbox/api/index.mdx
@@ -17,7 +17,7 @@ import { getSandbox } from '@cloudflare/sandbox';
const sandbox = getSandbox(env.Sandbox, 'user-123');
```
-The sandbox ID should be unique per user or session. The same ID will always return the same sandbox instance with persistent state.
+The same sandbox ID will always return the same sandbox instance. You can architect your application to use a single sandbox ID for multiple users, or use unique IDs per user or session. Using unique sandbox IDs per user is recommended if you are providing code generation or execution capabilities directly to your users.
## API organization
diff --git a/src/content/docs/sandbox/api/ports.mdx b/src/content/docs/sandbox/api/ports.mdx
index ffd6fb522674680..73a1aeac27c6573 100644
--- a/src/content/docs/sandbox/api/ports.mdx
+++ b/src/content/docs/sandbox/api/ports.mdx
@@ -7,6 +7,10 @@ sidebar:
import { TypeScriptExample } from "~/components";
+:::note[Production requires custom domain]
+Preview URLs require a custom domain with wildcard DNS routing in production. See [Production Deployment](/sandbox/guides/production-deployment/).
+:::
+
Expose services running in your sandbox via public preview URLs. See [Preview URLs concept](/sandbox/concepts/preview-urls/) for details.
## Methods
@@ -32,7 +36,7 @@ await sandbox.startProcess('python -m http.server 8000');
const exposed = await sandbox.exposePort(8000);
console.log('Available at:', exposed.exposedAt);
-// https://abc123-8000.sandbox.workers.dev
+// https://8000-abc123.example.com
// Multiple services with names
await sandbox.startProcess('node api.js');
diff --git a/src/content/docs/sandbox/concepts/architecture.mdx b/src/content/docs/sandbox/concepts/architecture.mdx
index 64849c42c68b151..08d17f298736167 100644
--- a/src/content/docs/sandbox/concepts/architecture.mdx
+++ b/src/content/docs/sandbox/concepts/architecture.mdx
@@ -5,39 +5,37 @@ sidebar:
order: 1
---
-The Sandbox SDK provides isolated code execution environments on Cloudflare's edge network. It combines three Cloudflare technologies:
+Sandbox SDK lets you execute untrusted code safely from your Workers. It combines three Cloudflare technologies to provide secure, stateful, and isolated execution:
-- **Workers** - JavaScript runtime at the edge
-- **Durable Objects** - Stateful compute with persistent storage
-- **Containers** - Isolated execution environments with full Linux capabilities
+- **Workers** - Your application logic that calls the Sandbox SDK
+- **Durable Objects** - Persistent sandbox instances with unique identities
+- **Containers** - Isolated Linux environments where code actually runs
-## Three-layer architecture
+## Architecture overview
-```
-┌─────────────────────────────────────────────────────────┐
-│ Your Application │
-│ (Cloudflare Worker) │
-└───────────────────────────┬─────────────────────────────┘
- ├─ getSandbox()
- ├─ exec()
- ├─ writeFile()
- │
- ┌────────────────▼──────────────────┐
- │ Container-enabled Durable Object │
- │ (SDK methods via RPC from Worker) │
- └───────────────────────────────────┘
- │ HTTP/JSON
- │
- ┌───────▼───────┐
- │ Durable Object │ Layer 2: State Management
- │ (Persistent) │
- └───────┬───────┘
- │ Container Protocol
- │
- ┌───────▼───────┐
- │ Container │ Layer 3: Isolated Execution
- │ (Linux + Bun) │
- └───────────────┘
+```mermaid
+flowchart TB
+ accTitle: Sandbox SDK Architecture
+ accDescr: Three-layer architecture showing how Cloudflare Sandbox SDK combines Workers, Durable Objects, and Containers for secure code execution
+
+ subgraph UserSpace["Your Worker"]
+ Worker["Application code using the methods exposed by the Sandbox SDK"]
+ end
+
+ subgraph SDKSpace["Sandbox SDK Implementation"]
+ DO["Sandbox Durable Object routes requests & maintains state"]
+ Container["Isolated Ubuntu container executes untrusted code safely"]
+
+ DO -->|HTTP API| Container
+ end
+
+ Worker -->|RPC call via the Durable Object stub returned by `getSandbox`| DO
+
+ style UserSpace fill:#fff8f0,stroke:#f6821f,stroke-width:2px
+ style SDKSpace fill:#f5f5f5,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5
+ style Worker fill:#ffe8d1,stroke:#f6821f,stroke-width:2px
+ style DO fill:#dce9f7,stroke:#1d8cf8,stroke-width:2px
+ style Container fill:#d4f4e2,stroke:#17b26a,stroke-width:2px
```
### Layer 1: Client SDK
@@ -70,7 +68,7 @@ export class Sandbox extends DurableObject {
**Why Durable Objects**:
- **Persistent identity** - Same sandbox ID always routes to same instance
-- **State management** - Filesystem and processes persist between requests
+- **Container management** - Durable Object owns and manages the container lifecycle
- **Geographic distribution** - Sandboxes run close to users
- **Automatic scaling** - Cloudflare manages provisioning
@@ -82,9 +80,8 @@ Executes code in isolation with full Linux capabilities.
**Why containers**:
-- **True isolation** - Process-level isolation with namespaces
-- **Full environment** - Real Linux with Python, Node.js, Git, etc.
-- **Resource limits** - CPU, memory, disk constraints
+- **VM-based isolation** - Each sandbox runs in its own VM
+- **Full environment** - Ubuntu Linux with Python, Node.js, Git, etc.
## Request flow
@@ -99,32 +96,6 @@ await sandbox.exec("python script.py");
3. **Container Runtime** validates inputs, executes command, captures output
4. **Response flows back** through all layers with proper error transformation
-## State persistence
-
-Sandboxes maintain state across requests:
-
-**Filesystem**:
-
-```typescript
-// Request 1
-await sandbox.writeFile("/workspace/data.txt", "hello");
-
-// Request 2 (minutes later)
-const file = await sandbox.readFile("/workspace/data.txt");
-// Returns 'hello' - file persisted
-```
-
-**Processes**:
-
-```typescript
-// Request 1
-await sandbox.startProcess("node server.js");
-
-// Request 2 (minutes later)
-const processes = await sandbox.listProcesses();
-// Server still running
-```
-
## Related resources
- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - How sandboxes are created and managed
diff --git a/src/content/docs/sandbox/concepts/containers.mdx b/src/content/docs/sandbox/concepts/containers.mdx
index d80298576c0e1c7..8dd6c75521e388d 100644
--- a/src/content/docs/sandbox/concepts/containers.mdx
+++ b/src/content/docs/sandbox/concepts/containers.mdx
@@ -5,36 +5,11 @@ sidebar:
order: 3
---
-Each sandbox runs in an isolated Linux container based on Ubuntu 22.04.
+Each sandbox runs in an isolated Linux container with Python, Node.js, and common development tools pre-installed. For a complete list of pre-installed software and how to customize the container image, see [Dockerfile reference](/sandbox/configuration/dockerfile/).
-## Pre-installed software
+## Runtime software installation
-The base container comes pre-packaged with a full development environment:
-
-**Languages and runtimes**:
-- Python 3.11 (with pip)
-- Node.js 20 LTS (with npm)
-- Bun (JavaScript/TypeScript runtime)
-
-**Python packages**:
-- NumPy - Numerical computing
-- pandas - Data analysis
-- Matplotlib - Plotting and visualization
-- IPython - Interactive Python
-
-**Development tools**:
-- Git - Version control
-- Build tools (gcc, make, pkg-config)
-- Text editors (vim, nano)
-- Process monitoring (htop, procps)
-
-**Utilities**:
-- curl, wget - HTTP clients
-- jq - JSON processor
-- Network tools (ping, dig, netstat)
-- Compression (zip, unzip)
-
-Install additional software at runtime or [customize the base image](/sandbox/configuration/dockerfile/):
+Install additional software at runtime using standard package managers:
```bash
# Python packages
@@ -43,8 +18,8 @@ pip install scikit-learn tensorflow
# Node.js packages
npm install express
-# System packages
-apt-get install redis-server
+# System packages (requires apt-get update first)
+apt-get update && apt-get install -y redis-server
```
## Filesystem
diff --git a/src/content/docs/sandbox/concepts/index.mdx b/src/content/docs/sandbox/concepts/index.mdx
index 422eb9220fb23c9..abebbdee249c4ec 100644
--- a/src/content/docs/sandbox/concepts/index.mdx
+++ b/src/content/docs/sandbox/concepts/index.mdx
@@ -7,8 +7,6 @@ sidebar:
These pages explain how the Sandbox SDK works, why it's designed the way it is, and the concepts you need to understand to use it effectively.
-## Available concepts
-
- [Architecture](/sandbox/concepts/architecture/) - How the SDK is structured and why
- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Understanding sandbox states and behavior
- [Container runtime](/sandbox/concepts/containers/) - How code executes in isolated containers
diff --git a/src/content/docs/sandbox/concepts/preview-urls.mdx b/src/content/docs/sandbox/concepts/preview-urls.mdx
index 9dbcc26226895b0..c6419f084f11324 100644
--- a/src/content/docs/sandbox/concepts/preview-urls.mdx
+++ b/src/content/docs/sandbox/concepts/preview-urls.mdx
@@ -5,98 +5,99 @@ sidebar:
order: 5
---
-Preview URLs provide public access to services running inside sandboxes. When you expose a port, you get a unique HTTPS URL that proxies requests to your service.
+:::note[Production requires custom domain]
+Preview URLs work in local development without configuration. For production, you need a custom domain with wildcard DNS routing. See [Production Deployment](/sandbox/guides/production-deployment/).
+:::
+
+Preview URLs provide public HTTPS access to services running inside sandboxes. When you expose a port, you get a unique URL that proxies requests to your service.
```typescript
-await sandbox.startProcess('python -m http.server 8000');
+await sandbox.startProcess("python -m http.server 8000");
const exposed = await sandbox.exposePort(8000);
console.log(exposed.exposedAt);
-// https://abc123-8000.sandbox.workers.dev
+// Production: https://8000-abc123.example.com
+// Local dev: http://localhost:8787/...
```
-## URL format
+## URL Format
-Preview URLs follow this pattern:
+**Production**: `https://{port}-{sandbox-id}.yourdomain.com`
-```
-https://{sandbox-id}-{port}.sandbox.workers.dev
-```
+- Port 8080: `https://8080-abc123.example.com`
+- Port 3000: `https://3000-abc123.example.com`
-**Examples**:
-- Port 3000: `https://abc123-3000.sandbox.workers.dev`
-- Port 8080: `https://abc123-8080.sandbox.workers.dev`
+**Local development**: `http://localhost:8787/...`
-**URL stability**: URLs remain the same for a given sandbox ID and port. You can share, bookmark, or use them in webhooks.
+Preview URLs remain stable while a port is exposed and can be shared during that time. However, if you unexpose and re-expose a port, a new random token is generated and the URL changes. For persistent URLs, keep ports exposed for the duration you need them accessible.
-## Request routing
+## Request Routing
-```
-User's Browser
- ↓ HTTPS
-Your Worker
- ↓
-Durable Object (sandbox)
- ↓ HTTP
-Your Service (on exposed port)
-```
-
-**Important**: You must handle preview URL routing in your Worker using `proxyToSandbox()`:
+You must call `proxyToSandbox()` first in your Worker's fetch handler to route preview URL requests:
```typescript
import { proxyToSandbox, getSandbox } from "@cloudflare/sandbox";
export default {
- async fetch(request, env) {
- // Route preview URL requests to sandboxes
- const proxyResponse = await proxyToSandbox(request, env);
- if (proxyResponse) return proxyResponse;
-
- // Your custom routes here
- // ...
- }
+ async fetch(request, env) {
+ // Handle preview URL routing first
+ const proxyResponse = await proxyToSandbox(request, env);
+ if (proxyResponse) return proxyResponse;
+
+ // Your application routes
+ // ...
+ },
};
```
-Without this, preview URLs won't work.
+Requests flow: Browser → Your Worker → Durable Object (sandbox) → Your Service.
-## Multiple ports
+## Multiple Ports
Expose multiple services simultaneously:
```typescript
-await sandbox.startProcess('node api.js'); // Port 3000
-await sandbox.startProcess('node admin.js'); // Port 3001
+await sandbox.startProcess("node api.js"); // Port 3000
+await sandbox.startProcess("node admin.js"); // Port 3001
-const api = await sandbox.exposePort(3000, { name: 'api' });
-const admin = await sandbox.exposePort(3001, { name: 'admin' });
+const api = await sandbox.exposePort(3000, { name: "api" });
+const admin = await sandbox.exposePort(3001, { name: "admin" });
// Each gets its own URL:
-// https://abc123-3000.sandbox.workers.dev
-// https://abc123-3001.sandbox.workers.dev
+// https://3000-abc123.example.com
+// https://3001-abc123.example.com
```
-## What works
+## What Works
- HTTP/HTTPS requests
-- WebSocket (WSS) via HTTP upgrade
- Server-Sent Events
- All HTTP methods (GET, POST, PUT, DELETE, etc.)
- Request and response headers
-## What doesn't work
+## What Does Not Work
- Raw TCP/UDP connections
- Custom protocols (must wrap in HTTP)
-- Ports 80/443 (use 1024+)
+- WebSocket connections
+- Ports outside range 1024-65535
+- Port 3000 (used internally by the SDK)
## Security
:::caution
-Preview URLs are publicly accessible. Anyone with the URL can access your service.
+Preview URLs are publicly accessible by default, but require a valid access token that is generated when you expose a port.
:::
-**Add authentication in your service**:
+**Built-in security**:
+
+- **Token-based access** - Each exposed port gets a unique token in the URL (for example, `https://8080-sandbox-abc123token.example.com`)
+- **HTTPS in production** - All traffic is encrypted with automatic TLS
+- **Unpredictable URLs** - Tokens are randomly generated and difficult to guess
+
+**Add application-level authentication**:
+
+For additional security, implement authentication within your application:
```python
from flask import Flask, request, abort
@@ -105,20 +106,18 @@ app = Flask(__name__)
@app.route('/data')
def get_data():
- token = request.headers.get('Authorization')
- if token != 'Bearer secret-token':
+ # Check for your own authentication token
+ auth_token = request.headers.get('Authorization')
+ if auth_token != 'Bearer your-secret-token':
abort(401)
return {'data': 'protected'}
```
-**Security features**:
-- All traffic is HTTPS (automatic TLS)
-- URLs use random sandbox IDs (hard to guess)
-- You control authentication in your service
+This adds a second layer of security on top of the URL token.
## Troubleshooting
-### URL not accessible
+### URL Not Accessible
Check if service is running and listening:
@@ -131,27 +130,19 @@ const ports = await sandbox.getExposedPorts();
// 3. Is service binding to 0.0.0.0 (not 127.0.0.1)?
// Good:
-app.run(host='0.0.0.0', port=3000)
+app.run((host = "0.0.0.0"), (port = 3000));
// Bad (localhost only):
-app.run(host='127.0.0.1', port=3000)
+app.run((host = "127.0.0.1"), (port = 3000));
```
-## Best practices
-
-**Service design**:
-- Bind to `0.0.0.0` to make accessible
-- Add authentication (don't rely on URL secrecy)
-- Include health check endpoints
-- Handle CORS if accessed from browsers
+### Production Errors
-**Cleanup**:
-- Unexpose ports when done: `await sandbox.unexposePort(port)`
-- Stop processes: `await sandbox.killAllProcesses()`
+For custom domain issues, see [Production Deployment troubleshooting](/sandbox/guides/production-deployment/#troubleshooting).
-## Local development
+### Local Development
-:::caution[Local development only]
+:::caution[Local development limitation]
When using `wrangler dev`, you must expose ports in your Dockerfile:
```dockerfile
@@ -167,7 +158,9 @@ Without `EXPOSE`, you'll see: `connect(): Connection refused: container port not
This is **only required for local development**. In production, all container ports are automatically accessible.
:::
-## Related resources
+## Related Resources
-- [Ports API reference](/sandbox/api/ports/) - Complete port exposure API
-- [Expose services guide](/sandbox/guides/expose-services/) - Practical patterns
+- [Production Deployment](/sandbox/guides/production-deployment/) - Set up custom domains for production
+- [Expose Services](/sandbox/guides/expose-services/) - Practical patterns for exposing ports
+- [Ports API](/sandbox/api/ports/) - Complete API reference
+- [Security Model](/sandbox/concepts/security/) - Security best practices
diff --git a/src/content/docs/sandbox/concepts/sandboxes.mdx b/src/content/docs/sandbox/concepts/sandboxes.mdx
index 335d52f0139f472..98f04840e085835 100644
--- a/src/content/docs/sandbox/concepts/sandboxes.mdx
+++ b/src/content/docs/sandbox/concepts/sandboxes.mdx
@@ -10,7 +10,7 @@ A sandbox is an isolated execution environment where your code runs. Each sandbo
- Has a unique identifier (sandbox ID)
- Contains an isolated filesystem
- Runs in a dedicated Linux container
-- Persists state between requests
+- Maintains state while the container is active
- Exists as a Cloudflare Durable Object
## Lifecycle states
@@ -28,11 +28,11 @@ await sandbox.exec('echo "Hello"'); // First request creates sandbox
### Active
-The sandbox is running and processing requests. Filesystem, processes, and environment variables persist across requests.
+The sandbox container is running and processing requests. All state remains available: files, running processes, shell sessions, and environment variables.
### Idle
-After inactivity, the sandbox may enter idle state. Filesystem state is preserved, but the container may be paused. Next request triggers a warm start.
+After a period of inactivity, the container stops to free resources. When the next request arrives, a fresh container starts. All previous state is lost and the environment resets to its initial state.
### Destruction
@@ -43,19 +43,23 @@ await sandbox.destroy();
// All files, processes, and state deleted permanently
```
-## Persistence
+## Container lifetime and state
-Between requests to the same sandbox:
+Sandbox state exists only while the container is active. Understanding this is critical for building reliable applications.
-**What persists**:
-- Files in `/workspace`, `/tmp`, `/home`
-- Background processes (started with `startProcess()`)
-- Code interpreter contexts and variables
-- Environment variables and port exposures
+**While the container is active** (typically minutes to hours of activity):
+- Files written to `/workspace`, `/tmp`, `/home` remain available
+- Background processes continue running
+- Shell sessions maintain their working directory and environment
+- Code interpreter contexts retain variables and imports
-**What doesn't persist**:
-- Nothing survives `destroy()`
-- Background processes may stop after container restarts (rare)
+**When the container stops** (due to inactivity or explicit destruction):
+- All files are deleted
+- All processes terminate
+- All shell state resets
+- All code interpreter contexts are cleared
+
+The next request creates a fresh container with a clean environment.
## Naming strategies
@@ -65,7 +69,7 @@ Between requests to the same sandbox:
const sandbox = getSandbox(env.Sandbox, `user-${userId}`);
```
-User's work persists across sessions. Good for interactive environments, playgrounds, and notebooks.
+User's work persists while actively using the sandbox. Good for interactive environments, playgrounds, and notebooks where users work continuously.
### Per-session sandboxes
@@ -111,18 +115,19 @@ try {
**Don't destroy**: Personal environments, long-running services
-### Failure recovery
+### Handling container restarts
-If container crashes or Durable Object is evicted (rare):
+Containers restart after inactivity or failures. Design your application to handle state loss:
```typescript
-try {
- await sandbox.exec('command');
-} catch (error) {
- if (error.message.includes('container') || error.message.includes('connection')) {
- await sandbox.exec('command'); // Retry - container recreates
- }
+// Check if required files exist before using them
+const files = await sandbox.listFiles('/workspace');
+if (!files.includes('data.json')) {
+ // Reinitialize: container restarted and lost previous state
+ await sandbox.writeFile('/workspace/data.json', initialData);
}
+
+await sandbox.exec('python process.py');
```
## Best practices
@@ -131,7 +136,7 @@ try {
- **Clean up temporary sandboxes** - Always destroy when done
- **Reuse long-lived sandboxes** - One per user is often sufficient
- **Batch operations** - Combine commands: `npm install && npm test && npm build`
-- **Handle failures** - Design for container restarts
+- **Design for ephemeral state** - Containers restart after inactivity, losing all state
## Related resources
diff --git a/src/content/docs/sandbox/concepts/sessions.mdx b/src/content/docs/sandbox/concepts/sessions.mdx
index 035617fb8090a18..602ceaa021199ca 100644
--- a/src/content/docs/sandbox/concepts/sessions.mdx
+++ b/src/content/docs/sandbox/concepts/sessions.mdx
@@ -12,7 +12,7 @@ Sessions are bash shell execution contexts within a sandbox. Think of them like
## Default session
-Every sandbox has a default session that maintains shell state across commands:
+Every sandbox has a default session that maintains shell state between commands while the container is active:
```typescript
const sandbox = getSandbox(env.Sandbox, 'my-sandbox');
@@ -25,7 +25,7 @@ await sandbox.exec("export MY_VAR=hello");
await sandbox.exec("echo $MY_VAR"); // Output: hello
```
-Shell state persists: working directory, environment variables, exported variables all carry over between commands.
+Working directory, environment variables, and exported variables carry over between commands. This state resets if the container restarts due to inactivity.
## Creating sessions
diff --git a/src/content/docs/sandbox/configuration/dockerfile.mdx b/src/content/docs/sandbox/configuration/dockerfile.mdx
index bc0e342fde992f5..55b151ace9aff98 100644
--- a/src/content/docs/sandbox/configuration/dockerfile.mdx
+++ b/src/content/docs/sandbox/configuration/dockerfile.mdx
@@ -22,11 +22,11 @@ Always match the Docker image version to your npm package version. If you're usi
**What's included:**
- Ubuntu 22.04 LTS base
-- Python 3.11+ with pip
-- Bun (JavaScript/TypeScript runtime)
-- Git, curl, wget, and common CLI tools
-- Pre-installed Python packages: pandas, numpy, matplotlib
-- System libraries for most common use cases
+- Python 3.11 with pip and venv
+- Node.js 20 LTS with npm
+- Bun 1.x (JavaScript/TypeScript runtime)
+- Pre-installed Python packages: matplotlib, numpy, pandas, ipython
+- System utilities: curl, wget, git, jq, zip, unzip, file, procps, ca-certificates
## Creating a custom image
@@ -57,8 +57,8 @@ Update `wrangler.jsonc` to reference your Dockerfile:
{
"containers": [
{
- "binding": "CONTAINER",
- "dockerfile": "./Dockerfile",
+ "class_name": "Sandbox",
+ "image": "./Dockerfile",
},
],
}
diff --git a/src/content/docs/sandbox/configuration/environment-variables.mdx b/src/content/docs/sandbox/configuration/environment-variables.mdx
index a70cfca35e774e2..53999922d678050 100644
--- a/src/content/docs/sandbox/configuration/environment-variables.mdx
+++ b/src/content/docs/sandbox/configuration/environment-variables.mdx
@@ -7,39 +7,61 @@ sidebar:
Pass configuration, secrets, and runtime settings to your sandboxes using environment variables.
-### Command and process variables
+## Three ways to set environment variables
-Pass environment variables when executing commands or starting processes:
+The Sandbox SDK provides three methods for setting environment variables, each suited for different use cases:
+
+### 1. Sandbox-level with setEnvVars()
+
+Set environment variables globally for all commands in the sandbox:
+
+```typescript
+const sandbox = getSandbox(env.Sandbox, "my-sandbox");
+
+// Set once, available for all subsequent commands
+await sandbox.setEnvVars({
+ DATABASE_URL: env.DATABASE_URL,
+ API_KEY: env.API_KEY,
+});
+
+await sandbox.exec("python migrate.py"); // Has DATABASE_URL and API_KEY
+await sandbox.exec("python seed.py"); // Has DATABASE_URL and API_KEY
+```
+
+**Use when:** You need the same environment variables for multiple commands.
+
+### 2. Per-command with exec() options
+
+Pass environment variables for a specific command:
```typescript
-// Commands
await sandbox.exec("node app.js", {
env: {
NODE_ENV: "production",
- API_KEY: env.API_KEY, // Pass from Worker env
PORT: "3000",
},
});
-// Background processes (same syntax)
+// Also works with startProcess()
await sandbox.startProcess("python server.py", {
env: {
DATABASE_URL: env.DATABASE_URL,
- SECRET_KEY: env.SECRET_KEY,
},
});
```
-### Session-level variables
+**Use when:** You need different environment variables for different commands, or want to override sandbox-level variables.
-Set environment variables for all commands in a session:
+### 3. Session-level with createSession()
-```typescript
-const session = await sandbox.createSession();
+Create an isolated session with its own environment variables:
-await session.setEnvVars({
- DATABASE_URL: env.DATABASE_URL,
- SECRET_KEY: env.SECRET_KEY,
+```typescript
+const session = await sandbox.createSession({
+ env: {
+ DATABASE_URL: env.DATABASE_URL,
+ SECRET_KEY: env.SECRET_KEY,
+ },
});
// All commands in this session have these vars
@@ -47,16 +69,25 @@ await session.exec("python migrate.py");
await session.exec("python seed.py");
```
+**Use when:** You need isolated execution contexts with different environment variables running concurrently.
+
## Common patterns
### Pass Worker secrets to sandbox
-Securely pass secrets from Worker environment:
+Securely pass secrets from your Worker to the sandbox. First, set secrets using Wrangler:
+
+```bash
+wrangler secret put OPENAI_API_KEY
+wrangler secret put DATABASE_URL
+```
+
+Then pass them to your sandbox:
```typescript
interface Env {
Sandbox: DurableObjectNamespace;
- OPENAI_API_KEY: string; // Set with `wrangler secret put`
+ OPENAI_API_KEY: string;
DATABASE_URL: string;
}
@@ -64,128 +95,77 @@ export default {
async fetch(request: Request, env: Env): Promise {
const sandbox = getSandbox(env.Sandbox, "user-sandbox");
- const result = await sandbox.exec("python analyze.py", {
+ // Option 1: Set globally for all commands
+ await sandbox.setEnvVars({
+ OPENAI_API_KEY: env.OPENAI_API_KEY,
+ DATABASE_URL: env.DATABASE_URL,
+ });
+ await sandbox.exec("python analyze.py");
+
+ // Option 2: Pass per-command
+ await sandbox.exec("python analyze.py", {
env: {
OPENAI_API_KEY: env.OPENAI_API_KEY,
- DATABASE_URL: env.DATABASE_URL,
},
});
- return Response.json({ result });
+ return Response.json({ success: true });
},
};
```
-### Default values with spreading
-
-Combine default and command-specific variables:
+### Combine default and specific variables
```typescript
-const defaults = {
- NODE_ENV: env.ENVIRONMENT || "production",
- LOG_LEVEL: "info",
- TZ: "UTC",
-};
+const defaults = { NODE_ENV: "production", LOG_LEVEL: "info" };
await sandbox.exec("npm start", {
- env: {
- ...defaults,
- PORT: "3000", // Command-specific override
- API_KEY: env.API_KEY,
- },
-});
-```
-
-## Environment variable precedence
-
-When the same variable is set at multiple levels:
-
-1. **Command-level** (highest) - Passed to `exec()` or `startProcess()`
-2. **Session-level** - Set with `setEnvVars()`
-3. **Container default** - Built into the Docker image
-4. **System default** (lowest) - Operating system defaults
-
-Example:
-
-```typescript
-// In Dockerfile: ENV NODE_ENV=development
-// In session: await sandbox.setEnvVars({ NODE_ENV: 'staging' });
-
-// In command (overrides all):
-await sandbox.exec("node app.js", {
- env: { NODE_ENV: "production" }, // This wins
+ env: { ...defaults, PORT: "3000", API_KEY: env.API_KEY },
});
```
-## Security best practices
+### Multiple isolated sessions
-### Never hardcode secrets
-
-**Bad** - Secrets in code:
+Run different tasks with different environment variables concurrently:
```typescript
-await sandbox.exec("python app.py", {
- env: {
- API_KEY: "sk-1234567890abcdef", // NEVER DO THIS
- },
+// Production database session
+const prodSession = await sandbox.createSession({
+ env: { DATABASE_URL: env.PROD_DATABASE_URL },
});
-```
-
-**Good** - Secrets from Worker environment:
-```typescript
-await sandbox.exec("python app.py", {
- env: {
- API_KEY: env.API_KEY, // From Wrangler secret
- },
+// Staging database session
+const stagingSession = await sandbox.createSession({
+ env: { DATABASE_URL: env.STAGING_DATABASE_URL },
});
-```
-
-Set secrets with Wrangler:
-
-```bash
-wrangler secret put API_KEY
-```
-
-## Debugging
-List all environment variables:
-
-```typescript
-const result = await sandbox.exec("env");
-console.log(result.stdout);
+// Run migrations on both concurrently
+await Promise.all([
+ prodSession.exec("python migrate.py"),
+ stagingSession.exec("python migrate.py"),
+]);
```
-Check specific variable:
-
-```typescript
-const result = await sandbox.exec("echo $NODE_ENV");
-console.log("NODE_ENV:", result.stdout.trim());
-```
+## Environment variable precedence
-## Troubleshooting
+When the same variable is set at multiple levels, the most specific level takes precedence:
-### Variable not set
+1. **Command-level** (highest) - Passed to `exec()` or `startProcess()` options
+2. **Sandbox or session-level** - Set with `setEnvVars()`
+3. **Container default** - Built into the Docker image with `ENV`
+4. **System default** (lowest) - Operating system defaults
-Verify the variable is being passed:
+Example:
```typescript
-console.log("Worker env:", env.API_KEY ? "Set" : "Missing");
-
-const result = await sandbox.exec("env | grep API_KEY", {
- env: { API_KEY: env.API_KEY },
-});
-console.log("Sandbox:", result.stdout);
-```
-
-### Shell expansion issues
+// In Dockerfile: ENV NODE_ENV=development
-Use runtime-specific access instead of shell variables:
+// Sandbox-level
+await sandbox.setEnvVars({ NODE_ENV: "staging" });
-```typescript
-// Instead of: await sandbox.exec('echo $NODE_ENV')
-await sandbox.exec('node -e "console.log(process.env.NODE_ENV)"', {
- env: { NODE_ENV: "production" },
+// Command-level overrides all
+await sandbox.exec("node app.js", {
+ env: { NODE_ENV: "production" }, // This wins
});
```
diff --git a/src/content/docs/sandbox/configuration/index.mdx b/src/content/docs/sandbox/configuration/index.mdx
index fc9a582aa34fe37..4b064e8a701cea4 100644
--- a/src/content/docs/sandbox/configuration/index.mdx
+++ b/src/content/docs/sandbox/configuration/index.mdx
@@ -14,91 +14,31 @@ Configure your Sandbox SDK deployment with Wrangler, customize container images,
- Configure Durable Objects bindings, container images, and Worker settings in wrangler.jsonc.
+ Configure Durable Objects bindings, container images, and Worker settings in
+ wrangler.jsonc.
- Customize the sandbox container image with your own packages, tools, and configurations.
+ Customize the sandbox container image with your own packages, tools, and
+ configurations.
Pass configuration and secrets to your sandboxes using environment variables.
-## Quick reference
-
-### Essential wrangler.jsonc settings
-
-```jsonc
-{
- "name": "my-worker",
- "main": "src/index.ts",
- "compatibility_date": "2024-09-02",
- "compatibility_flags": ["nodejs_compat"],
- "durable_objects": {
- "bindings": [
- {
- "name": "Sandbox",
- "class_name": "Sandbox",
- "script_name": "@cloudflare/sandbox"
- }
- ]
- },
- "containers": [
- {
- "binding": "CONTAINER",
- "image": "ghcr.io/cloudflare/sandbox-runtime:latest"
- }
- ]
-}
-```
-
-### Common Dockerfile customizations
-
-```dockerfile
-FROM ghcr.io/cloudflare/sandbox-runtime:latest
-
-# Install additional Python packages
-RUN pip install scikit-learn tensorflow pandas
-
-# Install Node.js packages globally
-RUN npm install -g typescript ts-node
-
-# Install system packages
-RUN apt-get update && apt-get install -y postgresql-client
-
-# Add custom scripts
-COPY ./scripts /usr/local/bin/
-```
-
-### Environment variables
-
-```typescript
-// Pass to sandbox at creation
-const sandbox = getSandbox(env.Sandbox, 'my-sandbox');
-
-// Configure environment for commands
-await sandbox.exec('node app.js', {
- env: {
- NODE_ENV: 'production',
- API_KEY: env.API_KEY,
- DATABASE_URL: env.DATABASE_URL
- }
-});
-```
-
## Related resources
- [Get Started guide](/sandbox/get-started/) - Initial setup walkthrough
diff --git a/src/content/docs/sandbox/configuration/wrangler.mdx b/src/content/docs/sandbox/configuration/wrangler.mdx
index 5e79c88d944e869..443d9358798e0a4 100644
--- a/src/content/docs/sandbox/configuration/wrangler.mdx
+++ b/src/content/docs/sandbox/configuration/wrangler.mdx
@@ -40,137 +40,13 @@ The minimum required configuration for using Sandbox SDK:
## Required settings
-### containers
+The Sandbox SDK is built on Cloudflare Containers. Your configuration requires three sections:
-Each container is backed by its own Durable Object. The container image contains your runtime environment.
+1. **containers** - Define the container image (your runtime environment)
+2. **durable_objects.bindings** - Bind the Sandbox Durable Object to your Worker
+3. **migrations** - Initialize the Durable Object class
-```jsonc
-{
- "containers": [
- {
- "class_name": "Sandbox",
- "image": "./Dockerfile",
- },
- ],
-}
-```
-
-**Parameters**:
-
-- **class_name** (string, required) - Must match the `class_name` of the Durable Object.
-- **image** (string, required) - The Docker image to use. Must match your npm package version.
-
-See [Dockerfile reference](/sandbox/configuration/dockerfile/) for information on customizing your Docker image.
-
-### durable_objects.bindings
-
-Bind the Sandbox Durable Object to your Worker:
-
-```jsonc
-{
- "durable_objects": {
- "bindings": [
- {
- "class_name": "Sandbox",
- "name": "Sandbox",
- },
- ],
- },
-}
-```
-
-**Parameters**:
-
-- **class_name** (string, required) - Must match the `class_name` of the container configuration.
-- **name** (string, required) - The binding name you'll use in your code. Conventionally `"Sandbox"`.
-
-### migrations
-
-Required for Durable Object initialization:
-
-```jsonc
-{
- "migrations": [
- {
- "new_sqlite_classes": ["Sandbox"],
- "tag": "v1",
- },
- ],
-}
-```
-
-This tells Cloudflare to initialize the Sandbox Durable Object with SQLite storage.
-
-## Optional settings
-
-These settings are illustrative and not required for basic usage.
-
-### Environment variables
-
-Pass configuration to your Worker:
-
-```jsonc
-{
- "vars": {
- "ENVIRONMENT": "production",
- "LOG_LEVEL": "info",
- },
-}
-```
-
-Access in your Worker:
-
-```typescript
-export default {
- async fetch(request: Request, env: Env): Promise {
- console.log(`Running in ${env.ENVIRONMENT} mode`);
- // ...
- },
-};
-```
-
-### Secrets
-
-Store sensitive values securely:
-
-```bash
-# Set secrets via CLI (never commit these)
-wrangler secret put ANTHROPIC_API_KEY
-wrangler secret put GITHUB_TOKEN
-wrangler secret put DATABASE_URL
-```
-
-Access like environment variables:
-
-```typescript
-interface Env {
- Sandbox: DurableObjectNamespace;
- ANTHROPIC_API_KEY: string;
- GITHUB_TOKEN: string;
-}
-```
-
-### Cron triggers
-
-Run sandboxes on a schedule:
-
-```jsonc
-{
- "triggers": {
- "crons": ["0 0 * * *"], // Daily at midnight
- },
-}
-```
-
-```typescript
-export default {
- async scheduled(event: ScheduledEvent, env: Env): Promise {
- const sandbox = getSandbox(env.Sandbox, "scheduled-task");
- await sandbox.exec("python3 /workspace/daily-report.py");
- await sandbox.destroy();
- },
-};
-```
+The minimal configuration shown above includes all required settings. For detailed configuration options, refer to the [Containers configuration documentation](/workers/wrangler/configuration/#containers).
## Troubleshooting
diff --git a/src/content/docs/sandbox/get-started.mdx b/src/content/docs/sandbox/get-started.mdx
index f1d496f64d1399d..746e3256c44394e 100644
--- a/src/content/docs/sandbox/get-started.mdx
+++ b/src/content/docs/sandbox/get-started.mdx
@@ -19,17 +19,9 @@ A simple API that can safely execute Python code and perform file operations in
### Ensure Docker is running locally
-Sandbox SDK uses [Docker](https://www.docker.com/) to build container images alongside your Worker. Docker must be running when you deploy or run locally.
+Sandbox SDK uses [Docker](https://www.docker.com/) to build container images alongside your Worker.
-Install Docker by following the [Docker Desktop installation guide](https://docs.docker.com/desktop/).
-
-Verify Docker is running:
-
-```sh
-docker info
-```
-
-If Docker is not running, this command will hang or return "Cannot connect to the Docker daemon".
+
## 1. Create a new project
@@ -56,61 +48,58 @@ cd my-sandbox
The template provides a minimal Worker that demonstrates core sandbox capabilities:
```typescript
-import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox';
+import { getSandbox, proxyToSandbox, type Sandbox } from "@cloudflare/sandbox";
-export { Sandbox } from '@cloudflare/sandbox';
+export { Sandbox } from "@cloudflare/sandbox";
type Env = {
- Sandbox: DurableObjectNamespace;
+ Sandbox: DurableObjectNamespace;
};
export default {
- async fetch(request: Request, env: Env): Promise {
- // Required for preview URLs (if you expose ports later)
- const proxyResponse = await proxyToSandbox(request, env);
- if (proxyResponse) return proxyResponse;
-
- const url = new URL(request.url);
-
- // Get or create a sandbox instance
- const sandbox = getSandbox(env.Sandbox, 'my-sandbox');
-
- // Execute Python code
- if (url.pathname === '/run') {
- const result = await sandbox.exec('python -c "print(2 + 2)"');
- return Response.json({
- output: result.stdout,
- success: result.success
- });
- }
-
- // Work with files
- if (url.pathname === '/file') {
- await sandbox.writeFile('/workspace/hello.txt', 'Hello, Sandbox!');
- const file = await sandbox.readFile('/workspace/hello.txt');
- return Response.json({
- content: file.content
- });
- }
-
- return new Response('Try /run or /file');
- },
+ async fetch(request: Request, env: Env): Promise {
+ const url = new URL(request.url);
+
+ // Get or create a sandbox instance
+ const sandbox = getSandbox(env.Sandbox, "my-sandbox");
+
+ // Execute Python code
+ if (url.pathname === "/run") {
+ const result = await sandbox.exec('python3 -c "print(2 + 2)"');
+ return Response.json({
+ output: result.stdout,
+ error: result.stderr,
+ exitCode: result.exitCode,
+ success: result.success,
+ });
+ }
+
+ // Work with files
+ if (url.pathname === "/file") {
+ await sandbox.writeFile("/workspace/hello.txt", "Hello, Sandbox!");
+ const file = await sandbox.readFile("/workspace/hello.txt");
+ return Response.json({
+ content: file.content,
+ });
+ }
+
+ return new Response("Try /run or /file");
+ },
};
```
**Key concepts**:
-- `getSandbox()` - Gets or creates a sandbox instance by ID
-- `proxyToSandbox()` - Required at the top for preview URLs to work
-- `sandbox.exec()` - Execute commands and capture output
-- `sandbox.writeFile()` / `readFile()` - File operations
+- `getSandbox()` - Gets or creates a sandbox instance by ID. Use the same ID to reuse the same sandbox instance across requests.
+- `sandbox.exec()` - Execute shell commands in the sandbox and capture stdout, stderr, and exit codes.
+- `sandbox.writeFile()` / `readFile()` - Write and read files in the sandbox filesystem.
## 3. Test locally
Start the development server:
```sh
-npm run dev
+npm run dev. If you expect to have multiple sandbox instances, you can increase `max_instances`.
```
:::note
@@ -138,6 +127,7 @@ npx wrangler deploy
```
This will:
+
1. Build your container image using Docker
2. Push it to Cloudflare's Container Registry
3. Deploy your Worker globally
@@ -161,7 +151,11 @@ Visit your Worker URL (shown in deploy output):
curl https://my-sandbox.YOUR_SUBDOMAIN.workers.dev/run
```
-Your sandbox is now deployed.
+Your sandbox is now deployed and can execute code in isolated containers.
+
+:::note[Preview URLs require custom domain]
+If you plan to expose ports from sandboxes (using `exposePort()` for preview URLs), you will need to set up a custom domain with wildcard DNS routing. The `.workers.dev` domain does not support the subdomain patterns required for preview URLs. See [Production Deployment](/sandbox/guides/production-deployment/) when you are ready to expose services.
+:::
## Understanding the configuration
@@ -171,34 +165,36 @@ Your `wrangler.jsonc` connects three pieces together:
```jsonc
{
- "containers": [
- {
- "class_name": "Sandbox",
- "image": "./Dockerfile"
- }
- ],
- "durable_objects": {
- "bindings": [
- {
- "class_name": "Sandbox",
- "name": "Sandbox"
- }
- ]
- },
- "migrations": [
- {
- "new_sqlite_classes": ["Sandbox"],
- "tag": "v1"
- }
- ]
+ "containers": [
+ {
+ "class_name": "Sandbox",
+ "image": "./Dockerfile",
+ "instance_type": "lite",
+ "max_instances": 1,
+ },
+ ],
+ "durable_objects": {
+ "bindings": [
+ {
+ "class_name": "Sandbox",
+ "name": "Sandbox",
+ },
+ ],
+ },
+ "migrations": [
+ {
+ "new_sqlite_classes": ["Sandbox"],
+ "tag": "v1",
+ },
+ ],
}
```
-- **containers** - Your Dockerfile defines the execution environment
-- **durable_objects** - Makes the `Sandbox` binding available in your Worker
-- **migrations** - Initializes Durable Object storage (required once)
+- **containers** - Defines the [container image, instance type, and resource limits](/workers/wrangler/configuration/#containers) for your sandbox environment. If you expect to have multiple sandbox instances, you can increase `max_instances`.
+- **durable_objects** - You need not be familiar with [Durable Objects](/durable-objects) to use Sandbox SDK, but if you'd like, you can [learn more about Cloudflare Containers and Durable Objects](/containers/get-started/#each-container-is-backed-by-its-own-durable-object). This configuration creates a [binding](/workers/runtime-apis/bindings#what-is-a-binding) that makes the `Sandbox` Durable Object accessible in your Worker code.
+- **migrations** - Registers the `Sandbox` class, implemented by the Sandbox SDK, with [SQLite storage backend](/durable-objects/best-practices/access-durable-objects-storage) (required once)
For detailed configuration options including environment variables, secrets, and custom images, see the [Wrangler configuration reference](/sandbox/configuration/wrangler/).
@@ -209,4 +205,5 @@ Now that you have a working sandbox, explore more capabilities:
- [Execute commands](/sandbox/guides/execute-commands/) - Run shell commands and stream output
- [Manage files](/sandbox/guides/manage-files/) - Work with files and directories
- [Expose services](/sandbox/guides/expose-services/) - Get public URLs for services running in your sandbox
+- [Production Deployment](/sandbox/guides/production-deployment/) - Set up custom domains for preview URLs
- [API reference](/sandbox/api/) - Complete API documentation
diff --git a/src/content/docs/sandbox/guides/background-processes.mdx b/src/content/docs/sandbox/guides/background-processes.mdx
index eabf973ebbbf997..c9d48b30ef96c2f 100644
--- a/src/content/docs/sandbox/guides/background-processes.mdx
+++ b/src/content/docs/sandbox/guides/background-processes.mdx
@@ -3,6 +3,7 @@ title: Run background processes
pcx_content_type: how-to
sidebar:
order: 3
+description: Start and manage long-running services and applications.
---
import { TypeScriptExample } from "~/components";
@@ -19,10 +20,9 @@ Use `startProcess()` instead of `exec()` when:
- **Continuous monitoring** - Log watchers, health checkers
- **Parallel execution** - Multiple services running simultaneously
-Use `exec()` for:
-
-- **One-time commands** - Installations, builds, data processing
-- **Quick scripts** - Simple operations that complete and exit
+:::note
+For **one-time commands, builds, or scripts that complete and exit**, use `exec()` instead. See the [Execute commands guide](/sandbox/guides/execute-commands/).
+:::
## Start a background process
diff --git a/src/content/docs/sandbox/guides/code-execution.mdx b/src/content/docs/sandbox/guides/code-execution.mdx
index 85945f6c037d6e8..75b304c50bfee72 100644
--- a/src/content/docs/sandbox/guides/code-execution.mdx
+++ b/src/content/docs/sandbox/guides/code-execution.mdx
@@ -3,6 +3,7 @@ title: Use code interpreter
pcx_content_type: how-to
sidebar:
order: 5
+description: Execute Python and JavaScript code with rich outputs.
---
import { TypeScriptExample } from "~/components";
@@ -73,9 +74,9 @@ console.log('Success:', result.success);
```
-### Persistent state
+### State within a context
-Variables and imports persist between executions in the same context:
+Variables and imports remain available between executions in the same context, as long as the container stays active:
```
@@ -102,6 +103,10 @@ console.log(result.output); // "Mean: 3.0"
```
+:::note
+Context state is lost if the container restarts due to inactivity. For critical data, store results outside the sandbox or design your code to reinitialize as needed.
+:::
+
## Handle rich outputs
The code interpreter returns multiple output formats:
diff --git a/src/content/docs/sandbox/guides/execute-commands.mdx b/src/content/docs/sandbox/guides/execute-commands.mdx
index 8564ae320474638..2e465f32d0574d1 100644
--- a/src/content/docs/sandbox/guides/execute-commands.mdx
+++ b/src/content/docs/sandbox/guides/execute-commands.mdx
@@ -3,6 +3,7 @@ title: Execute commands
pcx_content_type: how-to
sidebar:
order: 1
+description: Run commands with streaming output, error handling, and shell access.
---
import { TypeScriptExample } from "~/components";
@@ -11,10 +12,15 @@ This guide shows you how to execute commands in the sandbox, handle output, and
## Choose the right method
-The SDK provides two methods for command execution:
+The SDK provides multiple approaches for running commands:
-- **`exec()`** - Run a command and wait for complete result. Best for most use cases.
+- **`exec()`** - Run a command and wait for complete result. Best for one-time commands like builds, installations, and scripts.
- **`execStream()`** - Stream output in real-time. Best for long-running commands where you need immediate feedback.
+- **`startProcess()`** - Start a background process. Best for web servers, databases, and services that need to keep running.
+
+:::note
+For **web servers, databases, or services that need to keep running**, use `startProcess()` instead. See the [Background processes guide](/sandbox/guides/background-processes/).
+:::
## Execute basic commands
@@ -132,6 +138,7 @@ await sandbox.exec('python /workspace/analyze.py data.csv');
- **Check exit codes** - Always verify `result.success` and `result.exitCode`
- **Validate inputs** - Escape or validate user input to prevent injection
- **Use streaming** - For long operations, use `execStream()` for real-time feedback
+- **Use background processes** - For services that need to keep running (web servers, databases), use the [Background processes guide](/sandbox/guides/background-processes/) instead
- **Handle errors** - Check stderr for error details
## Troubleshooting
diff --git a/src/content/docs/sandbox/guides/expose-services.mdx b/src/content/docs/sandbox/guides/expose-services.mdx
index 713ba3ff6c3d2e6..70152d834ad0271 100644
--- a/src/content/docs/sandbox/guides/expose-services.mdx
+++ b/src/content/docs/sandbox/guides/expose-services.mdx
@@ -3,10 +3,15 @@ title: Expose services
pcx_content_type: how-to
sidebar:
order: 4
+description: Create preview URLs and expose ports for web services.
---
import { TypeScriptExample } from "~/components";
+:::note[Production requires custom domain]
+Preview URLs require a custom domain with wildcard DNS routing in production. See [Production Deployment](/sandbox/guides/production-deployment/) for setup instructions.
+:::
+
This guide shows you how to expose services running in your sandbox to the internet via preview URLs.
## When to expose ports
@@ -40,7 +45,8 @@ const exposed = await sandbox.exposePort(8000);
// 4. Preview URL is now available (public by default)
console.log('Server accessible at:', exposed.exposedAt);
-// Returns: https://abc123-8000.sandbox.workers.dev
+// Production: https://8000-abc123.example.com
+// Local dev: http://localhost:8787/...
// 5. Handle preview URL requests in your Worker
export default {
@@ -255,12 +261,14 @@ if (!ports.some(p => p.port === 8080)) {
```
-## Preview URL format
+## Preview URL Format
+
+**Production**: `https://{port}-{sandbox-id}.yourdomain.com`
-Preview URLs follow the pattern `https://{sandbox-id}-{port}.sandbox.workers.dev`:
+- Port 8080: `https://8080-abc123.example.com`
+- Port 5173: `https://5173-abc123.example.com`
-- Port 8080: `https://abc123-8080.sandbox.workers.dev`
-- Port 5173: `https://abc123-5173.sandbox.workers.dev`
+**Local development**: `http://localhost:8787/...`
**Note**: Port 3000 is reserved for the internal Bun server and cannot be exposed.
diff --git a/src/content/docs/sandbox/guides/git-workflows.mdx b/src/content/docs/sandbox/guides/git-workflows.mdx
index 87656296362a795..20f3a14a7d43c5f 100644
--- a/src/content/docs/sandbox/guides/git-workflows.mdx
+++ b/src/content/docs/sandbox/guides/git-workflows.mdx
@@ -3,6 +3,7 @@ title: Work with Git
pcx_content_type: how-to
sidebar:
order: 6
+description: Clone repositories, manage branches, and automate Git operations.
---
import { TypeScriptExample } from "~/components";
diff --git a/src/content/docs/sandbox/guides/index.mdx b/src/content/docs/sandbox/guides/index.mdx
index 81c700f7ebb6d12..4b236c0e7786e10 100644
--- a/src/content/docs/sandbox/guides/index.mdx
+++ b/src/content/docs/sandbox/guides/index.mdx
@@ -5,17 +5,11 @@ sidebar:
order: 4
---
-These guides show you how to solve specific problems and implement features with the Sandbox SDK. Each guide focuses on a particular task and provides practical, production-ready solutions.
+import { ResourcesBySelector } from "~/components";
-## Available guides
+These guides show you how to solve specific problems and implement features with the Sandbox SDK. Each guide focuses on a particular task and provides practical, production-ready solutions.
-- [Execute commands](/sandbox/guides/execute-commands/) - Run commands with streaming output, error handling, and shell access
-- [Manage files](/sandbox/guides/manage-files/) - Read, write, organize, and synchronize files in the sandbox
-- [Run background processes](/sandbox/guides/background-processes/) - Start and manage long-running services and applications
-- [Expose services](/sandbox/guides/expose-services/) - Create preview URLs and expose ports for web services
-- [Use code interpreter](/sandbox/guides/code-execution/) - Execute Python and JavaScript code with rich outputs
-- [Work with Git](/sandbox/guides/git-workflows/) - Clone repositories, manage branches, and automate Git operations
-- [Stream output](/sandbox/guides/streaming-output/) - Handle real-time output from commands and processes
+
## Related resources
diff --git a/src/content/docs/sandbox/guides/manage-files.mdx b/src/content/docs/sandbox/guides/manage-files.mdx
index 69b1bd40d972860..be3551842bd2d43 100644
--- a/src/content/docs/sandbox/guides/manage-files.mdx
+++ b/src/content/docs/sandbox/guides/manage-files.mdx
@@ -3,6 +3,7 @@ title: Manage files
pcx_content_type: how-to
sidebar:
order: 2
+description: Read, write, organize, and synchronize files in the sandbox.
---
import { TypeScriptExample } from "~/components";
diff --git a/src/content/docs/sandbox/guides/production-deployment.mdx b/src/content/docs/sandbox/guides/production-deployment.mdx
new file mode 100644
index 000000000000000..5286b2aafda038c
--- /dev/null
+++ b/src/content/docs/sandbox/guides/production-deployment.mdx
@@ -0,0 +1,93 @@
+---
+title: Deploy to Production
+pcx_content_type: how-to
+sidebar:
+ order: 10
+description: Set up custom domains for preview URLs in production.
+---
+
+import { WranglerConfig } from "~/components";
+
+:::note[Only required for preview URLs]
+Custom domain setup is ONLY needed if you use `exposePort()` to expose services from sandboxes. If your application does not expose ports, you can deploy to `.workers.dev` without this configuration.
+:::
+
+Deploy your Sandbox SDK application to production with preview URL support. Preview URLs require wildcard DNS routing because they generate unique subdomains for each exposed port: `https://8080-abc123.yourdomain.com`.
+
+The `.workers.dev` domain does not support wildcard subdomains, so production deployments that use preview URLs need a custom domain.
+
+## Prerequisites
+
+- Active Cloudflare zone with a domain
+- Worker that uses `exposePort()`
+- [Wrangler CLI](/workers/wrangler/install-and-update/) installed
+
+## Setup
+
+### Create Wildcard DNS Record
+
+In the Cloudflare dashboard, go to your domain and create an A record:
+
+- **Type**: A
+- **Name**: * (wildcard)
+- **IPv4 address**: 192.0.2.0
+- **Proxy status**: Proxied (orange cloud)
+
+This routes all subdomains through Cloudflare's proxy. The IP address `192.0.2.0` is a documentation address (RFC 5737) that Cloudflare recognizes when proxied.
+
+### Configure Worker Routes
+
+Add a wildcard route to your Wrangler configuration:
+
+
+```toml
+name = "my-sandbox-app"
+main = "src/index.ts"
+compatibility_date = "$today"
+
+[[routes]]
+pattern = "*.yourdomain.com/*"
+zone_name = "yourdomain.com"
+```
+
+
+Replace `yourdomain.com` with your actual domain. This routes all subdomain requests to your Worker and enables Cloudflare to provision SSL certificates automatically.
+
+### Deploy
+
+Deploy your Worker:
+
+```sh
+npx wrangler deploy
+```
+
+## Verify
+
+Test that preview URLs work:
+
+```typescript
+const sandbox = getSandbox(env.Sandbox, 'test-sandbox');
+await sandbox.startProcess('python -m http.server 8080');
+const exposed = await sandbox.exposePort(8080);
+
+console.log(exposed.exposedAt);
+// https://8080-test-sandbox.yourdomain.com
+```
+
+Visit the URL in your browser to confirm your service is accessible.
+
+## Troubleshooting
+
+- **CustomDomainRequiredError**: Verify your Worker is not deployed to `.workers.dev` and that the wildcard DNS record and route are configured correctly.
+- **SSL/TLS errors**: Wait a few minutes for certificate provisioning. Verify the DNS record is proxied and SSL/TLS mode is set to "Full" or "Full (strict)" in your dashboard.
+- **Preview URL not resolving**: Confirm the wildcard DNS record exists and is proxied. Wait 30-60 seconds for DNS propagation.
+- **Port not accessible**: Ensure your service binds to `0.0.0.0` (not `localhost`) and that `proxyToSandbox()` is called first in your Worker's fetch handler.
+
+For detailed troubleshooting, see the [Workers routing documentation](/workers/configuration/routing/).
+
+## Related Resources
+
+- [Preview URLs](/sandbox/concepts/preview-urls/) - How preview URLs work
+- [Expose Services](/sandbox/guides/expose-services/) - Patterns for exposing ports
+- [Workers Routing](/workers/configuration/routing/) - Advanced routing configuration
+- [Cloudflare DNS](/dns/) - DNS management
diff --git a/src/content/docs/sandbox/guides/streaming-output.mdx b/src/content/docs/sandbox/guides/streaming-output.mdx
index d1ac81f09e6b380..182860bc1c4bb22 100644
--- a/src/content/docs/sandbox/guides/streaming-output.mdx
+++ b/src/content/docs/sandbox/guides/streaming-output.mdx
@@ -3,6 +3,7 @@ title: Stream output
pcx_content_type: how-to
sidebar:
order: 7
+description: Handle real-time output from commands and processes.
---
import { TypeScriptExample } from "~/components";
diff --git a/src/content/docs/sandbox/index.mdx b/src/content/docs/sandbox/index.mdx
index 2283be49c474318..d70a31282ec5111 100644
--- a/src/content/docs/sandbox/index.mdx
+++ b/src/content/docs/sandbox/index.mdx
@@ -128,7 +128,7 @@ df['sales'].sum() # Last expression is automatically returned
-Run shell commands, Python scripts, Node.js applications, and more in isolated containers with streaming output support and automatic timeout handling.
+Run shell commands, Python scripts, Node.js applications, and more with streaming output support and automatic timeout handling.
@@ -144,6 +144,12 @@ Expose HTTP services running in your sandbox with automatically generated previe
+
+
+Execute Python and JavaScript code with rich outputs including charts, tables, and images. Maintain persistent state between executions for AI-generated code and interactive workflows.
+
+
+
---
## Use Cases
@@ -156,7 +162,7 @@ Execute code generated by Large Language Models safely and reliably. Perfect for
### Data Analysis & Notebooks
-Create interactive data analysis environments with Python, pandas, and visualization libraries. Build notebook-like experiences at the edge.
+Create interactive data analysis environments with pandas, NumPy, and Matplotlib. Generate charts, tables, and visualizations with automatic rich output formatting.
### Interactive Development Environments
@@ -194,35 +200,34 @@ Stateful coordination layer that enables Sandbox to maintain persistent environm
-
+
Explore complete examples including AI code execution, data analysis, and
interactive environments.
- Learn about the Sandbox Beta, current status, and upcoming features.
+ Learn how to solve specific problems and implement features with the Sandbox
+ SDK.
-
- Understand Sandbox pricing based on the underlying Containers platform.
+
+ Explore the complete API documentation for the Sandbox SDK.
-
- Learn about resource limits, quotas, and best practices for working within
- them.
+
+ Learn about the key concepts and architecture of the Sandbox SDK.
-
- Learn how to solve specific problems and implement features with the Sandbox
- SDK.
+
+ Learn about the configuration options for the Sandbox SDK.
+ Learn about the Sandbox Beta, current status, and upcoming features.
+
+
+
+ Understand Sandbox pricing based on the underlying Containers platform.
+
+
+
+ Learn about resource limits, quotas, and best practices for working within
+ them.
+
+
+
- Connect with the Workers community on Discord. Ask questions, share what
- you're building, and get help from other developers.
+ Connect with the community on Discord. Ask questions, share what you're
+ building, and get help from other developers.
diff --git a/src/content/docs/sandbox/tutorials/ai-code-executor.mdx b/src/content/docs/sandbox/tutorials/ai-code-executor.mdx
index 33351b1d8db3531..ba25dea14d1b540 100644
--- a/src/content/docs/sandbox/tutorials/ai-code-executor.mdx
+++ b/src/content/docs/sandbox/tutorials/ai-code-executor.mdx
@@ -3,6 +3,7 @@ title: Build an AI code executor
pcx_content_type: tutorial
sidebar:
order: 1
+description: Use Claude to generate Python code from natural language and execute it securely in sandboxes.
---
import { Render, PackageManagers } from "~/components";
@@ -97,9 +98,15 @@ Return ONLY the code, no explanations.`
return Response.json({ error: 'Failed to generate code' }, { status: 500 });
}
+ // Strip markdown code fences if present
+ const cleanCode = generatedCode
+ .replace(/^```python?\n?/, '')
+ .replace(/\n?```\s*$/, '')
+ .trim();
+
// Execute the code in a sandbox
const sandbox = getSandbox(env.Sandbox, 'demo-user');
- await sandbox.writeFile('/tmp/code.py', generatedCode);
+ await sandbox.writeFile('/tmp/code.py', cleanCode);
const result = await sandbox.exec('python /tmp/code.py');
return Response.json({
@@ -127,15 +134,19 @@ Return ONLY the code, no explanations.`
4. Executes with `sandbox.exec('python /tmp/code.py')`
5. Returns both the code and execution results
-## 4. Set your Anthropic API key
+## 4. Set up local environment variables
-Store your Anthropic API key as a secret:
+Create a `.dev.vars` file in your project root for local development:
```sh
-npx wrangler secret put ANTHROPIC_API_KEY
+echo "ANTHROPIC_API_KEY=your_api_key_here" > .dev.vars
```
-Paste your API key from the [Anthropic Console](https://console.anthropic.com/) when prompted.
+Replace `your_api_key_here` with your actual API key from the [Anthropic Console](https://console.anthropic.com/).
+
+:::note
+The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.
+:::
## 5. Test locally
@@ -177,6 +188,14 @@ Deploy your Worker:
npx wrangler deploy
```
+Then set your Anthropic API key as a production secret:
+
+```sh
+npx wrangler secret put ANTHROPIC_API_KEY
+```
+
+Paste your API key from the [Anthropic Console](https://console.anthropic.com/) when prompted.
+
:::caution
After first deployment, wait 2-3 minutes for container provisioning. Check status with `npx wrangler containers list`.
:::
diff --git a/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx b/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx
index 227a4b45faa3de7..0c9e450b41b89f5 100644
--- a/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx
+++ b/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx
@@ -3,6 +3,7 @@ title: Analyze data with AI
pcx_content_type: tutorial
sidebar:
order: 2
+description: Upload CSV files, generate analysis code with Claude, and return visualizations.
---
import { Render, PackageManagers } from "~/components";
@@ -16,6 +17,7 @@ Build an AI-powered data analysis system that accepts CSV uploads, uses Claude t
You'll also need:
+
- An [Anthropic API key](https://console.anthropic.com/) for Claude
- [Docker](https://www.docker.com/) running locally
@@ -42,10 +44,10 @@ cd analyze-data
Replace `src/index.ts`:
```typescript
-import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox';
-import Anthropic from '@anthropic-ai/sdk';
+import { getSandbox, proxyToSandbox, type Sandbox } from "@cloudflare/sandbox";
+import Anthropic from "@anthropic-ai/sdk";
-export { Sandbox } from '@cloudflare/sandbox';
+export { Sandbox } from "@cloudflare/sandbox";
interface Env {
Sandbox: DurableObjectNamespace;
@@ -57,48 +59,65 @@ export default {
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) return proxyResponse;
- if (request.method !== 'POST') {
- return Response.json({ error: 'POST CSV file and question' }, { status: 405 });
+ if (request.method !== "POST") {
+ return Response.json(
+ { error: "POST CSV file and question" },
+ { status: 405 },
+ );
}
try {
const formData = await request.formData();
- const csvFile = formData.get('file') as File;
- const question = formData.get('question') as string;
+ const csvFile = formData.get("file") as File;
+ const question = formData.get("question") as string;
if (!csvFile || !question) {
- return Response.json({ error: 'Missing file or question' }, { status: 400 });
+ return Response.json(
+ { error: "Missing file or question" },
+ { status: 400 },
+ );
}
// Upload CSV to sandbox
const sandbox = getSandbox(env.Sandbox, `analysis-${Date.now()}`);
- const csvPath = '/workspace/data.csv';
- await sandbox.writeFile(csvPath, new Uint8Array(await csvFile.arrayBuffer()));
+ const csvPath = "/workspace/data.csv";
+ await sandbox.writeFile(csvPath, await csvFile.text());
// Analyze CSV structure
const structure = await sandbox.exec(
- `python -c "import pandas as pd; df = pd.read_csv('${csvPath}'); print(f'Rows: {len(df)}'); print(f'Columns: {list(df.columns)[:5]}')"`
+ `python3 -c "import pandas as pd; df = pd.read_csv('${csvPath}'); print(f'Rows: {len(df)}'); print(f'Columns: {list(df.columns)[:5]}')"`,
);
if (!structure.success) {
- return Response.json({ error: 'Failed to read CSV', details: structure.stderr }, { status: 400 });
+ return Response.json(
+ { error: "Failed to read CSV", details: structure.stderr },
+ { status: 400 },
+ );
}
// Generate analysis code with Claude
- const code = await generateAnalysisCode(env.ANTHROPIC_API_KEY, csvPath, question, structure.stdout);
+ const code = await generateAnalysisCode(
+ env.ANTHROPIC_API_KEY,
+ csvPath,
+ question,
+ structure.stdout,
+ );
// Write and execute the analysis code
- await sandbox.writeFile('/workspace/analyze.py', code);
- const result = await sandbox.exec('python /workspace/analyze.py');
+ await sandbox.writeFile("/workspace/analyze.py", code);
+ const result = await sandbox.exec("python /workspace/analyze.py");
if (!result.success) {
- return Response.json({ error: 'Analysis failed', details: result.stderr }, { status: 500 });
+ return Response.json(
+ { error: "Analysis failed", details: result.stderr },
+ { status: 500 },
+ );
}
// Check for generated chart
let chart = null;
try {
- const chartFile = await sandbox.readFile('/workspace/chart.png');
+ const chartFile = await sandbox.readFile("/workspace/chart.png");
const buffer = new Uint8Array(chartFile.content);
chart = `data:image/png;base64,${btoa(String.fromCharCode(...buffer))}`;
} catch {
@@ -111,9 +130,8 @@ export default {
success: true,
output: result.stdout,
chart,
- code
+ code,
});
-
} catch (error: any) {
return Response.json({ error: error.message }, { status: 500 });
}
@@ -124,16 +142,17 @@ async function generateAnalysisCode(
apiKey: string,
csvPath: string,
question: string,
- csvStructure: string
+ csvStructure: string,
): Promise {
const anthropic = new Anthropic({ apiKey });
const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-5',
+ model: "claude-sonnet-4-5",
max_tokens: 2048,
- messages: [{
- role: 'user',
- content: `CSV at ${csvPath}:
+ messages: [
+ {
+ role: "user",
+ content: `CSV at ${csvPath}:
${csvStructure}
Question: "${question}"
@@ -144,37 +163,48 @@ Generate Python code that:
- Saves charts to /workspace/chart.png if helpful
- Prints findings to stdout
-Use pandas, numpy, matplotlib.`
- }],
- tools: [{
- name: 'generate_python_code',
- description: 'Generate Python code for data analysis',
- input_schema: {
- type: 'object',
- properties: {
- code: { type: 'string', description: 'Complete Python code' }
+Use pandas, numpy, matplotlib.`,
+ },
+ ],
+ tools: [
+ {
+ name: "generate_python_code",
+ description: "Generate Python code for data analysis",
+ input_schema: {
+ type: "object",
+ properties: {
+ code: { type: "string", description: "Complete Python code" },
+ },
+ required: ["code"],
},
- required: ['code']
- }
- }]
+ },
+ ],
});
for (const block of response.content) {
- if (block.type === 'tool_use' && block.name === 'generate_python_code') {
+ if (block.type === "tool_use" && block.name === "generate_python_code") {
return (block.input as { code: string }).code;
}
}
- throw new Error('Failed to generate code');
+ throw new Error("Failed to generate code");
}
```
-## 4. Set your API key
+## 4. Set up local environment variables
+
+Create a `.dev.vars` file in your project root for local development:
```sh
-npx wrangler secret put ANTHROPIC_API_KEY
+echo "ANTHROPIC_API_KEY=your_api_key_here" > .dev.vars
```
+Replace `your_api_key_here` with your actual API key from the [Anthropic Console](https://console.anthropic.com/).
+
+:::note
+The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.
+:::
+
## 5. Test locally
Download a sample CSV:
@@ -205,19 +235,29 @@ Response:
```json
{
- "success": true,
- "output": "Average ratings by year:\n2020: 8.5\n2021: 7.2\n2022: 9.1",
- "chart": "data:image/png;base64,...",
- "code": "import pandas as pd\nimport matplotlib.pyplot as plt\n..."
+ "success": true,
+ "output": "Average ratings by year:\n2020: 8.5\n2021: 7.2\n2022: 9.1",
+ "chart": "data:image/png;base64,...",
+ "code": "import pandas as pd\nimport matplotlib.pyplot as plt\n..."
}
```
## 6. Deploy
+Deploy your Worker:
+
```sh
npx wrangler deploy
```
+Then set your Anthropic API key as a production secret:
+
+```sh
+npx wrangler secret put ANTHROPIC_API_KEY
+```
+
+Paste your API key from the [Anthropic Console](https://console.anthropic.com/) when prompted.
+
:::caution
Wait 2-3 minutes after first deployment for container provisioning.
:::
@@ -225,6 +265,7 @@ Wait 2-3 minutes after first deployment for container provisioning.
## What you built
An AI data analysis system that:
+
- Uploads CSV files to sandboxes
- Uses Claude's tool calling to generate analysis code
- Executes Python with pandas and matplotlib
diff --git a/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx b/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx
index 7324de9daea228a..093ff871385329c 100644
--- a/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx
+++ b/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx
@@ -3,6 +3,7 @@ title: Automated testing pipeline
pcx_content_type: tutorial
sidebar:
order: 4
+description: Build a testing pipeline that clones Git repositories, installs dependencies, runs tests, and reports results.
---
import { Render, PackageManagers } from "~/components";
@@ -34,7 +35,7 @@ cd test-pipeline
Replace `src/index.ts`:
```typescript
-import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox';
+import { getSandbox, proxyToSandbox, parseSSEStream, type Sandbox, type ExecEvent } from '@cloudflare/sandbox';
export { Sandbox } from '@cloudflare/sandbox';
@@ -53,7 +54,7 @@ export default {
}
try {
- const { repoUrl, branch = 'main' } = await request.json();
+ const { repoUrl, branch } = await request.json();
if (!repoUrl) {
return Response.json({ error: 'repoUrl required' }, { status: 400 });
@@ -63,39 +64,69 @@ export default {
try {
// Clone repository
+ console.log('Cloning repository...');
let cloneUrl = repoUrl;
- if (env.GITHUB_TOKEN && repoUrl.includes('github.com')) {
- cloneUrl = repoUrl.replace('https://', `https://${env.GITHUB_TOKEN}@`);
+
+ if (env.GITHUB_TOKEN && cloneUrl.includes('github.com')) {
+ cloneUrl = cloneUrl.replace('https://', `https://${env.GITHUB_TOKEN}@`);
}
- await sandbox.exec(`git clone --depth=1 --branch=${branch} ${cloneUrl} /workspace/repo`);
+ await sandbox.gitCheckout(cloneUrl, {
+ ...(branch && { branch }),
+ depth: 1,
+ targetDir: 'repo'
+ });
+ console.log('Repository cloned');
// Detect project type
const projectType = await detectProjectType(sandbox);
+ console.log(`Detected ${projectType} project`);
// Install dependencies
const installCmd = getInstallCommand(projectType);
if (installCmd) {
- const installResult = await sandbox.exec(`cd /workspace/repo && ${installCmd}`);
- if (!installResult.success) {
+ console.log('Installing dependencies...');
+ const installStream = await sandbox.execStream(`cd /workspace/repo && ${installCmd}`);
+
+ let installExitCode = 0;
+ for await (const event of parseSSEStream(installStream)) {
+ if (event.type === 'stdout' || event.type === 'stderr') {
+ console.log(event.data);
+ } else if (event.type === 'complete') {
+ installExitCode = event.exitCode;
+ }
+ }
+
+ if (installExitCode !== 0) {
return Response.json({
success: false,
error: 'Install failed',
- output: installResult.stderr
+ exitCode: installExitCode
});
}
+ console.log('Dependencies installed');
}
// Run tests
+ console.log('Running tests...');
const testCmd = getTestCommand(projectType);
- const testResult = await sandbox.exec(`cd /workspace/repo && ${testCmd}`);
+ const testStream = await sandbox.execStream(`cd /workspace/repo && ${testCmd}`);
+
+ let testExitCode = 0;
+ for await (const event of parseSSEStream(testStream)) {
+ if (event.type === 'stdout' || event.type === 'stderr') {
+ console.log(event.data);
+ } else if (event.type === 'complete') {
+ testExitCode = event.exitCode;
+ }
+ }
+ console.log(`Tests completed with exit code ${testExitCode}`);
return Response.json({
- success: testResult.exitCode === 0,
- exitCode: testResult.exitCode,
- output: testResult.stdout,
- errors: testResult.stderr,
- projectType
+ success: testExitCode === 0,
+ exitCode: testExitCode,
+ projectType,
+ message: testExitCode === 0 ? 'All tests passed' : 'Tests failed'
});
} finally {
@@ -160,19 +191,18 @@ Test with a repository:
curl -X POST http://localhost:8787 \
-H "Content-Type: application/json" \
-d '{
- "repoUrl": "https://github.com/sindresorhus/is-promise",
- "branch": "main"
+ "repoUrl": "https://github.com/cloudflare/sandbox-sdk"
}'
```
-Response:
+You will see progress logs in the wrangler console, and receive a JSON response:
```json
{
"success": true,
"exitCode": 0,
- "output": "...test output...",
- "projectType": "nodejs"
+ "projectType": "nodejs",
+ "message": "All tests passed"
}
```
diff --git a/src/content/docs/sandbox/tutorials/code-review-bot.mdx b/src/content/docs/sandbox/tutorials/code-review-bot.mdx
index 87df1c451defdc4..4087a8b84887e8d 100644
--- a/src/content/docs/sandbox/tutorials/code-review-bot.mdx
+++ b/src/content/docs/sandbox/tutorials/code-review-bot.mdx
@@ -3,6 +3,7 @@ title: Build a code review bot
pcx_content_type: tutorial
sidebar:
order: 3
+description: Clone repositories, analyze code with Claude, and post review comments to GitHub PRs.
---
import { Render, PackageManagers } from "~/components";
@@ -16,7 +17,13 @@ Build a GitHub bot that responds to pull requests, clones the repository in a sa
You'll also need:
-- A [GitHub account](https://github.com/) and personal access token with repo permissions
+
+- A [GitHub account](https://github.com/) and [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) with the following permissions:
+ - **Repository access**: Select the specific repository you want to test with
+ - **Permissions** > **Repository permissions**:
+ - **Metadata**: Read-only (required)
+ - **Contents**: Read-only (required to clone the repository)
+ - **Pull requests**: Read and write (required to post review comments)
- An [Anthropic API key](https://console.anthropic.com/) for Claude
- A GitHub repository for testing
@@ -41,11 +48,11 @@ cd code-review-bot
Replace `src/index.ts`:
```typescript
-import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox';
-import { Octokit } from '@octokit/rest';
-import Anthropic from '@anthropic-ai/sdk';
+import { getSandbox, proxyToSandbox, type Sandbox } from "@cloudflare/sandbox";
+import { Octokit } from "@octokit/rest";
+import Anthropic from "@anthropic-ai/sdk";
-export { Sandbox } from '@cloudflare/sandbox';
+export { Sandbox } from "@cloudflare/sandbox";
interface Env {
Sandbox: DurableObjectNamespace;
@@ -55,51 +62,87 @@ interface Env {
}
export default {
- async fetch(request: Request, env: Env): Promise {
+ async fetch(
+ request: Request,
+ env: Env,
+ ctx: ExecutionContext,
+ ): Promise {
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) return proxyResponse;
const url = new URL(request.url);
- if (url.pathname === '/webhook' && request.method === 'POST') {
- const signature = request.headers.get('x-hub-signature-256');
+ if (url.pathname === "/webhook" && request.method === "POST") {
+ const signature = request.headers.get("x-hub-signature-256");
+ const contentType = request.headers.get("content-type") || "";
const body = await request.text();
// Verify webhook signature
- if (!signature || !(await verifySignature(body, signature, env.WEBHOOK_SECRET))) {
- return Response.json({ error: 'Invalid signature' }, { status: 401 });
+ if (
+ !signature ||
+ !(await verifySignature(body, signature, env.WEBHOOK_SECRET))
+ ) {
+ return Response.json({ error: "Invalid signature" }, { status: 401 });
}
- const event = request.headers.get('x-github-event');
- const payload = JSON.parse(body);
+ const event = request.headers.get("x-github-event");
+
+ // Parse payload (GitHub can send as JSON or form-encoded)
+ let payload;
+ if (contentType.includes("application/json")) {
+ payload = JSON.parse(body);
+ } else {
+ // Handle form-encoded payload
+ const params = new URLSearchParams(body);
+ payload = JSON.parse(params.get("payload") || "{}");
+ }
- // Only handle opened PRs
- if (event === 'pull_request' && payload.action === 'opened') {
- reviewPullRequest(payload, env).catch(console.error);
- return Response.json({ message: 'Review started' });
+ // Handle opened and reopened PRs
+ if (
+ event === "pull_request" &&
+ (payload.action === "opened" || payload.action === "reopened")
+ ) {
+ console.log(`Starting review for PR #${payload.pull_request.number}`);
+ // Use waitUntil to ensure the review completes even after response is sent
+ ctx.waitUntil(
+ reviewPullRequest(payload, env).catch(console.error),
+ );
+ return Response.json({ message: "Review started" });
}
- return Response.json({ message: 'Event ignored' });
+ return Response.json({ message: "Event ignored" });
}
- return new Response('Code Review Bot\n\nConfigure GitHub webhook to POST /webhook');
+ return new Response(
+ "Code Review Bot\n\nConfigure GitHub webhook to POST /webhook",
+ );
},
};
-async function verifySignature(payload: string, signature: string, secret: string): Promise {
+async function verifySignature(
+ payload: string,
+ signature: string,
+ secret: string,
+): Promise {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
- 'raw',
+ "raw",
encoder.encode(secret),
- { name: 'HMAC', hash: 'SHA-256' },
+ { name: "HMAC", hash: "SHA-256" },
false,
- ['sign']
+ ["sign"],
);
- const signatureBytes = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
- const expected = 'sha256=' + Array.from(new Uint8Array(signatureBytes))
- .map(b => b.toString(16).padStart(2, '0'))
- .join('');
+ const signatureBytes = await crypto.subtle.sign(
+ "HMAC",
+ key,
+ encoder.encode(payload),
+ );
+ const expected =
+ "sha256=" +
+ Array.from(new Uint8Array(signatureBytes))
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
return signature === expected;
}
@@ -108,76 +151,89 @@ async function reviewPullRequest(payload: any, env: Env): Promise {
const pr = payload.pull_request;
const repo = payload.repository;
const octokit = new Octokit({ auth: env.GITHUB_TOKEN });
-
- // Post initial comment
- await octokit.issues.createComment({
- owner: repo.owner.login,
- repo: repo.name,
- issue_number: pr.number,
- body: 'Code review in progress...'
- });
-
const sandbox = getSandbox(env.Sandbox, `review-${pr.number}`);
try {
+ // Post initial comment
+ console.log("Posting initial comment...");
+ await octokit.issues.createComment({
+ owner: repo.owner.login,
+ repo: repo.name,
+ issue_number: pr.number,
+ body: "Code review in progress...",
+ });
// Clone repository
+ console.log("Cloning repository...");
const cloneUrl = `https://${env.GITHUB_TOKEN}@github.com/${repo.owner.login}/${repo.name}.git`;
- await sandbox.exec(`git clone --depth=1 --branch=${pr.head.ref} ${cloneUrl} /workspace/repo`);
+ await sandbox.exec(
+ `git clone --depth=1 --branch=${pr.head.ref} ${cloneUrl} /workspace/repo`,
+ );
// Get changed files
+ console.log("Fetching changed files...");
const comparison = await octokit.repos.compareCommits({
owner: repo.owner.login,
repo: repo.name,
base: pr.base.sha,
- head: pr.head.sha
+ head: pr.head.sha,
});
const files = [];
for (const file of (comparison.data.files || []).slice(0, 5)) {
- if (file.status !== 'removed') {
- const content = await sandbox.readFile(`/workspace/repo/${file.filename}`);
+ if (file.status !== "removed") {
+ const content = await sandbox.readFile(
+ `/workspace/repo/${file.filename}`,
+ );
files.push({
path: file.filename,
- patch: file.patch || '',
- content: content.content
+ patch: file.patch || "",
+ content: content.content,
});
}
}
// Generate review with Claude
+ console.log(`Analyzing ${files.length} files with Claude...`);
const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-5',
+ model: "claude-sonnet-4-5",
max_tokens: 2048,
- messages: [{
- role: 'user',
- content: `Review this PR:
+ messages: [
+ {
+ role: "user",
+ content: `Review this PR:
Title: ${pr.title}
Changed files:
-${files.map(f => `File: ${f.path}\nDiff:\n${f.patch}\n\nContent:\n${f.content.substring(0, 1000)}`).join('\n\n')}
+${files.map((f) => `File: ${f.path}\nDiff:\n${f.patch}\n\nContent:\n${f.content.substring(0, 1000)}`).join("\n\n")}
-Provide a brief code review focusing on bugs, security, and best practices.`
- }]
+Provide a brief code review focusing on bugs, security, and best practices.`,
+ },
+ ],
});
- const review = response.content[0]?.type === 'text' ? response.content[0].text : 'No review generated';
+ const review =
+ response.content[0]?.type === "text"
+ ? response.content[0].text
+ : "No review generated";
// Post review comment
+ console.log("Posting review...");
await octokit.issues.createComment({
owner: repo.owner.login,
repo: repo.name,
issue_number: pr.number,
- body: `## Code Review\n\n${review}\n\n---\n*Generated by Claude*`
+ body: `## Code Review\n\n${review}\n\n---\n*Generated by Claude*`,
});
-
+ console.log("Review complete!");
} catch (error: any) {
+ console.error("Review failed:", error);
await octokit.issues.createComment({
owner: repo.owner.login,
repo: repo.name,
issue_number: pr.number,
- body: `Review failed: ${error.message}`
+ body: `Review failed: ${error.message}`,
});
} finally {
await sandbox.destroy();
@@ -185,35 +241,65 @@ Provide a brief code review focusing on bugs, security, and best practices.`
}
```
-## 4. Set your secrets
+## 4. Set up local environment variables
+
+Create a `.dev.vars` file in your project root for local development:
```sh
-# GitHub token (needs repo permissions)
-npx wrangler secret put GITHUB_TOKEN
+cat > .dev.vars << EOF
+GITHUB_TOKEN=your_github_token_here
+ANTHROPIC_API_KEY=your_anthropic_key_here
+WEBHOOK_SECRET=your_webhook_secret_here
+EOF
+```
-# Anthropic API key
-npx wrangler secret put ANTHROPIC_API_KEY
+Replace the placeholder values with:
-# Webhook secret (generate a random string)
-npx wrangler secret put WEBHOOK_SECRET
+- `GITHUB_TOKEN`: Your GitHub personal access token with repo permissions
+- `ANTHROPIC_API_KEY`: Your API key from the [Anthropic Console](https://console.anthropic.com/)
+- `WEBHOOK_SECRET`: A random string (for example: `openssl rand -hex 32`)
+
+:::note
+The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.
+:::
+
+## 5. Expose local server with Cloudflare Tunnel
+
+To test with real GitHub webhooks locally, use [Cloudflare Tunnel](/cloudflare-one/connections/connect-networks/) to expose your local development server.
+
+Start the development server:
+
+```sh
+npm run dev
```
-## 5. Deploy
+In a separate terminal, create a tunnel to your local server:
```sh
-npx wrangler deploy
+cloudflared tunnel --url http://localhost:8787
```
-## 6. Configure GitHub webhook
+This will output a public URL (for example, `https://example.trycloudflare.com`). Copy this URL for the next step.
+
+:::note
+If you do not have `cloudflared` installed, refer to [Install cloudflared](/cloudflare-one/connections/connect-networks/downloads/).
+:::
+
+## 6. Configure GitHub webhook for local testing
+
+:::note[Important]
+Configure this webhook on a **specific GitHub repository** where you will create test pull requests. The bot will only review PRs in repositories where the webhook is configured.
+:::
-1. Go to your repository **Settings** > **Webhooks** > **Add webhook**
-2. Set **Payload URL**: `https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook`
-3. Set **Content type**: `application/json`
-4. Set **Secret**: Same value you used for `WEBHOOK_SECRET`
-5. Select **Let me select individual events** → Check **Pull requests**
-6. Click **Add webhook**
+1. Navigate to your test repository on GitHub
+2. Go to **Settings** > **Webhooks** > **Add webhook**
+3. Set **Payload URL**: Your Cloudflare Tunnel URL from Step 5 with `/webhook` appended (for example, `https://example.trycloudflare.com/webhook`)
+4. Set **Content type**: `application/json`
+5. Set **Secret**: Same value you used for `WEBHOOK_SECRET` in your `.dev.vars` file
+6. Select **Let me select individual events** → Check **Pull requests**
+7. Click **Add webhook**
-## 7. Test with a pull request
+## 7. Test locally with a pull request
Create a test PR:
@@ -225,11 +311,42 @@ git commit -m "Add test file"
git push origin test-review
```
-Open the PR on GitHub and watch for the bot's review comment!
+Open the PR on GitHub. The bot should post a review comment within a few seconds.
+
+## 8. Deploy to production
+
+Deploy your Worker:
+
+```sh
+npx wrangler deploy
+```
+
+Then set your production secrets:
+
+```sh
+# GitHub token (needs repo permissions)
+npx wrangler secret put GITHUB_TOKEN
+
+# Anthropic API key
+npx wrangler secret put ANTHROPIC_API_KEY
+
+# Webhook secret (use the same value from .dev.vars)
+npx wrangler secret put WEBHOOK_SECRET
+```
+
+## 9. Update webhook for production
+
+1. Go to your repository **Settings** > **Webhooks**
+2. Click on your existing webhook
+3. Update **Payload URL** to your deployed Worker URL: `https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook`
+4. Click **Update webhook**
+
+Your bot is now running in production and will review all new pull requests automatically.
## What you built
A GitHub code review bot that:
+
- Receives webhook events from GitHub
- Clones repositories in isolated sandboxes
- Uses Claude to analyze code changes
diff --git a/src/content/docs/sandbox/tutorials/index.mdx b/src/content/docs/sandbox/tutorials/index.mdx
index 7975ec6472c76c3..a127de63a14fdd7 100644
--- a/src/content/docs/sandbox/tutorials/index.mdx
+++ b/src/content/docs/sandbox/tutorials/index.mdx
@@ -5,57 +5,11 @@ sidebar:
order: 3
---
-import { LinkTitleCard, CardGrid } from "~/components";
+import { ResourcesBySelector } from "~/components";
Learn how to build applications with Sandbox SDK through step-by-step tutorials. Each tutorial takes 20-30 minutes.
-
-
-
- Use Claude to generate Python code from natural language and execute it securely in sandboxes.
-
-
-
- Upload CSV files, generate analysis code with Claude, and return visualizations.
-
-
-
- Clone repositories, analyze code with Claude, and post review comments to GitHub PRs.
-
-
-
- Clone repositories, install dependencies, run tests, and report results.
-
-
-
-
-## What you'll learn
-
-These tutorials cover real-world applications:
-
-- **AI Code Execution** - Integrate Claude with secure code execution
-- **Data Analysis** - Generate and run analysis code on uploaded datasets
-- **Code Review Automation** - Clone repositories and analyze code changes
-- **CI/CD Pipelines** - Automated testing workflows
-- **File Operations** - Work with files and directories
-- **Error Handling** - Validation and error management
-- **Deployment** - Deploy Workers with containers
+
## Before you start
diff --git a/src/content/partials/containers/docker-setup.mdx b/src/content/partials/containers/docker-setup.mdx
new file mode 100644
index 000000000000000..f6b94389ea8eb89
--- /dev/null
+++ b/src/content/partials/containers/docker-setup.mdx
@@ -0,0 +1,4 @@
+You must have Docker running locally when you run `wrangler deploy`. For most people, the best way to install Docker is to follow the [docs for installing Docker Desktop](https://docs.docker.com/desktop/). Other tools like [Colima](https://github.com/abiosoft/colima) may also work.
+
+You can check that Docker is running properly by running the `docker info` command in your terminal. If Docker is running, the command will succeed. If Docker is not running,
+the `docker info` command will hang or return an error including the message "Cannot connect to the Docker daemon".