A deployment is a site or a Worker whenever it can be, and a container only as the fallback. With Build as on Auto, a Wrangler config or a framework's Cloudflare adapter is a Workers project; a framework's site, a build script's or an index.html is that site even beside a Dockerfile, and the log says "Built as a Vite site; Dockerfile is not used (a container is only the fallback; choose **Build as**: Container to use it)", where a Dockerfile that ran a server used to win; a Node.js server with no Dockerfile (Express, Koa, Hono, Polka, Restify, hapi, node:http) runs as a Worker; any other server g1t recognises, a Node.js server with its own Dockerfile, a framework set up to need a server, or a Dockerfile with nothing else to serve is a container.
A Node.js server runs as a Worker through httpServerHandler from cloudflare:node with nodejs_compat (developers.cloudflare.com/workers/runtime-apis/nodejs/http: from the compatibility date 2025-09-01 nodejs_compat turns on the HTTP server modules, and g1t's date is 2026-09-26). deploy/node_worker.rs finds the file that starts it (what its start script runs with node, tsx or ts-node, else main, else server.js and the like), and the build installs and builds it, writes a prelude that keeps every server the app creates or listens on and lets listen take any port and address, and an entry that imports the app and answers with httpServerHandler, or with the app's default export's fetch when it has one (a Hono app, whose Response @hono/node-server replaces is turned back into the runtime's). wrangler deploy --dry-run bundles it, and wrangler dev starts it once with the project's variables and asks it for /: a module that cannot load, a top-level await that never settles, or a server that never listens fails there, not in production. A folder served with app.use(express.static("public")) is uploaded as the Worker's static files. What cannot run as a Worker is found before anything is built, and the app is a container from the Dockerfile g1t writes, the log saying "Could not run as a Worker: <reason>; deploying as a container.": a native addon (better-sqlite3, bcrypt, sharp and others), Fastify, NestJS, Sails, remix-serve or react-router-serve, a WebSocket server (socket.io, express-ws, WebSocketServer, an upgrade handler), child_process, cluster or worker_threads, files read or written while it runs, files served from disk under a path, or a start script g1t cannot read. A Worker that does not bundle or start falls back the same way. Build as Workers runs a Node.js server as one whatever is found, and Build as Container (shown as Container now, still `docker` as stored) runs its container. g1t-runner detect and g1t/detect print entry for such a Worker.
- Checked:
- cargo test -p g1t-runner (the precedence table: Vite with a Dockerfile is a site, Express without one a Worker, Express with better-sqlite3 a container, a Go server with its Dockerfile a container, a Wrangler config with a Dockerfile Workers, index.html with an nginx Dockerfile a site), and on Linux the ignored end-to-end tests in tests/deploy_cli.rs, which install Express, bundle and start its Worker and deploy it to a fake API with public/ as its assets, and fall back to a container when the bundle fails or a top-level await never settles. Express, Hono on @hono/node-server and Fastify were run by hand with wrangler dev: Express and Hono answered, and Fastify, which boots on timers, did not, so it is a container. The deployments service's tests and typecheck, the web typecheck, and the docs build pass. The Deployments guide has the rule, Node.js servers as Workers, and how to force a container; the architecture page, the Build as hint and g1t/detect's outputs follow it.