Skip to content

[wrangler] Expand auto-config deploy flows to support express.js, index.html, containers#14554

Draft
irvinebroque wants to merge 12 commits into
mainfrom
auto-config-expansion-plan
Draft

[wrangler] Expand auto-config deploy flows to support express.js, index.html, containers#14554
irvinebroque wants to merge 12 commits into
mainfrom
auto-config-expansion-plan

Conversation

@irvinebroque

@irvinebroque irvinebroque commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

What Users Can Now Do
Deploy a standalone HTML file without creating project files:

wrangler deploy index.html \
  --name my-site \
  --compatibility-date 2026-07-04

For a named file, Wrangler serves it as both / and its original filename:

wrangler deploy landing.html \
  --name launch-page \
  --compatibility-date 2026-07-04

Uploads assets like:

{
  "/index.html": { "hash": "...", "size": 20 },
  "/landing.html": { "hash": "...", "size": 20 }
}

Deploy an explicit static folder through the default no-write path:

wrangler deploy ./site \
  --name my-static-site \
  --compatibility-date 2026-07-04

Example folder:

site/
  index.html
  main.css

Wrangler deploys it as static assets without writing wrangler.jsonc.

Deploy a static app directory, such as a Vite app, directly:

wrangler deploy ./app \
  --name vite-site \
  --compatibility-date 2026-07-04 \
  --experimental-auto-config-static-assets

Example app:

{
  "scripts": {
    "build": "vite build"
  },
  "devDependencies": {
    "vite": "7.0.0"
  }
}

Wrangler detects the app, runs the build, verifies dist/index.html, and deploys dist.

Deploy an Express-style Node server directly:

wrangler deploy server.js \
  --name express-app \
  --compatibility-date 2026-07-04

Input:

import express from "express";

const app = express();
const PORT = process.env.PORT ?? 8787;

app.get("/", (_req, res) => res.send("ok"));
app.listen(PORT);

Wrangler generates something equivalent to:

import { httpServerHandler } from "cloudflare:node";

const port = 8787;
process.env.PORT ??= String(port);
await import("../server.js");

export default httpServerHandler({ port });

And creates config like:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "express-app",
  "main": "src/worker.js",
  "compatibility_date": "2026-07-04",
  "compatibility_flags": ["nodejs_compat"],
  "observability": {
    "enabled": true
  }
}

For TypeScript Express-style apps, Wrangler generates src/worker.ts:

import { httpServerHandler } from "cloudflare:node";
import app from "./index.ts";

const port: number = 3000;
process.env.PORT ??= String(port);
app.listen(port);

export default httpServerHandler({ port });

Set up Dockerfile-backed Containers projects non-interactively:

wrangler setup \
  --yes

Example Dockerfile:

FROM node:22
EXPOSE 3000
CMD ["node", "server.js"]

Wrangler generates:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "container-project",
  "main": "src/worker.js",
  "compatibility_date": "2026-07-04",
  "observability": {
    "enabled": true
  },
  "containers": [
    {
      "name": "container-project",
      "class_name": "AppContainer",
      "image": "./Dockerfile",
      "max_instances": 1
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "name": "APP_CONTAINER",
        "class_name": "AppContainer"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["AppContainer"]
    }
  ]
}

Generated Worker:

import { Container, getContainer } from "@cloudflare/containers";

export class AppContainer extends Container {
	defaultPort = 3000;
	sleepAfter = "10m";
	envVars = {
		PORT: "3000",
	};
}

export default {
	async fetch(request, env) {
		const container = getContainer(env.APP_CONTAINER);
		return container.fetch(request);
	},
};

Deploy an explicit Dockerfile target in CI-style flows:

CI=1 wrangler deploy Dockerfile \
  --name container-project \
  --compatibility-date 2026-07-04

Nested targets, such as services/api/Dockerfile, use the Dockerfile's directory as the project root. Bare wrangler deploy can also fall back to a root Dockerfile when no stronger project detection applies.

After a Containers project is configured, use plain deploy:

wrangler deploy

If you accidentally run this:

wrangler deploy Dockerfile

Wrangler now gives targeted guidance instead of treating Dockerfile as a Worker entrypoint.

Use output files for automation with richer deployment data:

