Warning
Experimental. computerd and the container backend may change in any release and
is not yet suitable for production use. See the
status table.
Note
This doc now reflects shipped code in packages/computerd/ and
packages/computer/src/backends/. Items marked (planned) are
deferred work.
The "injected service" is the workspace daemon that runs inside the sandbox container. It owns the FUSE mount, the in-container VFS, the exec runner, and the capnweb RPC endpoint the DO talks to.
The package ships it as a single self-contained Node SEA binary —
computerd — produced by packages/computerd/ (npm package
@cloudflare/computerd, bin name computerd). The binary embeds Node,
the fuse-native prebuilds, and libfuse as SEA assets, so the host
image does not need a Node runtime. Build it with:
npm run build:bin --workspace @cloudflare/computerd
# → artifacts/computerd/computerd-linux-x64
# → artifacts/computerd/computerd-macos-x64examples/container/Dockerfile is the canonical recipe for staging the
binary into a container image.
- FUSE mount. Mounts the in-container VFS at
MOUNT_POINT(default/workspace) so any tool that runs inside the container — node, shells, compilers — sees the same tree the DO sees, with the same paths. The backend is picked byFUSE_MOUNT(defaultauto, see the env-var table below). - Dirty tracking. Writes that flow through FUSE land in the in-container VFS database; the host pulls those revisions back out across the capnweb session. See doc 02 for the sync protocol.
- Exec. Runs shell commands and streams stdout/stderr back over capnweb. See 05. Shell Interface.
- Apply. Accepts changes pushed by the DO and writes them into the VFS, suppressing its own dirty-tracking so deletes don't bounce back.
- Health. Exposes
GET /healthso the host-side workspace can probe for readiness before opening the RPC connection.
computerd listens on a single port (default 45678; the Cloudflare
backend pins it to 8080) and serves:
| Route | Method | Purpose |
|---|---|---|
/health |
GET, HEAD |
Liveness probe; 200 ok\n as soon as the HTTP server binds. The only route reachable without the shared secret. |
/__computerd/info |
GET |
Runtime info: FUSE backend, mount point, port. |
/api |
GET (upgrade) |
WebSocket capnweb transport — the bootstrap stub is WorkspaceRPC. Only the exact path upgrades. A request without an Upgrade header gets 400; an unsupported Sec-WebSocket-Version gets 426 and the versions the server speaks. |
/api/watermarks |
GET, HEAD |
Sync revisions: currentRev, pushRev, fetchCursor. The same values sync.watermarks() returns, for callers that want a few numbers without holding a session. |
/connect |
POST |
Tells computerd to dial out to a caller-supplied endpoint and serve a WorkspaceRPC session over that outbound WebSocket. Used by the Cloudflare backend (see below). |
/ |
GET |
Banner/info page. |
/api is the workspace surface: the session itself, plus anything that
reads through it. /__computerd is daemon introspection, which is why
runtime info sits there and revisions do not.
The capnweb bootstrap interface is WorkspaceRPC (defined in
packages/rpc/), split into sync and shell sub-stubs.
The canonical recipe is examples/container/Dockerfile:
FROM --platform=linux/amd64 debian:stable-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
fuse3 libfuse2t64 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY build/computerd-linux-x64 /usr/local/bin/computerd
RUN chmod +x /usr/local/bin/computerd
ENV PORT=8080
ENV MOUNT_POINT=/workspace
ENV FUSE_MOUNT=auto
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/computerd"]Notes:
- No Node, no
npm install, nopackage.json— the SEA binary embeds everything. libfuseitself is bundled into the binary as a SEA asset; the apt install offuse3 libfuse2t64provides the userland tooling and/dev/fuseplumbing for the host kernel.EXPOSE 8080matches the Cloudflare backend's pinned port. If you runcomputerdoutside Cloudflare Containers, leavePORTunset (default45678) or pick your own.- The port is currently hard-coded in code via
DEFAULT_PORT; making it a build-time variable is on the roadmap (planned).
Provider-agnostic shape — three steps, in order:
- Start the binary. The host-side workspace asks its sandbox
provider to launch
computerdas the container's entrypoint. - Poll the health endpoint. The host issues
HEAD /healthuntil it returns200. Caveat:/healthis wired by the HTTP server and answers200as soon as the socket binds. In the FUSE-enabled path the mount is awaited beforelisten, so by the time/healthanswers FUSE is up too. WithFUSE_MOUNT=nonethere is no FUSE step at all. - Open the capnweb session. Either the host upgrades to
/apidirectly, or it askscomputerd(viaPOST /connect) to dial out to an endpoint it controls and serve the session over that outbound socket. Either way, the bootstrap stub isWorkspaceRPC.
Cloudflare Computer has one backend for each container scheduling policy.
ContainerBackend is the default. It works with containers that the durable
object schedules, configured with scheduling_policy: "durable_object" and
an images map. Each launch selects an image from
ctx.container.images. The backend can also request an instance size and
pass options such as entrypoint, labels, and snapshot settings to
container.start().
LegacyContainerBackend is deprecated; use ContainerBackend for new
code. It works with containers that the platform schedules.
The containers block chooses the image and instance size, so this backend
passes only the environment and internet setting when it starts the
container.
Both backends use the same connection flow:
- Start the container. The image's
ENTRYPOINTrunscomputerddirectly. The backend setsPORT=8080, preserves the image'sFUSE_MOUNTsetting, and addsRPC_CLIENT_SECRET. It records the launch settings so it can reject or replace a running container with the wrong configuration. - Wire egress.
container.interceptOutboundHttp(egressHost, egress)routes outbound HTTP from the container back to a WorkerFetcherowned by the durable object. - Probe health. The backend sends
HEAD /healththrough the container's TCP port untilcomputerdresponds. - Open the WebSocket. The backend prepares an upgrade slot before it
posts to
/connect.computerdthen dials the intercepted egress URL, which routes the upgrade back tohandleFetch(). The capnweb session runs over that WebSocket.
A reconnect creates a complete new session. If the WebSocket closes,
Workspace drops the old backend handle and calls the selected backend's
connect() method again. The replacement starts or adopts the container,
checks health, and repeats the /connect handshake. Sync and process
lifecycle operations get one retry when replay is safe. A command is only
retried when the request was not dispatched.
These are the variables computerd actually consumes (see
packages/computerd/src/cli/computerd.ts and packages/computerd/src/fuse/backend.ts):
| Variable | Default | Meaning |
|---|---|---|
PORT |
45678 |
Port the HTTP server listens on. CF backend pins this to 8080. |
MOUNT_POINT |
/workspace |
Absolute path inside the container to mount the FUSE filesystem at. Ignored when FUSE_MOUNT=none. |
FUSE_MOUNT |
auto |
Backend selector: auto probes /dev/fuse (linux) or macFUSE (darwin) and falls back to the userspace shim; fuse / macfuse require the corresponding real backend; shim forces the userspace shim; none skips the mount entirely. |
EXEC_LOG_MAX_BYTES |
runner default | Caps the per-exec stdout/stderr log retained in-memory. |
RPC_CLIENT_SECRET |
unset | When set, every route except /health requires it as Authorization: Bearer <secret>, including the /api upgrade. Unset leaves the surface open. The Cloudflare backend generates one per workspace and sets it at launch. |
COMPUTER_VAR_* |
unset | Forwarded into every shell.exec command with the prefix stripped, so COMPUTER_VAR_NODE_ENV arrives as NODE_ENV. |
LOG_FILE |
unset | If set, every console.log / console.error line and any uncaughtException / unhandledRejection is also appended to this file. Stdout/stderr behaviour is unchanged. |
When LOG_FILE is set, computerd mirrors console output into the file in
addition to stdout/stderr. See "Failure handling" below for the crash
handlers that share the same logger.
Today:
computerdinstallsuncaughtExceptionandunhandledRejectionhandlers viainstallLogging()(incli/logger.ts). Each handler writes a formatted entry to the same logger —console.errorand, ifLOG_FILEis set, the file too — then callsprocess.exit(1).- Logs go to stdout/stderr by default. When
LOG_FILEis set, everyconsole.log/console.errorline is also appended to that file (open inO_APPENDmode, ISO-timestamped,[info]/[error]prefixed). No rotation; the operator is expected to manage the file. FUSE_MOUNT=fuse(ormacfuse) errors at startup if the corresponding kernel surface isn't available;FUSE_MOUNT=autosilently falls back to the userspace shim instead. The only "skip the mount entirely" path is the explicitFUSE_MOUNT=noneopt-out.
Planned:
- Soft-fail on FUSE-detect failure: the server still starts, exposes
RPC, and reports
fuseActive=falsevia/__computerd/info. Whether that includes a host-FS mirror for in-container writes is still open.
The computerd process is long-lived and outlives Durable Object restarts — the sandbox container is reaped only when its lifetime policy says so, and a fresh Durable Object incarnation reconnects to the same running daemon over a new WebSocket. The container monitor and transport error classifier drop stale handles so an operation can reconnect through the readiness gate.
Caveat: no on-disk persistence yet (packages/computerd/README.md). The same in-memory VFS across Durable Object restarts only holds while the container process is alive. A container restart loses VFS state. Watermark reconciliation and the next push rebuild the mirror from Durable Object storage, but reconnect cannot recover container-local files that were never pulled before the process died.
These behaviours aren't fully specified yet. File an issue if your use case depends on a particular resolution.
- Connection auth. Today the WebSocket endpoint trusts anything that can reach the port. On Cloudflare Containers that's safe because only the owning DO can reach the container's TCP port, but the moment we support providers with broader network exposure the server needs its own auth on the RPC handshake. Candidates: a short-lived shared secret minted by the workspace and passed via an env var, a per-connection challenge, or an mTLS client cert provisioned at boot. The wire surface (08. Capnweb Interface) will need a hello/auth phase before the bootstrap stub is exposed.
- Process user and file ownership.
computerdcurrently runs as whatever user the sandbox image'sENTRYPOINTruns as — typicallyroot, which is a poor default for a process that mounts FUSE and spawns arbitrary shell commands. The intent is to runcomputerdas an unprivileged user so a misbehaving exec can't escalate, but exec'd commands need to be able to read and write the FUSE-mounted tree. Open: which user owns the mount, what userexecruns as (workspace? per-exec dynamic?), and howallow_other/ setuid / shared-group ownership get wired so the two see the same files without opening the mount to every process in the container. - FUSE soft-fail behaviour. See "Failure handling" above —
whether the degraded
fuseActive=falsemode includes a host-FS mirror or just refuses container-side writes is unresolved.