Build executor (Milestone 2): Containers + Queue + R2
On this page
Status 2026-10-22: code complete and tested locally; not provisioned (see Blocked).
Flow#
POST /api/projects/import(orPOST /api/projects/:id/builds) →startContainerBuild: daily cap check, commit pinned from the branch,build_jobsrow (executor = cf-container),deploymentsrow (target = devstack-sites), message onBUILD_QUEUE.- Queue consumer (
custom-worker.tsqueue()) →runContainerJob:claimWithCap(max 2 running, conditional UPDATE) → containerbuild-<jobId>POST /build→ NDJSON phases (installing→building→uploading) and log chunks →createLogStream(line-buffered, redacted) →build_events+logs/<jobId>.txtin R2. GET /artifact(tar of the output dir) →artifacts/<jobId>.tar→ files →SITESbucketsites/<deploymentId>/…→ hostname aliaseshosts/<label>.json.- Production builds also move
<slug>.devstack.bid(promote). Previews get<slug>-<shortid>.devstack.bid. - Cancel:
requestCancel→ heartbeat watchdog (10 s) → container/cancel(SIGTERM, then SIGKILL). 15-minute hard limit →timed_out. Cron sweeper unchanged.
Caps#
MAX_CONCURRENT_BUILDS = 2, BUILD_TIMEOUT_MS = 15 min, DAILY_BUILDS_PER_USER = 10 (src/lib/build/caps.ts), container max_instances: 2, queue max_concurrency: 2.
Security#
- Container image (
container/Dockerfile): pinned digests, non-rootnodeuser, tini, no secrets/ARGs. - GitHub token: only in the
/buildrequest body → githttp.extraheaderviaGIT_CONFIG_*env (never argv, never the build env, never logged). Public repos build without a token. The user OAuth token is used (no GitHub App yet), so it is not short-lived or repo-scoped: see Next. - Commands run without a shell (argv split, metacharacters rejected). Dangerous env keys (
PATH,NODE_OPTIONS,LD_PRELOAD, …) are dropped. - Egress allowlist on the container: GitHub + npm/yarn/bun registries + nodejs.org.
- Logs:
sanitizeLogeverywhere (verbatim, URL-encoded and base64 forms, longest first, GitHub token patterns); warning when a secret is shorter than 6 chars. - User sites are served by a separate Worker (
sites-worker/) with no D1 and no platform secrets; static files only;nosniff; no cookies. A hostname belongs to the first project that claims it. - Recommendation: move user sites to a dedicated registrable domain (for example
devstack.app) so user JavaScript is not same-site withdevstack.bid.
Blocked (needs the owner / infrastructure)#
- Container image: built and verified locally (see Verified). Pushing it happens only as part of
npm run deploy:release, which needs a GO. - Queue / R2 buckets not created yet:
wrangler queues create devstack-build-jobs,wrangler r2 bucket create devstack-build-artifacts,wrangler r2 bucket create devstack-sites. - Wildcard DNS + route (the API token has no Zone DNS permission):
- DNS:
AAAA * 100:: proxiedin zonedevstack.bid(apex,wwwand all other records unchanged). - Worker route:
*.devstack.bid/*→ Workerdevstack-sites(deploy withwrangler deploy -c sites-worker/wrangler.jsonc). The zone already has a route*.devstack.bidwith no Worker; it must be replaced by (or narrowed to) the route above. - Then set
SITES_DNS_READY=1.
- DNS:
Deploy configs (hotfix vs full release)#
wrangler.jsonc(base, hotfix): no Containers, Durable Object, Queue or build R2.npm run deploy:hotfix(same asnpm run deploy) never needs Docker. The executor reports the missing bindings (missingInfra) and builds stay queued/blocked.release/build-bindings.jsonc: the build-executor bindings only.npm run cf:release-configmerges them over the base intowrangler.release.jsonc(generated, committed;tests/release-config.test.tsfails if it is stale).npm run deploy:release= regenerate +opennextjs-cloudflare deploy -c wrangler.release.jsonc. Needs Docker plus the Queue and buckets above.- After the first release deploy (DO migration
v1-build-containerapplied), preferdeploy:releasefor later deploys. A hotfix deploy keepsBuildContainerexported, so it does not delete the class, but it removes the bindings until the next release deploy.
Verified#
- Docker 26.1.5 on the box (dockerd started by hand, no systemd).
docker build --network host -t devstack-builder:local container/(the box sandbox only gives apt network on the host network). The real container (runs asnode, uid 1000) builtsveltejs/template@5b3da65throughPOST /build→{"t":"result","ok":true}, andGET /artifactreturned a tar withindex.html,global.css,favicon.pngandbuild/bundle.{js,css}. - Dry runs:
wrangler deploy --dry-runpasses with no Docker (base);wrangler deploy -c wrangler.release.jsonc --dry-runbuilds the image and lists BUILD_CONTAINER, BUILD_QUEUE, BUILD_ARTIFACTS and SITES. - Unit tests:
tests/build-executor.test.ts,tests/hosting.test.ts,tests/security-review.test.ts. - Real end-to-end without Docker:
E2E_BUILD=1 npx vitest run tests/build-executor.test.tsrunscontainer/build-server.mjslocally, clonessveltejs/template@5b3da65,npm install,npm run build, and the executor publishespublic/and serves it by hostname.
Source: docs/platform/BUILD-EXECUTOR.md · /docs/platform/build-executor.md