WRANGLER_OUTPUT_FILE_PATH=output.json wrangler deploy

Autoconfig entry:

{
  "type": "autoconfig",
  "version": 1,
  "command": "deploy",
  "summary": {
    "adapterId": "single-file-site",
    "projectKind": "single-file-site",
    "deployMode": "no-write",
    "sourceCategory": "html-file"
  }
}

Deploy entry:

{
  "type": "deploy",
  "version": 1,
  "worker_name": "my-site",
  "auto_config_adapter_id": "single-file-site",
  "auto_config_project_kind": "single-file-site",
  "auto_config_deploy_mode": "no-write",
  "auto_config_source_category": "html-file",
  "live_url": "https://my-site.example.workers.dev"
}

Container deploy output now includes app changes:

{
  "type": "deploy",
  "version": 1,
  "containers_rollout": "gradual",
  "containers": [
    {
      "name": "my-container",
      "application_id": "app-id",
      "action": "created",
      "image": "registry.cloudflare.com/account/my-container@sha256:...",
      "image_digest": "sha256:..."
    }
  ]
}

Check container health/details more directly:

wrangler containers list --json

Now includes:

[
  {
    "id": "app-id",
    "name": "my-container",
    "state": "active",
    "instances": 1,
    "health": {
      "instances": {
        "active": 1,
        "failed": 0,
        "starting": 0,
        "scheduling": 0
      }
    },
    "image": "registry.cloudflare.com/...",
    "version": 3,
    "updated_at": "2026-07-04T00:00:00Z",
    "created_at": "2026-07-04T00:00:00Z"
  }
]

Inspect placement status:

wrangler containers instances app-id --json

Now includes:

[
  {
    "id": "instance-id",
    "state": "running",
    "status": {
      "health": "running",
      "container_status": "running"
    },
    "location": "sfo06",
    "version": 3,
    "created": "2026-07-04T00:00:00Z"
  }
]

If instances are stopped or unhealthy, normal table output now nudges you toward:

wrangler containers instances app-id --json

Internal Changes
Autoconfig now has an explicit deploy-intent model:

export type DeployIntent = {
	trigger: "bare" | "explicit-target" | "setup";
	originalTarget?: string;
	targetKind?: "file" | "directory" | "missing";
	currentDeployInterpretation?: "script" | "assets" | "none";
	sourceCategory?: SourceCategory;
	staticAssetsAutoConfig?: boolean;
	allowNonInteractivePersistentSetup?: boolean;
};

Wrangler computes that intent before autoconfig:

const deployIntent = getDeployIntent(args, config);

const shouldRunExplicitTargetAutoConfig =
	args.autoconfig &&
	deployIntent?.trigger === "explicit-target" &&
	(deployIntent.targetKind === "file" ||
		deployIntent.staticAssetsAutoConfig === true ||
		shouldRunDefaultDirectoryAutoConfig(args, config, deployIntent)) &&
	!args.config &&
	!config.configPath;

Autoconfig checks explicit/high-confidence project adapters before framework detection:

const adapterDetails = await detectProjectAdapter({
	projectPath,
	wranglerConfigPath: wranglerConfig?.configPath,
	context,
	deployIntent,
});

if (adapterDetails) {
	return adapterDetails;
}

Root Dockerfile fallback is evaluated later, after stronger framework/static detection has had a chance to match.

Adapters return configuration plans instead of pretending everything is a framework:

export type ConfigurationPlan = {
	mode: "persistent" | "no-write";
	wranglerConfig?: RawConfig | null;
	packageJsonScripts?: Record<string, string>;
	dependencies?: Array<{ name: string; dev?: boolean }>;
	filesToCreate?: Array<{ path: string; contents: string }>;
	commands?: Array<{
		command: string;
		when: "setup" | "build";
		label?: string;
	}>;
	warnings?: string[];
	generatedFiles?: string[];
	deploy?: {
		assets?: string;
		script?: string;
		generatedAssetsDirectory?: "temporary" | "existing" | "build-output";
	};
	summaryFields?: Record<string, string | number | boolean>;
};

Single-file HTML is represented as a no-write plan:

