feat(cloudflare): add React frontend and improve deployment docs

- Add React + Vite frontend for Cloudflare example with sandbox-agent SDK
- Update ensureRunning to poll health endpoint instead of fixed wait
- Fix SDK fetch binding issue (globalThis.fetch.bind)
- Update docs with .dev.vars format warning and container caching tip
- Use containerFetch proxy pattern for reliable local dev
This commit is contained in:
Nathan Flurry 2026-02-03 02:10:45 -08:00
parent 44382d2c12
commit 3576b7fcca
12 changed files with 619 additions and 425 deletions

View file

@ -22,10 +22,69 @@ npm create cloudflare@latest -- my-sandbox --template=cloudflare/sandbox-sdk/exa
cd my-sandbox
```
## Dockerfile
Create a `Dockerfile` with sandbox-agent and agents pre-installed:
```dockerfile
FROM cloudflare/sandbox:0.7.0
# Install sandbox-agent
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
# Pre-install agents
RUN sandbox-agent install-agent claude && \
sandbox-agent install-agent codex
# Required for local development with wrangler dev
EXPOSE 8000
```
<Note>
The `EXPOSE 8000` directive is required for `wrangler dev` to proxy requests to the container. Port 3000 is reserved for the Cloudflare control plane.
</Note>
## Wrangler Configuration
Update `wrangler.jsonc` to use your Dockerfile:
```jsonc
{
"name": "my-sandbox-agent",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"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"
}
]
}
```
## TypeScript Example
This example proxies requests to sandbox-agent via `containerFetch`, which works reliably in both local development and production:
```typescript
import { getSandbox, proxyToSandbox, type Sandbox } from "@cloudflare/sandbox";
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
type Env = {
@ -34,62 +93,60 @@ type Env = {
OPENAI_API_KEY?: string;
};
/** Check if sandbox-agent is already running by probing its health endpoint */
const PORT = 8000;
/** Check if sandbox-agent is already running */
async function isServerRunning(sandbox: Sandbox): Promise<boolean> {
try {
const result = await sandbox.exec("curl -sf http://localhost:8000/v1/health");
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v1/health`);
return result.success;
} catch {
return false;
}
}
/** Ensure sandbox-agent is running in the container */
async function ensureRunning(sandbox: Sandbox, env: Env): Promise<void> {
if (await isServerRunning(sandbox)) return;
// Set environment variables for agents
const envVars: Record<string, string> = {};
if (env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
if (env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = env.OPENAI_API_KEY;
await sandbox.setEnvVars(envVars);
// Start sandbox-agent server
await sandbox.startProcess(
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`
);
// Poll health endpoint until server is ready
for (let i = 0; i < 30; i++) {
if (await isServerRunning(sandbox)) return;
await new Promise((r) => setTimeout(r, 200));
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Proxy requests to exposed ports first
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) return proxyResponse;
const url = new URL(request.url);
const { hostname } = new URL(request.url);
const sandbox = getSandbox(env.Sandbox, "sandbox-agent");
// Proxy requests: /sandbox/:name/v1/...
const match = url.pathname.match(/^\/sandbox\/([^/]+)(\/.*)?$/);
if (match) {
const [, name, path = "/"] = match;
const sandbox = getSandbox(env.Sandbox, name);
// Check if server is already running to avoid port conflicts
const alreadyRunning = await isServerRunning(sandbox);
await ensureRunning(sandbox, env);
if (!alreadyRunning) {
// Install sandbox-agent
await sandbox.exec(
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"
// Proxy request to container
return sandbox.containerFetch(
new Request(`http://localhost${path}${url.search}`, request),
PORT
);
// Install agents
await sandbox.exec("sandbox-agent install-agent claude");
await sandbox.exec("sandbox-agent install-agent codex");
// Set environment variables for agents
const envVars: Record<string, string> = {};
if (env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
if (env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = env.OPENAI_API_KEY;
await sandbox.setEnvVars(envVars);
// Start sandbox-agent server as background process
await sandbox.startProcess(
"sandbox-agent server --no-token --host 0.0.0.0 --port 8000"
);
// Wait for server to start
await new Promise((r) => setTimeout(r, 2000));
}
// Expose the port with a preview URL
const exposed = await sandbox.exposePort(8000, { hostname });
return Response.json({
endpoint: exposed.url,
message: alreadyRunning
? "sandbox-agent server was already running"
: "sandbox-agent server started",
});
return new Response("Not found", { status: 404 });
},
};
```
@ -99,11 +156,10 @@ export default {
```typescript
import { SandboxAgent } from "sandbox-agent";
// Get the endpoint from the Worker
const { endpoint } = await fetch("http://localhost:8787").then((r) => r.json());
// Connect to sandbox-agent
const client = await SandboxAgent.connect({ baseUrl: endpoint });
// Connect via the proxy endpoint
const client = await SandboxAgent.connect({
baseUrl: "http://localhost:8787/sandbox/my-sandbox",
});
// Wait for server to be ready
for (let i = 0; i < 30; i++) {
@ -116,42 +172,49 @@ for (let i = 0; i < 30; i++) {
}
// Create a session and start coding
await client.createSession("my-session", {
agent: "claude",
permissionMode: "default",
});
await client.createSession("my-session", { agent: "claude" });
await client.postMessage("my-session", {
message: "Summarize this repository",
});
for await (const event of client.streamEvents("my-session")) {
console.log(event.type, event.data);
}
```
// Auto-approve permissions
if (event.type === "permission.requested") {
await client.replyPermission("my-session", event.data.permission_id, {
reply: "once",
});
}
## Configuration
Update `wrangler.jsonc` to add environment variables:
```jsonc
{
"vars": {
"ANTHROPIC_API_KEY": "your-api-key"
// Handle text output
if (event.type === "item.delta" && event.data?.delta) {
process.stdout.write(event.data.delta);
}
}
```
Or use `.dev.vars` for local development:
## Environment Variables
Use `.dev.vars` for local development:
```bash
echo "ANTHROPIC_API_KEY=your-api-key" > .dev.vars
```
<Warning>
Use plain `KEY=value` format in `.dev.vars`. Do not use `export KEY=value` - wrangler won't parse the bash syntax.
</Warning>
<Note>
The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.
</Note>
For production, set secrets via wrangler:
```bash
wrangler secret put ANTHROPIC_API_KEY
```
## Local Development
Start the development server:
@ -167,40 +230,22 @@ First run builds the Docker container (2-3 minutes). Subsequent runs are much fa
Test with curl:
```bash
curl http://localhost:8787
curl http://localhost:8787/sandbox/demo/v1/health
```
<Tip>
Containers cache environment variables. If you change `.dev.vars`, either use a new sandbox name or clear existing containers:
```bash
docker ps -a | grep sandbox | awk '{print $1}' | xargs -r docker rm -f
```
</Tip>
## Production Deployment
For production, preview URLs require a custom domain with wildcard DNS routing.
Deploy to Cloudflare:
See [Cloudflare Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) for setup instructions.
## Faster Cold Starts
To speed up cold starts, you can:
1. Create a custom Dockerfile with sandbox-agent pre-installed
2. Pre-install agents in the Docker image
Example `Dockerfile`:
```dockerfile
FROM docker.io/cloudflare/sandbox:0.7.0
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh && \
sandbox-agent install-agent claude && \
sandbox-agent install-agent codex
```bash
wrangler deploy
```
Then update `wrangler.jsonc`:
```jsonc
{
"containers": {
"sandbox": {
"image": "./Dockerfile"
}
}
}
```
For production with preview URLs (direct container access), you'll need a custom domain with wildcard DNS routing. See [Cloudflare Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) for setup instructions.

View file

@ -49,9 +49,9 @@
"deploy/index",
"deploy/local",
"deploy/e2b",
"deploy/daytona",
"deploy/vercel",
"deploy/cloudflare",
"deploy/daytona",
"deploy/docker"
]
},