Fall 2026brianmars.comBuilt at EGBT Technologies
ReferenceRuntime

workerd for a wrangler project

A wrangler-built Worker on bare workerd, with Durable Objects on local disk.

Names and addresses are placeholders. The list says what each one stands for.

What it does here

workerd is the runtime Cloudflare runs Workers on. Here it runs the yaos Worker and its two Durable Objects in a container, with their SQLite files on the data disk.

From the manual

Canonical setup

wrangler bundles the Worker and uploads it to Cloudflare. workerd loads a Worker from a Cap'n Proto config file instead, and serves it with workerd serve config.capnp. Upstream: cloudflare/workerd.

Edited for this build

In this build

  1. Bundle with esbuildThe same import conditions wrangler uses: workerd, worker, browser, with cloudflare:* left external. Only the bundle is needed, not the upload.
  2. Durable Objects with SQLEach namespace gets enableSql = true and its own uniqueKey.
  3. Durable Object storage on local diskdurableObjectStorage = (localDisk = "do-disk"), pointing at a writable disk service backed by a directory.
  4. Private backends as `external` servicesThe S3 store is reachable only through a service binding.
  5. Secrets via `fromEnvironment`They come from the container's environment, so none of them end up in the image.
  6. workerd pinned1.20261019.1, about two weeks old at build time, not the release from that morning.
config.capnp
using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [
    (name = "yaos", worker = .yaosWorker),
    (name = "do-disk", disk = (path = "/data/do", writable = true)),
    (name = "garage", external = (address = "garage:3900", http = ())),
  ],
  sockets = [ (name = "http", address = "*:8787", http = (), service = "yaos") ],
);

const yaosWorker :Workerd.Worker = (
  modules = [ (name = "worker.js", esModule = embed "dist/worker.js") ],
  compatibilityDate = "2026-03-02",
  bindings = [
    (name = "YAOS_SYNC", durableObjectNamespace = "VaultSyncServer"),
    (name = "YAOS_CONFIG", durableObjectNamespace = "ServerConfig"),
    (name = "S3", service = "garage"),
    (name = "S3_ACCESS_KEY_ID", fromEnvironment = "S3_ACCESS_KEY_ID"),
    (name = "S3_SECRET_ACCESS_KEY", fromEnvironment = "S3_SECRET_ACCESS_KEY"),
    # ... S3_BUCKET, SYNC_TOKEN and the optional yaos flags, same pattern
  ],
  durableObjectNamespaces = [
    (className = "VaultSyncServer", uniqueKey = "yaos-vault-sync", enableSql = true),
    (className = "ServerConfig", uniqueKey = "yaos-server-config", enableSql = true),
  ],
  durableObjectStorage = (localDisk = "do-disk"),
);

The Dockerfile has two stages. The build stage fetches yaos at a pinned commit, runs npm ci in server/, installs workerd from npm, and bundles. The runtime stage has only the workerd binary, the config and the bundle.

worker/Dockerfile
FROM node:22-slim AS build
ARG YAOS_REF=2174eb89538ce7e213e9e33a0e75ecaf1a3f1f58
ARG WORKERD_VERSION=1.20261019.1
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates
WORKDIR /build/yaos
RUN git init -q && git remote add origin https://github.com/kavinsood/yaos \
 && git fetch -q --depth 1 origin "$YAOS_REF" && git checkout -q FETCH_HEAD
RUN cd server && npm ci --no-audit --no-fund
WORKDIR /build/shim
COPY package.json ./
RUN npm install && npm install --no-save "workerd@${WORKERD_VERSION}"
COPY entry.js r2-s3.js build.mjs config.capnp ./
RUN node build.mjs \
 && cp node_modules/@cloudflare/workerd-linux-64/bin/workerd /build/workerd

FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates
COPY --from=build /build/workerd /usr/local/bin/workerd
WORKDIR /app
COPY --from=build /build/shim/config.capnp ./config.capnp
COPY --from=build /build/shim/dist ./dist
CMD ["workerd", "serve", "/app/config.capnp", "--verbose"]
Where it breaks

Seams

Where this meets the rest of the build. Each one leads with what you will actually see.

The Worker meets a private Docker network

Symptomfetch("http://garage:3900/...") fails, though the name resolves inside the container

By default workerd's global outbound only allows public addresses, and a Docker network is private. Don't widen the outbound. Give the store its own external service and call it through the binding (env.S3.fetch(...)).

Verify

The Durable Object directory holds one .sqlite file per object instance, plus workerd's metadata.sqlite.

Checks
ls /srv/data/yaos/workerd/do
docker compose logs yaos
Corrections and updates
  1. 4 Nov 2026First written.