{
	adapterId: "single-file-site",
	adapterName: "Single file site",
	projectKind: "single-file-site",
	confidence: "high",
	sourceCategory: "html-file",
	configurationPlan: {
		mode: "no-write",
		generatedFiles: ["temporary-assets-directory"],
		deploy: {
			generatedAssetsDirectory: "temporary",
		},
	},
	deployTarget: {
		type: "single-html-file",
		sourcePath,
	},
}

Express is represented as a persistent plan:

{
	adapterId: "express-node-http-server",
	adapterName: "Express Node HTTP server",
	projectKind: "node-http-server",
	configurationPlan: {
		mode: "persistent",
		wranglerConfig: {
			name,
			main: "src/worker.js",
			compatibility_date,
			compatibility_flags: ["nodejs_compat"],
			observability: { enabled: true },
		},
		filesToCreate: [
			{
				path: "src/worker.js",
				contents: generatedWrapper,
			},
		],
		packageJsonScripts: {
			deploy: "wrangler deploy",
			preview: "wrangler dev",
		},
	},
}

Dockerfile Containers are represented as a persistent plan:

{
	adapterId: "dockerfile-container",
	adapterName: "Dockerfile Container",
	projectKind: "container-image",
	sourceCategory: "dockerfile",
	configurationPlan: {
		mode: "persistent",
		dependencies: [{ name: "@cloudflare/containers" }],
		wranglerConfig,
		filesToCreate: [
			{
				path: "src/worker.js",
				contents: generatedContainerWorker,
			},
		],
		packageJsonScripts: {
			deploy: "wrangler deploy",
			preview: "wrangler dev",
		},
	},
}

runAutoConfig() now branches into framework config or adapter config:

if (autoConfigDetails.configurationPlan) {
	return await runConfigurationPlan(autoConfigDetails, autoConfigOptions);
}

const framework = autoConfigDetails.framework;
assert(framework, "The framework is unexpectedly missing");

Adapter execution handles generated files safely:

for (const file of plan.filesToCreate ?? []) {
	const filePath = resolve(autoConfigDetails.projectPath, file.path);

	if (existsSync(filePath)) {
		throw new FatalError(
			`Refusing to overwrite generated file ${file.path}. Move or remove it and try again.`
		);
	}

	await ensureDirectoryForFile(filePath);
	await writeFile(filePath, file.contents);
}

No-write deployment preparation mutates deploy args instead of writing config:

case "single-html-file": {
	const assetsDirectory = await mkdtemp(
		path.join(tmpdir(), "wrangler-single-file-site-")
	);

	await copyFile(sourcePath, path.join(assetsDirectory, "index.html"));

	args.assets = assetsDirectory;
	args.script = undefined;
	args.path = undefined;

	metadata.cleanup = async () => {
		await removeDir(assetsDirectory);
	};

	return metadata;
}

Persistent explicit-target autoconfig clears the original target after generating config:

if (args.path === deployIntent.originalTarget) {
	args.path = undefined;
}

if (args.script === deployIntent.originalTarget) {
	args.script = undefined;
}

if (args.assets === deployIntent.originalTarget) {
	args.assets = undefined;
}

if (details.configurationPlan.wranglerConfig) {
	args.config = path.join(details.projectPath, "wrangler.jsonc");
}

Container deploy now returns data instead of only printing:

type ContainerDeployResult = {
	name: string;
	applicationId?: string;
	action: "created" | "modified" | "unchanged";
	image?: string;
	imageDigest?: string;
};

Wrangler maps that into output-file shape:

containers: containerDeployments?.map((container) => ({
	name: container.name,
	application_id: container.applicationId,
	action: container.action,
	image: container.image,
	image_digest: container.imageDigest,
}));

Deploy helpers print readiness guidance based on returned results:

const updatedContainers = containerDeployments?.filter(
	(container) =>
		container.action === "created" || container.action === "modified"
);

const statusCommand =
	updatedContainers.length === 1 && updatedContainers[0].applicationId
		? `wrangler containers instances ${updatedContainers[0].applicationId}`
		: "wrangler containers list";

logger.log(
	`Container applications may take a few minutes to become ready. Check status with \`${statusCommand}\`.`
);

Docker manifest preflight is now quiet unless debug logging is enabled:

