Skip to content

feat(cloudflare/container): build images from a prebuilt context directory#583

Open
Butch78 wants to merge 4 commits into
alchemy-run:mainfrom
Butch78:feat/container-prebuilt-context
Open

feat(cloudflare/container): build images from a prebuilt context directory#583
Butch78 wants to merge 4 commits into
alchemy-run:mainfrom
Butch78:feat/container-prebuilt-context

Conversation

@Butch78

@Butch78 Butch78 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Build a Cloudflare Container straight from a self-contained Docker build context, skipping JS bundling entirely. Cloudflare Containers run any linux/amd64 image; only the resource assumed a JS entrypoint.

export class SqlCompute extends Cloudflare.Container<SqlCompute>()("sql-compute", {
  context: "./sql-container",      // dir with its own Dockerfile
  // dockerfile: "Dockerfile.prod" // path, or inline contents; defaults to "Dockerfile"
  instanceType: "standard-1",
}) {}
  • context (a build-context dir) replaces the JS-bundling pipeline when no main is set. main and context are mutually exclusive; main stays optional.
  • dockerfile accepts a path or inline contents (told apart by whether it resolves to a file), in both modes. Context mode passes docker build -f; dockerBuild gains a dockerfile option.
  • One resolveContextMode helper decides mode + Dockerfile location for both the hash and the build, so they can't drift.
  • The context image hash folds in each file's contents (streamed, never buffered whole) plus its unix mode, honoring .dockerignore and always skipping .git, so content changes trigger a rebuild and rollout.

Used to deploy a Rust (DataFusion) container attached to a Durable Object: builds, pushes to registry.cloudflare.com with the content-addressed tag, and serves via ctx.container.

The caller supplies a complete Docker build context (its own Dockerfile
plus everything it COPYs) and the JS bundling pipeline is skipped: no
main, no runtime shim, no appended ENTRYPOINT. The image hash folds in
every file in the directory so content changes still trigger a rebuild
and rollout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 11, 2026 06:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for deploying Cloudflare Containers from a fully self-contained, prebuilt Docker build context (skipping JS bundling and generated Dockerfile/entrypoint logic).

Changes:

  • Introduces prebuiltContext to allow building/pushing images directly from a caller-provided Docker context directory.
  • Adjusts validation so main is no longer required when prebuiltContext is provided.
  • Updates the build pipeline to branch between “prebuilt context” and “bundled JS context” modes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
