Deploy
Create an app and ship it — push over Git, deploy from CI, and what each push triggers.
Create the app
Open the control UI and go to Apps → New app. Pick what the app starts from — Blank, Import from GitHub, or a Template — then enter a name and slug. Creating the app provisions its git remote; deploys follow each push. A git push to a slug that does not exist yet creates the app too, with you as admin. The three modes, the import limits and push-to-create are on Create an app.
Slug rules: 1–48 chars, lowercase letters/digits/hyphens, starting and ending with a letter or digit. Reserved: _control, app, api, git.
Push with git
Every app has a Git remote at https://git.<domain>/<slug>. Authenticate as user git with an account API key (Account → API keys) as the password; the push needs the push role on the app. Each push to main deploys.
The smallest app is a Wrangler config plus a module Worker:
{
"name": "hello",
"main": "index.js",
"compatibility_date": "2026-09-01",
}
export default {
fetch: () => new Response("Hello from Noite"),
};
git init -b main
git add -A && git commit -m "first deploy"
git remote add origin "https://git:<api-key>@git.<domain>/<slug>"
git push -u origin main
Open the result at https://<slug>.<domain>. With a package.json, the runner installs and builds it first with the project’s package manager (npm, pnpm, Yarn or Bun), so Vite and Rsbuild apps need no CI (Build); without one it deploys the tree as is. The Wrangler config is optional: a static site (a build that writes dist/index.html) or a package.json main that exports fetch deploys without one (No config at all). Pushes must fast-forward unless you hold the admin role.
Your first app, in order
- Create the app and an API key (above).
- Describe the Worker with a
wrangler.jsoncor, if you use thecfCLI, acloudflare.config.ts(which one). Or neither, for a static site or afetchmodule (no config at all). - Add a build if you have one: a
buildscript orbuild.command(Build). - Set env vars in the app’s Settings if the build or release command needs them. They do not reach the running Worker; use
varsin the config for that (Configure). - Push to
main, watch the Deployments tab, openhttps://<slug>.<domain>. - Check what does not run: Node HTTP servers (Express, Fastify,
app.listen()) are refused at deploy (Frameworks and Node servers). Quotas, rate limits and memory bounds are on Limits.
Release command
A release string in the Wrangler config runs once per deploy, after the build and before celld deploy: the place for database migrations.
{
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2026-09-01",
"release": "bun run db:migrate",
}
It runs with sh -c in the project with your app’s env vars, and is killed after 10 minutes. The command is capped at 500 characters. A non-zero exit fails the deploy and the previous deployment keeps serving. release is removed from the config before celld deploy.
Database migrations (Void apps)
Void apps keep their schema in db/migrations/*.sql, and every deploy applies what is pending — no release command needed. On a redeploy the runner calls the migration endpoints against the staged generation before traffic switches to it, with the fleet’s own token — checking the Worker’s migration list against the pushed tree — and only then activates. The log reports migrate: applied N, skipped M with the file names; a redeploy skips what is already applied, and a failed migration fails the deploy with migration <name> failed: <error>, leaving the old version serving.
Migrations are forward-only: rolling back the code never un-applies them, so keep migrations backward-compatible with the previous release (add tables/columns first, remove them in a later deploy). Each preview environment migrates its own database — preview data never touches production.
Like release (where a failure keeps the old deployment serving), a failed staged migrate keeps the old Worker serving: traffic only switches at activation, after the migrations succeed. On a first deploy — or when the fleet has no live node — the step runs after activation instead, as before; there a failed migrate leaves the new Worker serving a half-migrated schema while the deploy reads Failed. Fix the migration and redeploy — applied migrations are skipped. The step is bounded: worker responses past 4 MiB fail the deploy, warning and name lists log the first 20 plus a count, and per-request timeouts (300 s executions; probes and reconciles budget 60 s each on the staged path for the cold pool build) cap the worst case at about 15 minutes (18 for older runtimes on the legacy endpoint).
Postgres/MySQL apps are refused at this step: their migrations need a Hyperdrive binding Noite does not provide, and deploying them would serve an empty database. Move the app to D1 (database.dialect: "sqlite" in void.json).
Deploy from CI
@noitenow/cli pushes a prebuilt dist/ plus wrangler.jsonc as a synthetic fast-forward commit — no server build, push role suffices, never force-pushes.
bunx @noitenow/cli deploy --slug <slug> --url https://git.<domain> --token "$NOITE_API_KEY"
- run: bun run build
- run: bunx @noitenow/cli deploy --comment update
env:
NOITE_API_KEY: ${{ secrets.NOITE_API_KEY }}
NOITE_BASE: https://git.<domain>
| Flag | Env | Default |
|---|---|---|
--dist |
— | dist |
--slug |
NOITE_SLUG |
repo name, slugified |
--url |
GIT_PUBLIC_BASE / NOITE_BASE |
http://git.noite.localhost:9080 |
--token |
NOITE_API_KEY / NOITE_GIT_TOKEN |
— |
--wrangler |
— | wrangler.jsonc |
--message |
— | deploy <sha12> |
--comment |
— | update (create / off) |
In Actions it writes url= + sha= to $GITHUB_OUTPUT and posts (or updates, via marker) a PR comment. A Worker may be declared in cloudflare.config.ts instead of wrangler.json(c) — the runner converts it after the build when no Wrangler config exists.
What happens on push
- Tip
.bundlelands; the runner updates its bare mirror and worktree. - The sandboxed install and build (
build.commandor thebuildscript) run when the tree declares them, with the project’s package manager. - The runner picks the config to deploy: the build’s output when it produced one, else the source config, else one it generates (Which config deploys).
- Optional
releasecommand (e.g. migrations) runs. A failed release keeps the old deployment serving. - The config is stripped of runner-only keys (
release,build), Settings merge into Workervars(Void apps split their proxy-only credentials out first, then get their runner-owned identity vars, services andISR_CACHE), then the fleet deploys in order: each script of the generation is staged first withcelld deploy --no-activate— each cohosted script first (Void apps: the platform’s Void proxy, plus the sandbox controller for apps with a managed sandbox — each skipped when unchanged), the app last — then onecelld activateserves the generation with a single atomic pointer write, each command retried once on infra errors. A failed stage runs no activate, so the previous generation keeps serving. A new fleet spawns or the existing one getsPOST /reload, whose outcome the runner checks — a refused adoption fails the deploy with celld’s error instead of leaving the old generation serving under a Success row. - Void apps with D1 migrations get a
migratestep: on a redeploy the runner drives the staged generation’s migration endpoints throughcelld invokebefore the activate, everywhere else the new Worker’s endpoints after adoption, before the deploy is marked success (Database migrations). A failed stagedmigrateruns no activate, so the previous generation keeps serving.
The Deployments tab shows status per push with a build-log pane: one ▸ step: command line per step and its output tail, and on failure the step that failed. Every finished row says how long the run took (took 1m 42s), and a failed, stopped or timed-out row shows its one-line reason in the row itself (build failed: …), so the app header does not repeat it. Deploy logs keep a 64 KB tail.
A deploy ends as success, failed, Stopped (cancelled: the operator pressed Stop, and ERROR: cancelled by the operator is appended) or Timed out (stalled: the runner was interrupted or a step outlived its bound, logged as ERROR: deploy stalled (host interrupted or timed out)). All four are final — a late log line or a second Stop never rewrites a closed row — and only a successful deploy is a rollback target. The runner’s API and its type bindings spell the new two cancelled and stalled; older rows keep failed.
Rollback
A failing deploy is not retried forever: after two consecutive failed deploys of the same target (app or environment) at the same SHA, the runner pauses its automatic deploys of that SHA — the push fast path and the fallback tip sweep both stop — and the Deployments tab says so. The state is derived from the deploy history, so it survives a restart. A new push, or an explicit Deploy, Redeploy or Roll back (UI, CLI or API) resumes it — that deploy is also how a settings or env-var change that affects the build lands. A deploy you Stop is not a failure and never counts toward the two, but it also leaves the SHA alone until one of those actions happens.
POST /v1/apps/<id>/rollback {sha} redeploys a past successful SHA — bundles are immutable per SHA. The UI offers rollback on success rows.
Every deploy row also records why it exists: intent is deploy for a push, a web commit or the runner’s tip sweep, rollback for a rollback, redeploy for an environment deploy (the Redeploy action), and branch for a one-off branch deploy (with the branch ref in actor). It is settled when the row is created and never rewritten, so a rollback that then fails is still a rollback; the API serves it on every deploy row, and the Deployments tab labels those rows Rollback to <sha>, Redeploy and Deploy <branch>@<sha>.
Deploy another branch
The app header’s Deploy menu has a branch picker. With the production branch selected (the default) it is exactly Redeploy: it re-runs the pipeline at the commit production serves. With any other branch selected, Deploy builds that branch’s current tip and serves it in production once — a way to ship a hotfix branch without merging it first. The production branch setting is unchanged, so its next push deploys as usual (and the history says Deploy <branch>@<sha> for the one-off run). The runner’s tip sweep leaves the one-off release alone until that push, even if the production branch’s current tip failed to build, and rolling back to the one-off sha reads the branch’s own bundle. It needs the push role, like Redeploy — except on an app with branch protection on (a required pull request or approvals), where only an admin can deploy a branch: everyone else must open a pull request.
POST /v1/apps/<id>/deploy?role=push {ref} runs the same action (role may also go in the body; use admin while branch protection is on); the ref resolves the branch tip, and the run reads that branch’s own tip bundle. It answers 202 with the queued deploy’s id and sha.