function tryFindRemoteManifestDigest(
	pathToDocker: string,
	remoteImageRef: string
): string | undefined {
	try {
		return findManifestDigest(
			runDockerCmdWithOutput(pathToDocker, [
				"manifest",
				"inspect",
				"-v",
				remoteImageRef,
			])
		);
	} catch (error) {
		logger.debug(
			`Remote image manifest ${remoteImageRef} was not found or could not be inspected; image will be pushed.`
		);
		return undefined;
	}
}

Asset manifests now exclude Wrangler-generated output/temp files:

const outputFileRelativePath = getOutputFileRelativePath(dir);

if (relativeFilepath === outputFileRelativePath) {
	logger.debug("Ignoring asset:", relativeFilepath);
	return;
}

And root .wrangler files are ignored by default:

const ignorePatterns = [
	`/${CF_ASSETS_IGNORE_FILENAME}`,
	`/${REDIRECTS_FILENAME}`,
	`/${HEADERS_FILENAME}`,
	`/.wrangler/**`,
];

@changeset-bot

changeset-bot Bot commented Jul 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d4443fc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@cloudflare/autoconfig Minor
wrangler Minor
@cloudflare/workers-shared Patch
@cloudflare/deploy-helpers Minor
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

✅ All changesets look good

@pkg-pr-new

pkg-pr-new Bot commented Jul 4, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@14554

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@14554

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@14554

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@14554

miniflare

npm i https://pkg.pr.new/miniflare@14554

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@14554

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@14554

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@14554

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@14554

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@14554

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@14554

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@14554

wrangler

npm i https://pkg.pr.new/wrangler@14554

commit: d4443fc

@ask-bonk ask-bonk Bot left a comment

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'm Bonk, and I've done a quick review of your PR.

Summary: Expands auto-config to more project shapes (Express, single-file HTML, static apps, Dockerfile containers), adds structured container/auto-config fields to deploy output, and refines container build/deploy diagnostics.

The code changes look solid and well-tested. The one clearly actionable item is unrelated development artifacts committed to the repo:

1. (High) Internal planning docs committed to repo root

Three AI/development planning documents totaling ~2,191 lines are committed at the repository root:

  • auto-config-expansion-plan.md (1,433 lines)
  • auto-config-live-deploy-findings.md (482 lines)
  • auto-config-live-issue-fix-plan.md (276 lines)

These are internal working notes (e.g. "fixes implemented locally; targeted tests ... still pending", dated status lines) and should not ship in the repo. Please remove all three from the PR (or relocate to an untracked scratch location). They aren't referenced by any code or build.

2. (Low, non-blocking) Inconsistent default instance state in instances.ts

In rowsToJsonOutput the no-instance fallback state is "unknown" (line 160), but hasActionableInstanceState uses "inactive" (line 174) for the same case. Neither value triggers the diagnostic hint, so there is no functional bug, but the two fallbacks should match to avoid confusion for future readers.

Everything else — the container deploy result plumbing, tryFindRemoteManifestDigest refactor (behavior-preserving), the no-write temp-dir cleanup in finally, and the .wrangler/** asset ignore — looks correct and is covered by tests.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #14554.

Summary of what I found:

Actionable:

  1. (High) Three internal AI/dev planning markdown files (~2,191 lines total) accidentally committed to the repo root — auto-config-expansion-plan.md, auto-config-live-deploy-findings.md, auto-config-live-issue-fix-plan.md. They contain working-status notes and shouldn't ship.
  2. (Low, non-blocking) Inconsistent no-instance fallback state ("unknown" vs "inactive") between rowsToJsonOutput and hasActionableInstanceState in instances.ts — no functional impact.

Verified as correct:

  • tryFindRemoteManifestDigest refactor preserves the original push-on-error behavior (with updated tests).
  • No-write deploy temp-dir cleanup is properly awaited in a finally block.
  • doAction now returns values on all non-throwing paths; container deploy result plumbing is consistent through deploy-helpers and output.ts.
  • containersRollout correctly reports "none" when the user opts out (the ?? default only applies when undefined).
  • Dockerfile explicit-target path correctly clears args.path so it isn't treated as a script entrypoint.
  • Changesets are present and complete for all modified published packages.

github run

@irvinebroque irvinebroque changed the title [wrangler] Improve auto-config and container deploy feedback [wrangler] Expand auto-config deploy flows to support express.js, index.html, containers Jul 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

2 participants