packages/alchemy/src/Cloudflare/Container/ContainerApplication.ts Implements prebuiltContext mode: hashes the directory contents for tagging and builds the provided context verbatim.
packages/alchemy/src/Cloudflare/Container/Container.ts Makes main optional to support prebuiltContext-only containers and documents the new behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +36 to +40
/**
* JS entrypoint. Required unless `prebuiltContext` supplies a complete
* non-JS Docker build context.
*/
main?: string;
Comment on lines +437 to +447
const entries = yield* fs.readDirectory(contextDir, {
recursive: true,
});
const fileHashes: Record<string, string> = {};
for (const entry of [...entries].sort()) {
const fullPath = `${contextDir}/${entry}`;
const info = yield* fs.stat(fullPath);
if (info.type === "File") {
fileHashes[entry] = yield* sha256(yield* fs.readFile(fullPath));
}
}
});
const fileHashes: Record<string, string> = {};
for (const entry of [...entries].sort()) {
const fullPath = `${contextDir}/${entry}`;
Comment on lines +443 to +446
const info = yield* fs.stat(fullPath);
if (info.type === "File") {
fileHashes[entry] = yield* sha256(yield* fs.readFile(fullPath));
}
Comment on lines +431 to +434
if (props.prebuiltContext) {
// Prebuilt-context mode: the caller supplies a complete Docker
// build context. Hash every regular file in it (sorted, so the
// digest is order-stable) in place of the bundle hash.
Comment on lines +455 to +456
return { files: [], imageRef, imageHash };
}
Comment on lines +686 to +693
if (props.prebuiltContext) {
// Prebuilt-context mode: build the caller's directory verbatim,
// with its own Dockerfile, no bundle files, no appended ENTRYPOINT.
yield* dockerBuild({
tag: imageRef,
context: props.prebuiltContext,
platform: "linux/amd64",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think prebuiltContext is an intuitive name for this property. I do agree it should be possible to skip bundling.

Why not just call it context?: string and make main?: string optional. Skip JS bundling if main and dockerfile are not provided?

I also wonder if dockerfile should be a path (or maybe even allow it to be both a path or dockerfile contents because we can detect that with path.resolve?

Cloudflare.Container("container", {
  context: "./sql-container",
  // dockerfile: "Dockerfile.custom" // <- defaults to "Dockerfile"
})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, done in 9fbf01d:

  • Renamed prebuiltContextcontext. JS bundling is skipped whenever there's no main (the two are now mutually exclusive).
  • dockerfile accepts a path or inline contents, resolved by whether it points at an existing file (path.resolve against the context, then cwd). In context mode it defaults to <context>/Dockerfile and is passed via docker build -f.
Cloudflare.Container("container", {
  context: "./sql-container",
  // dockerfile: "Dockerfile.custom" // path, or inline "FROM …" contents
})

…dress review

Respond to PR review (sam-goodwin + Copilot):

- Rename `prebuiltContext` -> `context`; build context mode iff no `main`
  (the two are mutually exclusive). `main` stays optional.
- `dockerfile` accepts a path OR inline contents, resolved by existence,
  in both modes. Context mode defaults to `<context>/Dockerfile` and
  passes `docker build -f`; `dockerBuild` gains a `dockerfile` option.
- Single `resolveContextMode` helper decides mode + Dockerfile location
  for both the hash and build steps, so they can't drift.
- Context hash: honor `.dockerignore` + always skip `.git`, fold in unix
  mode, and stream each file via `sha256File` (no whole-file buffering).
- Cross-platform `path.join` instead of string concatenation.
- Tests: streaming `sha256File`, and `docker build -f` custom Dockerfile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Butch78 Butch78 changed the title feat(cloudflare/container): prebuiltContext for non-JS container images feat(cloudflare/container): build images from a prebuilt context directory Jun 11, 2026
@Butch78

Butch78 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @copilot — addressed in 9fbf01d (alongside the prebuiltContextcontext rename @sam-goodwin asked for):

  • Path building / Windows — now uses path.join everywhere instead of string concatenation; directory-walk keys are normalized to POSIX.
  • Reads every file into memory — context files are now hashed with a streaming sha256File (chunks through node:crypto, never buffered whole). Added .dockerignore support plus an always-on .git skip so large/volatile dirs aren't walked.
  • File mode / symlinks — each file's unix mode is folded into the hash. fs.stat follows symlinks (effect exposes no lstat), so links are hashed by their dereferenced target; documented on the helper.
  • Duplicated branching — extracted a single resolveContextMode helper that decides the mode and resolves the Dockerfile location; both the hash step and the build step consume it, so they can't drift.
  • main optional weakens typing — kept main optional per the API direction in this thread (main/context mutually exclusive, validated at runtime) rather than a discriminated union, since the union would have to duplicate the full shared prop set across both members.

Added unit coverage for streaming sha256File and for the new docker build -f path.

Comment thread packages/alchemy/src/Bundle/Docker.ts Outdated
Comment on lines +61 to +68
export const sha256File = Effect.fn(function* (filePath: string) {
const fs = yield* FileSystem.FileSystem;
const hash = createHash("sha256");
yield* fs
.stream(filePath)
.pipe(Stream.runForEach((chunk) => Effect.sync(() => hash.update(chunk))));
return hash.digest("hex");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we already have a util for this in sha256.ts?

If not, add it to that file for re-usability.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea — moved it to Util/sha256.ts as sha256File (streams via the FileSystem service through node:crypto, alongside sha256/sha256Object) in 607f4ed. Tests moved to test/Util/sha256.test.ts.

Per review: keep the streaming file hash with the other SHA-256 helpers
for reuse instead of in Bundle/Docker.ts. Tests move to test/Util/sha256.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Butch78

Butch78 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the suggestions, @sam-goodwin — both are in: context + path-or-inline dockerfile, and the streaming hash now lives in Util/sha256.ts. Let me know if you'd like anything else tweaked.

@sam-goodwin sam-goodwin self-assigned this Jun 11, 2026
@john-royal john-royal self-assigned this Jun 16, 2026
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.

4 participants