| description | Deploy marimohub on Cloudflare Workers with R2, Containers, and Access. |
|---|
Run marimohub fully serverless — no always-on server, no database:
- R2 for storage,
- Containers (a
SandboxDurable Object) for compute, - Access for auth.
All three are platform bindings, not env credentials, so the Cloudflare deploy
is an SDK/library composition rather than the env-driven
container. You deploy a copy of examples/cloudflare-worker — a small, complete
worker that wires the Cloudflare adapters into the same createApi everything else
uses.
- A Cloudflare account with Workers, R2, and Containers enabled (Containers requires a paid Workers plan).
wranglerviapnpm(the example already depends on it) and Docker running locally (wrangler builds the sandbox image).- This repo checked out, with
pnpm installrun once.
The worker lives at
examples/cloudflare-worker.
Copy it somewhere you control (or deploy it in place) and open wrangler.jsonc —
that file is where you point everything at your account.
These bindings come pre-wired; you generally only rename them:
- R2 bucket →
NOTEBOOKS_BUCKET(storage and the credential-less sandbox mount). - Container + Durable Object →
SANDBOX(theSandboxclass, image fromsandbox.Dockerfile). The entrypoint also exportsContainerProxy, required for the binding mount below. - Assets → the web UI build (
packages/web/dist). - Cron →
*/5 * * * *runs session maintenance (there's no always-on replica).
pnpm exec wrangler r2 bucket create <bucket>Set the same name in wrangler.jsonc (r2_buckets[].bucket_name and
R2_BUCKET_NAME).
Protect the app host only with a self-hosted Access application and an Allow policy (e.g. your email domain). Access adds a signed JWT that the worker verifies — see Auth.
From the Access application, copy:
- the team name (
<team>.cloudflareaccess.com) →ACCESS_TEAM - the Application Audience (AUD) tag →
ACCESS_AUD
Set the runtime vars (non-secret, committed):
The worker refuses to start if AUTH_MODE is unset or unknown — there is no
insecure default.
See Editor sessions for the sharing and takeover model.
pnpm --filter @marimo-hub/web build # build the web UI assets
cd examples/cloudflare-worker # or your copy
pnpm exec wrangler deploy # provisions R2 + Container + DO + cronThe first wrangler deploy provisions everything. See
Troubleshooting for the known first-deploy hiccups.
Add a routes entry with custom_domain: true for your app host (its zone must
be active in the account):
custom domains.
Cloudflare provisions the DNS record and an edge TLS cert; a deep subdomain
(a.b.example.com) gets its own cert — allow a few minutes before HTTPS works.
Give every notebook a working AI assistant with no user key — see Managed AI for the model. On Workers, set the upstream as vars/secrets:
# non-secret: add to wrangler.jsonc "vars"
# AI_UPSTREAM_BASE_URL = "https://api.openai.com/v1"
# AI_MODEL = "gpt-4o-mini"
# secrets (never committed):
pnpm exec wrangler secret put AI_UPSTREAM_API_KEY # the real upstream key
pnpm exec wrangler secret put AI_SESSION_SECRET # signs per-session tokensManaged AI turns on only when all four are present. The real upstream key stays on the worker and is never injected into a sandbox.
The sandbox mounts the notebook bucket as a filesystem. Two modes (SDK docs):
-
R2 binding mount (default,
R2_S3_ENDPOINTempty) — mounts theNOTEBOOKS_BUCKETbinding with no credentials in the container (the Durable Object re-signs S3 egress). No secrets to set. Requires ther2_bucketsbinding and the entrypoint toexport { ContainerProxy }(both already wired). -
External S3 endpoint — set
R2_S3_ENDPOINT(+R2_BUCKET_NAME) and theR2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEYsecrets to mount any S3-compatible bucket (R2, AWS S3, GCS, …):pnpm exec wrangler secret put R2_ACCESS_KEY_ID pnpm exec wrangler secret put R2_SECRET_ACCESS_KEY
Kernels run untrusted code, so they must be cross-origin from the app. Two ways (Security → Kernel exposure):
- Quick tunnels (default,
SANDBOX_HOSTNAMEempty) — each kernel gets a random, unguessable*.trycloudflare.comURL. Zero config, no second domain. - Subdomain — set
SANDBOX_HOSTNAMEto a separate isolated domain and add its route. The worker fails closed if it shares an origin/parent with the app host.
Compute is a pluggable port, independent of storage and auth — you
can run kernels on another backend without touching R2 or Access. This example ships
an opt-in E2B wiring (src/e2b.ts + src/tieredCompute.ts):
managed microVM sandboxes with direct per-port kernel URLs and no container image to
build. E2B and Cloudflare are separate backends — see E2B compute
for the backend itself (SDK, template, the login-shell env gotcha) and
examples/e2b-template
for a copy-pasteable marimo template. src/tieredCompute.ts additionally shows E2B
primary with a Cloudflare-Sandbox fallback.
The one Worker-specific catch: the e2b SDK must be statically bundled (a Worker
can't lazily import() it), so src/e2b.ts injects a static import into
createE2bClient.
- Check the deployed Worker route.
- Sign in through Cloudflare Access.
- Create and save a notebook.
- Start a kernel and confirm it opens through the configured tunnel or sandbox hostname.
- Confirm the R2 bucket contains hub objects after saving.
- Protect only the app host with Cloudflare Access. Kernel hosts must stay isolated from the app origin.
- Store upstream AI keys and S3 keys as Worker secrets, never committed vars.
- Keep the sandbox image lean enough for Cloudflare Containers.
- Re-run
wrangler deployafter changing bindings or Durable Object migrations.
docker loginfails on macOS (error storing credentials … -25299): a stale keychain entry. Rundocker logout registry.cloudflare.com, then redeploy.wrangler deployfails building the sandbox image (TLS handshake timeoutpullingcloudflare/sandbox): a transient Docker Hub blip, not your config — just re-run once connectivity is back.- First deploy 500s on
POST /containers/applications("can't fetch the application"): the app is usually created anyway — just re-runwrangler deployand it reconciles. - TLS handshake fails right after first deploy: the custom-domain edge cert is still issuing. Wait a few minutes; check SSL/TLS → Edge Certificates.
- Kernel container won't start (
permanent error): usually an oversized image — keepsandbox.Dockerfilelean (per-notebook deps install at runtime via uv). - Credential-less R2 mount fails immediately: the entrypoint must
export { ContainerProxy }and ther2_bucketsbinding name must match what the worker mounts; on older wrangler, also add theenable_ctx_exportscompat flag. - (E2B) sandboxes silently never start and everything runs on the fallback: the
E2B_API_KEYsecret is wrong. If you piped it in (… | wrangler secret put), strip the trailing newline (printf '%s' "$key" | …) — a newline-suffixed key fails auth (401), which the tiered provider swallows as a fallback.
Auth · Storage · Compute · Managed AI · Configuration