A Cloudflare Worker that runs OpenCode inside Sandbox SDK containers, with subdomain-based routing to multiple isolated instances. Demonstrates programmatic command execution via execStream, git cloning, and directory backup/restore using R2.
Each sandbox gets its own subdomain. The Worker routes requests based on the first subdomain label:
| Route | Method | Description |
|---|---|---|
<id>.yourdomain.com/ |
GET | OpenCode web UI |
<id>.yourdomain.com/create |
POST | Start the OpenCode server |
<id>.yourdomain.com/run |
POST | Execute a command (streams SSE via execStream) |
<id>.yourdomain.com/gitClone |
POST | Clone a git repo into the container |
<id>.yourdomain.com/snapshot |
POST | Back up a directory to R2 |
<id>.yourdomain.com/restore/:snapshotId |
POST | Restore a directory from R2 |
The bare domain serves a static Alpine.js frontend that lets you interact with all of these.
npm installcp .dev.vars.example .dev.vars
# Edit .dev.vars and set ANTHROPIC_API_KEYFor production, set it as a secret:
wrangler secret put ANTHROPIC_API_KEYIn wrangler.jsonc, replace two placeholder values:
CLOUDFLARE_ACCOUNT_ID-- your Cloudflare account ID (find it in the dashboard URL or viawrangler whoami)DOMAIN-- the domain you'll use for routing (e.g.yourdomain.com), plus update theroutesentries to match
The snapshot/restore system stores backups in R2 using presigned URLs. You need an R2 bucket and API credentials.
# Create the bucket
wrangler r2 bucket create opencode-sandbox-snapshots
# Create an R2 API token in the Cloudflare dashboard:
# R2 > Manage R2 API Tokens > Create API Token
# Then set both as secrets:
wrangler secret put R2_ACCESS_KEY_ID
wrangler secret put R2_SECRET_ACCESS_KEYSubdomain routing requires a custom domain with a wildcard DNS record. Follow the Sandbox SDK production deployment guide to set this up.
In short: add a * CNAME record pointing to your Workers route. This takes a few minutes and only needs to be done once.
Update the routes in wrangler.jsonc to match your domain:
npx wrangler deployFor local development:
npm run devNote that subdomain routing won't work locally -- you'll hit the static frontend on localhost:8787 but can't route <id>.localhost:8787 to containers. Use wrangler deploy to test the full flow.
# Start a sandbox
curl -X POST https://my-sandbox.yourdomain.com/create
# Clone a repo
curl -X POST https://my-sandbox.yourdomain.com/gitClone \
-H 'Content-Type: application/json' \
-d '{"repo": "https://github.com/user/repo"}'
# Run a command (returns SSE stream)
curl -X POST https://my-sandbox.yourdomain.com/run \
-H 'Content-Type: application/json' \
-d '{"command": "ls -la /workspace"}'
# Snapshot a directory
curl -X POST https://my-sandbox.yourdomain.com/snapshot \
-H 'Content-Type: application/json' \
-d '{"dir": "/workspace"}'
# Restore a snapshot
curl -X POST https://my-sandbox.yourdomain.com/restore/<snapshot-id>Open the UI at your bare domain and follow these steps:
- Enter a sandbox ID (e.g.
test) and click Create - Clone a repo — paste
https://github.com/cloudflare/workers-sdkinto the Git clone field and click Clone - Run
ls /workspaceto verify the files are there - Click Snapshot — copy the returned snapshot ID
- Run
rm -rf /workspace/*to delete everything - Run
ls /workspaceagain to confirm it's empty - Paste the snapshot ID into the Restore field and click Restore
- Run
ls /workspaceone more time — the files are back
If you're putting container creation in a user's request path and want to avoid cold starts, check out cf-container-warm-pool. It maintains a pool of pre-warmed containers so the first request doesn't pay the startup cost.
- Faster backups -- Backup and restore will get significantly faster and integrate directly with R2 without requiring separate API tokens.
- Automatic snapshots -- On-start and on-stop hooks for automatic backup/restore, so sandbox state persists across container restarts without manual intervention.
- Sandbox SDK docs -- full API reference, guides, and tutorials
- Sandbox SDK GitHub -- source code and examples
- OpenCode -- the AI coding agent running inside each sandbox