Skip to content

feat(cloudflare): Durable Object-managed containers - #1904

Closed
Butch78 wants to merge 4 commits into
alchemy-run:mainfrom
Butch78:feat/cloudflare-do-containers
Closed

Butch78 wants to merge 4 commits into
alchemy-run:mainfrom
Butch78:feat/cloudflare-do-containers

Conversation

@Butch78

@Butch78 Butch78 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

schedulingPolicy: "durable_object" makes a Container Durable Object-managed: the application carries no image, instance type or instance count, and the Durable Object picks both each time it starts a container (Cloudflare's announcement).

Cloudflare.Container<Sandbox>("SANDBOX", {
  className: "Sandbox",
  schedulingPolicy: "durable_object",
  images: {
    node: { context: "./images/node" },
    python: { image: "python:3.13-slim" },
  },
});

// inside the Durable Object
const images = yield* container.images;
yield* container.start({ image: images.python, instance: "standard-2" });
  • Each named image, plus the container's own image as "default" when it has one, is built or re-published into its own repository. It is then prepared on Cloudflare's network (image-preparations, polled the way wrangler polls it).
  • The Worker upload declares { class_name, name, images } for the class, so images are versioned with the Worker.
  • Precreate only publishes images. Reconcile creates the image-less application once the Worker has created the namespace.
  • Switching a container between the two models deletes and recreates its application, because a namespace can back only one.
  • The runtime handle gains images and snapshotContainer().
  • images may be empty. The Durable Object then starts Cloudflare's pre-distributed cloudflare/debian-trixie by name, or a snapshot of it. The upload declares the class as { class_name, name }.
  • alchemy dev runs the "default" image, or the first one if there is no default; images is not emulated locally.
  • main is refused in this mode: the application has no environment to carry the bundled program's bindings.

Needs alchemy-run/distilled#684 (the submodule points at that branch). The distilled bump also makes an application's configuration, instances and maxInstances optional; the first commit adapts existing reads to that.

🤖 Generated with Claude Code

Matthew Aylward and others added 2 commits September 30, 2026 16:52
…ners

The distilled bump makes a container application's `configuration`,
`configuration.image`, `instances` and `maxInstances` optional, because
an application created for a Durable Object-managed container carries
none of them. Read them as absent rather than assume a legacy
application: instance counts default to 0, a missing configuration to
`{}`, and an existing application with no image publishes one instead
of reusing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`schedulingPolicy: "durable_object"` makes a Container Durable
Object-managed: the application carries no image, instance type or
instance count, and the Durable Object picks an image and an instance
type each time it starts a container.

```ts
Cloudflare.Container<Sandbox>("SANDBOX", {
  className: "Sandbox",
  schedulingPolicy: "durable_object",
  images: {
    node: { context: "./images/node" },
    python: { image: "python:3.13-slim" },
  },
});

// in the Durable Object
const images = yield* container.images;
yield* container.start({ image: images.python, instance: "standard-2" });
```

- `images` names the images, each built or re-published like the
  container's own (which is published as `"default"` when declared)
  into a repository of its own, then prepared on Cloudflare's network
  (`POST image-preparations`, polled as wrangler does).
- The Worker upload declares `{ class_name, name, images }` for the
  class, so the images are versioned with the Worker.
- Precreate only publishes images; reconcile creates the image-less
  application once the Worker has created the namespace. Switching an
  existing container between the two models deletes and recreates its
  application, since a namespace backs one.
- The runtime handle gains `images` and `snapshotContainer()`.
- `alchemy dev` runs the `"default"` (or first) image; `images` is not
  emulated locally. `main` (a bundled program) is refused in this mode,
  because the application has no environment to carry its bindings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alchemy-version-bot

alchemy-version-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

pnpm install https://pkg.alchemy.run/alchemy/pr:1904:1c859a3
@alchemy.run (6)
pnpm install https://pkg.alchemy.run/@alchemy.run/better-auth/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@alchemy.run/cloudflare-runtime/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@alchemy.run/frontend-frameworks/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@alchemy.run/node-utils/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@alchemy.run/floci/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@alchemy.run/pkg/pr:1904:1c859a3
@distilled.cloud (16)
pnpm install https://pkg.alchemy.run/@distilled.cloud/core/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/acme/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/aws/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/axiom/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/cloudflare/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/doppler/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/fly-io/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/github/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/hetzner/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/infisical/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/neon/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/prisma/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/planetscale/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/railway/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/stripe/pr:1904:1c859a3
pnpm install https://pkg.alchemy.run/@distilled.cloud/zerossl/pr:1904:1c859a3

Published Sep 30, 2026, 7:54 PM UTC. Expires Oct 7, 2026, 7:54 PM UTC, extended while this pull request is open.

Matthew Aylward and others added 2 commits September 30, 2026 17:25
…napshot

The live suite now round-trips a filesystem snapshot: write a marker
with `exec`, `snapshotContainer()`, destroy, start again from
`{ containerSnapshot: { id } }` and read the marker back. The round
trip runs once with a long timeout, since a readiness retry would
restart it mid-flight, and reports each step's duration.

`exec` runs as the image's own user: with `user: "root"` Cloudflare
answers an internal error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Durable Object-managed container can now declare no images at all.
Its Durable Object starts Cloudflare's pre-distributed system image by
name, or a filesystem snapshot of one:

```ts
Cloudflare.Container<Sandbox>("SANDBOX", {
  className: "Sandbox",
  schedulingPolicy: "durable_object",
});

container.start({ image: "cloudflare/debian-trixie", instance: "lite" });
container.start({ containerSnapshot: { id }, instance: "standard-1" });
```

The upload declares such a class by `{ class_name, name }` alone, the
image-less shape wrangler sends. `alchemy dev` refuses one with a
message, since workerd starts a declared image and has neither the
system image nor snapshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JRGiardiniere

JRGiardiniere commented Oct 2, 2026 •

Copy link
Copy Markdown

Same response-shape note as on #1905: live durable_object applications omit instances, max_instances and version. With distilled#684, strict decoding still fails on version; lenient succeeds. Live JSON is in alchemy-run/distilled#685 (comment). Separately, this PR deletes and recreates the application when the scheduling policy changes. #1906 and Cloudflare's docs treat the policy as immutable, and #1905 refuses that change instead.

@Butch78

Butch78 commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favour of #1905, which covers the same ground and more (named images, exec streams, snapshots, local dev through Docker) and, like Cloudflare's docs, refuses a scheduling-policy change rather than recreating the application. Our deploy notes from trying this preview are on #1905.

@Butch78 Butch78 closed this Oct 3, 2026
Butch78 added a commit to Butch78/ficus that referenced this pull request Oct 3, 2026
alchemy-run/alchemy#1904 is closed in favour of #1905, so alchemy is
pinned to #1905's preview instead. Under it:

- Changing an application's scheduling policy needs a new application
  and a new Durable Object class, so scoring moves to `Scorer` and the
  agent's container to `Worktree`, each with a new container application
  (`ScorerContainer`, `WorktreeContainer`). The tree and agents bind the
  new classes.
- The image is a named image, `images.scorer`; `env` is not valid on the
  policy, so the context is read through the scorer binary's hash, which
  keeps the build edge.
- `Cloudflare.Containers.bind` attaches a container without starting it,
  which replaces the internal `~alchemy/Container/Binding` key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants