Important
PREVIEW ONLY This package is provided as a preview for feedback only. APIs are unstable and the design is subject to change.
Suitable for experiments, exploration and prototypes. It is NOT suitable for production use at this time.
A Cloudflare Worker + Durable Object that boots a Container running
the computerd daemon and exposes a minimal write / read / exec
HTTP surface, modelled on the
cloudflare/sandbox-sdk
bridge.
client ββΊ Worker /c/<name>/{file,exec}
β (DO RPC calls)
βΌ
DO (ContainerExample) βββΊ Container βββΊ computerd (:8080)
β² β
β ws://computer.internal/api β
βββββββββββ capnweb session ββββββββ
- The DO constructs a
CloudflareContainerBackendfrom@cloudflare/computer/backends/containerand hands it to aWorkspaceinstance. That backend owns the entire computerd lifecycle: container start, outbound egress interception, port-readiness polling, POST/connectto computerd,/apiupgrade routing, and capnweb session attach. - computerd reaches the Worker through the container's outbound
interception (
ctx.container.interceptOutboundHttp("computer.internal", β¦), set up by the backend). The DO passesctx.exports.WorkspaceProxy({ props: { binding, id } })as the egress fetcher; thatWorkerEntrypoint(re-exported from@cloudflare/computer) routes/apiupgrades back to the owning DO. - When
Workspace.ready()is called for the first time, the backend posts/connectinto computerd with{ base: "http://computer.internal", health: "/health", api: "/api" }. computerd pollscomputer.internal/health, then dialsws://computer.internal/api. Naming both paths in the request keeps the daemon from assembling routes it does not serve. WorkspaceProxy.fetchforwards the upgrade to the DO'sfetch()via the DO binding looked up from its props. The DO'sfetch()delegates tobackend.handleFetch(req), which performs the WebSocket upgrade, resolves the in-flightconnect(), and attaches a capnweb client session to the server-side socket.- The DO exposes a single
getWorkspace()RPC method that returns aWorkspaceStub(anRpcTargetwrapping the innerWorkspace). The Worker's fetch handler callsawait stub.getWorkspace()once per request and then drivesws.fs.writeFile(...)/ws.fs.readFile(...)/ws.runtime.exec(...)directly. Promise pipelining keeps the nested-property pattern (ws.fs.writeFile) at one round trip.
The DO extends the plain DurableObject class from
cloudflare:workers. The container lifecycle plumbing all lives
in CloudflareContainerBackend β the DO is a thin host.
The container mounts computerd's VFS at MOUNT_POINT (/workspace) so
exec'd commands see the same tree the RPC surface reads and
writes. On Cloudflare Containers /dev/fuse is exposed and the
real FUSE backend mounts; under wrangler dev the same image
transparently falls back to the userspace shim (FUSE_MOUNT=auto
in the Dockerfile).
The file handler takes the URL path verbatim as an absolute VFS
path and rejects anything outside /workspace with 400. So
PUT /c/<name>/file/workspace/hello.txt writes
/workspace/hello.txt, and GET /c/<name>/file/workspace/r2/x
reads /workspace/r2/x β the URL and the on-disk path always
match. PUT /c/<name>/file/etc/passwd would be rejected; the
example only exposes the mounted tree.
exec is not path-restricted. The command runs as PID 1 of
the container with no cwd default; it can cat /etc/os-release,
touch /tmp/x, or anything else the container's userland allows.
Only writes that land under /workspace make their way back to
the DO via the FUSE mount.
The DO mounts the Bucket R2 binding at /workspace/r2 via
R2Bucket(env.Bucket). On the first call into the workspace the
mount indexer pages through the bucket, streams each object into
vfs_nodes, and from then on /workspace/r2/<key> reads like any
other file. The mount is read-only; writes under /workspace/r2
reject with EROFS.
Seed the bucket once with the bundled fixture (./seed/data/hello.txt,
which contains the bytes hello world):
# Local miniflare bucket β use this with `wrangler dev`.
npm run seed:r2:local --workspace @example/computer-container
# Real Cloudflare R2 bucket β use this after `wrangler deploy`.
npm run seed:r2 --workspace @example/computer-containerThen:
curl http://127.0.0.1:8787/c/demo/file/workspace/r2/hello.txt
# => hello worldPUT /c/<name>/file/workspace/<path> raw body β writeFile at /workspace/<path>
GET /c/<name>/file/workspace/<path> octet-stream of /workspace/<path>
(any path outside /workspace returns 400)
POST /c/<name>/exec { command | argv, cwd?, encoding? }
no cwd default; container PID 1 inherits /
β JSON { exitCode, stdout, stderr }
<name> selects a DO instance; each gets its own container.
The Worker enables Cloudflare's built-in tracing in
wrangler.jsonc (observability.traces.enabled = true) and wires
the workspace observer to ctx.tracing via the
@cloudflare/computer/observe/cloudflare adapter. Every workspace
operation (workspace.connect, workspace.sync.push,
workspace.sync.pull, workspace.runtime.exec, and the
workspace.fs.* family) opens a span on the runtime, so the
dashboard under Workers β Observability β Traces shows them
nested under the automatic fetch and Durable Object spans.
Exporting the traces to a third-party OpenTelemetry collector (Honeycomb, Grafana Cloud, Axiom, etc.) is configured at the account level in the Cloudflare dashboard; no code change is required in this example.
Requires Docker. The Dockerfile pulls
ghcr.io/cloudflare/computer-computerd-linux-x64:<version> from the
public GitHub Container Registry on first build, so no local image
prep is needed.
npm run dev --workspace @example/computer-containerSmoke test:
# Trigger the container (first call also boots computerd + the capnweb session).
curl http://127.0.0.1:8787/
# Write a file at /workspace/hello.txt
echo 'hello' | curl -X PUT --data-binary @- \
http://127.0.0.1:8787/c/demo/file/workspace/hello.txt
# Read it back
curl http://127.0.0.1:8787/c/demo/file/workspace/hello.txt
# Exec sees the same bytes via the absolute path.
curl -X POST http://127.0.0.1:8787/c/demo/exec \
-H 'content-type: application/json' \
-d '{"command":"cat /workspace/hello.txt && uname -a","encoding":"utf8"}'examples/container/
Dockerfile debian + libfuse + computerd binary (ENTRYPOINT)
wrangler.jsonc Worker + DO + Container binding
src/index.ts Worker handler, DO (ContainerExample)
- Exec is run-and-collect, not streamed. The handler awaits
handle.result()and emits one JSON response. Live streaming needs the DO to expose an async-iterable RPC; v1 keeps the surface flat. - No auth. The egress proxy trusts anything the container sends.
Fine for in-DO traffic (only the owning Worker can address it),
but the moment we expose
computer.internalmore broadly we need a handshake. - One-shot session. If computerd's WebSocket drops mid-session, the
cached
BackendHandlegoes stale and the next call throws.Workspace.ready()will retry on the next call, but in-flight operations are lost. Transparent reconnect is deferred work.