Push to deploy
Turn it on for one branch of a runlot-hosted repository, and every push to that branch builds on our side and deploys to production.
A repository does not deploy by itself. Turn push to deploy on for one
branch, and every push to that branch runs a build on the control plane and
deploys the result to the project — the same thing runlot deploy does from
your machine, started by git push.
runlot repo hook set app --branch main
# Every push to main in my-org/my-project/app will build and deploy to production,
# under your account. Continue? [y/N] y
# app: push-to-deploy is on.
# branch main
# directory the repository root
git push origin mainHow a push becomes a deploy
- You push to the hooked branch. Pushes to any other branch are ordinary pushes and start nothing.
- The git server tells the control plane which commit the branch now points at. A build is queued. If a build for the same branch was still waiting in the queue, it is cancelled and replaced — three pushes in a minute produce one deploy, of the last commit.
- The build runs: fetch the commit's tree, install dependencies, run the
buildscript if there is one, then bundle and upload exactly asrunlot deploywould. - On success the new version is live and the build links to it. On failure
nothing changes — the previous deployment goes on serving — and the
organization's admins get a
build_failednotification.
Builds are serialized per organization: one at a time, in the order the pushes arrived. Two builds of the same project racing each other would decide which code serves by whichever finished first, and nobody chose that.
Turning it on
Setting the hook needs admin. Turning it on converts every future push, by
anyone with write access, into a production deploy, and it does so under the
account of whoever turned it on — that is a standing grant, which is why a
deploy-scoped token cannot create one.
CLI
runlot repo hook set app --branch main # turn on, or change the branch
runlot repo hook set app --branch main --root-dir apps/web
runlot repo hook status app # is it on, and for which branch
runlot repo hook disable app # off; the branch and directory are kept
runlot repo hook unset app # off, and the setting is removed
runlot repo deploy app # build the hooked branch's tip now, without pushingrunlot repo deploy is refused while the hook is off: a build has no
directory to run in and no account to deploy as until someone has turned it on.
Dashboard
Open the repository and use the Builds tab. The card at the top states the
current setting in one sentence — off, or "a push to main deploys" — with a
button that changes it. Turning it on asks for the branch and, for a monorepo,
the root directory. Turning it off asks you to confirm. The build history and
the Deploy now button sit under it.
The branch rule
One branch, by name. Not a pattern, not a list. A pattern makes "why did my push not deploy" a question about glob semantics, and the answer to that question is never in the log.
The default offered is the repository's default branch. Changing the branch of a hook that is already on is not a new grant, so it does not move the account the deploys run under; only turning it on does.
The root directory
For a monorepo, name the subdirectory the build runs in — the one that holds
the package.json and runlot.json of the app being deployed:
runlot repo hook set app --branch main --root-dir apps/webIt is a relative path inside the repository with no leading slash and no ...
Empty is the repository root. The directory has to exist in the commit being
built; a commit that removed it fails with clone_failed.
What the build environment contains
The build runs on the control plane, in a subprocess with its own scratch directory that is removed when the build ends. What it sees:
| Node | The major version the control plane is installed with — currently Node 22. |
| Package manager | pnpm when the tree has pnpm-lock.yaml, through corepack, so the version is your packageManager field. Otherwise npm. Which one is decided by the lockfile in the commit, not by a setting. |
| Install | npm ci when package-lock.json or npm-shrinkwrap.json is present, pnpm install --frozen-lockfile with a pnpm lockfile. With no lockfile at all it is npm install, and the log says so — that build is not reproducible. Commit a lockfile. |
| Build script | npm run build or pnpm run build, only when package.json has a build script. A worker with no build step goes straight to the bundle. |
| Environment | Built from nothing. The build gets PATH, TZ, LANG/LC_ALL, the proxy variables if the operator set any, and these fixed values: CI=true, NO_COLOR=1, NODE_ENV=production, RUNLOT_BUILD=1. HOME, the npm and pnpm caches and TMPDIR all point inside the per-build directory. |
| Your secrets | Not present. A build is not a deploy, and runlot secret set values are runtime bindings; the build has no reason to see them. If your build script reads a secret from the environment, it will not be there — check RUNLOT_BUILD to tell the two apart. |
| Network | Open. The build fetches from registries and can reach anything else. |
| Memory and CPU | Bounded at 2 GiB of memory, two CPU cores and 512 processes. |
| Wall clock | 20 minutes, everything included — fetch, install, build, upload. Past it the whole process tree is killed and the build fails with timeout. |
| Deploy | npx runlot deploy --no-migrate, with a deploy token minted for this build and revoked when it ends. |
Migrations are not applied. A migration changes your data, and a git push should not perform that without anyone having asked. Run
runlot pg migrate yourself, before or after the push, as your release order
requires.
The build is your code, and it runs on our machine
The sandbox bounds what a build can use, not what it can do. It runs as the control plane's own service account and can reach the network. Do not put a build script in a repository you would not run on your own laptop.
Build minutes
Build time is metered per organization and per calendar month, in wall-clock minutes of a build slot — a build that waits on a slow registry costs the same minutes as one that pegs a core.
| Plan | Build minutes per month |
|---|---|
| Free | 300 |
| Pro | 3,000 |
The check runs when a build reaches the front of the queue. A build that finds
the month's minutes used up fails with minutes_exhausted and nothing is
deployed. The counter resets on the first of the month, UTC. The Builds tab and
runlot builds both show how much of the month is used.
The log
Every build keeps its output — the package manager, your build script and the deploy step, interleaved with a marker line per stage. Read it in the Builds tab by opening the row, or with the CLI:
runlot builds # newest first, with the month's minutes under it
runlot builds show <buildId>
runlot builds log <buildId> # the output, as it was written
runlot builds cancel <buildId> # only while queuedThe log is capped at 1 MiB, keeping the tail — the end of a failed build is where the error is. When the cap bites, the first line of the log says how many bytes were dropped. Logs are kept for as long as the project exists; there is no separate retention clock for builds.
When a build fails
The build's row carries a code, and the sentence explaining it is at the end of the log:
| Code | What happened |
|---|---|
clone_failed | The source could not be fetched, or the root directory is not in this commit |
no_build_script | package.json could not be read |
install_failed | The package manager exited non-zero |
build_failed | Your build script exited non-zero |
deploy_failed | The bundle could not be produced or uploaded |
timeout | The 20-minute wall clock ran out |
minutes_exhausted | The organization's build minutes for the month are used up |
toolchain_missing | The build environment could not provide the toolchain — that is on our side, not your repository |
superseded | A newer push to the same branch replaced this build while it was queued |
cancelled | Somebody cancelled it |
Two things hold on every failure. The previous deployment keeps serving: a
build that did not produce a deployment touched nothing. And the
organization's admins get a build_failed notification — in the
dashboard, and by email for accounts with an address on file — at most once an
hour per repository, so a push loop does not mail you eleven times but the
second failure of a morning, the one that says your fix did not work, is not
silent.
A build that is already running cannot be cancelled. It ends on its own inside the twenty minutes.
What it is not
- Not a CI system. There are no test steps, no matrices, no artifacts other
than the deployment. A build is
install → build → deploy, and the only thing it produces is a version. - Not preview deployments. A push to any branch other than the hooked one does
nothing. Preview addresses are
runlot deploy --previewfrom your machine. - Not a GitHub integration. The repository is one runlot hosts; see Repositories.