Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
917 linesCodeBlameRaw
1---
2title: Deployments
3description: A project's production on every push and a live preview of every branch with a pull request, on g1t.page. Scales to zero, and billed to the workspace.
4---
5
6Deployments put your [project](/guides/projects/) on the web. They are off
7for every project until you turn them on. Once they are on, every branch
8with a pull request gets its own live preview, linked on the pull request,
9and the default branch goes to production on every push. Reviewers, and the agents reviewing for you,
10click through the change instead of reading a diff. A [mirror](/guides/mirroring/)
11deploys nothing while the remote leads it: pushes copied in from the remote
12don't build, and its **Deployments** page comes with a takeover.
13
14Apps run on Cloudflare Workers, on `g1t.page`, and a project with a
15`Dockerfile` (or a server g1t can write one for) runs as a container on
16Cloudflare Containers behind one; see [Docker deployments](#docker-deployments):
17
18| | Address |
19| --- | --- |
20| Production | `https://<project>-<workspace>.g1t.page` |
21| Preview of the branch `fix-login` | `https://<project>-git-fix-login-<workspace>.g1t.page` |
22
23A preview's address follows its branch, so it stays the same for every push
24to it. A pull request from a fork, as g1t's agents make them, is named
25`pr-<number>` in place of the branch. A name too long for an address, or
26one another app already has, is shortened or given a short suffix.
27
28A workspace whose name ends like a domain is written without that last
29hyphen: `flagon-io` gives `https://web-flagonio.g1t.page`, not
30`web-flagon-io.g1t.page`. Browsers read `flagon-io` in an address as a
31copy of `flagon.io`, and warn people who visit flagon.io that the page
32looks fake. Apps made before this moved to the new address on their own;
33the old one redirects to it.
34
35When a workspace is [renamed](/guides/workspaces/#rename-a-workspace), or a
36repository is [transferred](/guides/transferring-repositories/#deployments)
37to another workspace or [renamed](/guides/managing-repositories/), its apps
38are built again under addresses with the new name, and the old addresses
39redirect to them for 90 days. A rebuild that fails is tried again an hour
40later, then two hours after that. After three failures with the same
41error, or after one whose commit no longer exists, it is not tried again
42until you push or choose **Redeploy**.
43
44When the [default branch changes](/guides/managing-repositories/), production
45is built again from the new default branch. An
46[archived](/guides/managing-repositories/) repository's apps keep serving.
47
48Every build is also a deployment in the repository's one list of
49deployments, beside those reported from any CI and those g1t Actions
50jobs make, with the source `g1t_page` and the environment `production` or
51`preview`. See [Deployments API](/guides/deployments-api/) to read them,
52report your own, and hear of them by webhook.
53
54An app runs only while it answers a request. One nobody visits runs
55nothing and costs nothing, and the next visit wakes it in milliseconds. A
56container wakes in seconds, and sleeps again after five minutes without a
57request.
58
59Deployments are part of the [g1t plan](/guides/usage-and-billing/#the-g1t-plan),
60and are opt-in per project. A project set to deploy on g1t, with
61deployments off, says **Deployments are off** on its overview, with **Turn on deployments** for
62people with the Admin [role](/guides/access-and-roles/) on its repository. Nothing builds or runs until you turn them on, and one click turns
63them off again.
64
65## Turn on deployments
66
671. **Start the g1t plan for the workspace**, if it is not on. An owner
68 opens **Settings → Billing and plans**, `g1t.sh/<workspace>/-/billing`,
69 and chooses **Start the g1t plan** on the **g1t** card. Back on
70 Billing, the card says **On the g1t plan**. The trial does not pay for
71 deployments.
722. **Turn on deployments for a project.** Someone with the Admin role on
73 its repository opens the project's
74 **Settings → Deployments**, `g1t.sh/<workspace>/<project>/settings/deployments`,
75 or chooses **Deploy** on its overview,
76 and chooses **Turn on deployments**.
77
78Production starts building at once from the default branch. Every pull
79request opened or pushed to from then on gets a preview.
80
81Deployments are for projects g1t runs. Not every project is one: a
82library, a tool or documentation is published rather than deployed, and
83an app may be deployed by its own pipeline somewhere else. Only a project
84that is [an app or site deployed on g1t](/guides/projects/#what-a-project-is)
85is asked to turn deployments on. Any other project's **Deployments** page
86still turns them on; doing so makes it an app deployed on g1t, including
87one that was deployed elsewhere. A project set to be a library, a tool or
88something else cannot have deployments turned on until that setting
89changes.
90
91While payments on g1t are in test mode, no real card is charged: use the
92test card `4242 4242 4242 4242` with any future date and any code.
93
94## What deploys
95
96g1t looks at the project's root directory and builds it the way it is meant to be built.
97You do not configure anything for the common cases.
98
99| The project has | g1t |
100| --- | --- |
101| `wrangler.jsonc`, `wrangler.json` or `wrangler.toml` | Builds it as a **Workers project**: installs dependencies, runs `wrangler deploy --dry-run` to bundle it (which runs the config's own `build` command), and deploys the bundle with the config's static assets, `compatibility_date`, `compatibility_flags` and `vars`. |
102| A framework from the table below | Installs dependencies, builds it with the framework's command, and serves the folder it writes as a **static site**, or deploys the Worker its Cloudflare adapter writes as a **Workers project**. |
103| A framework set up to need a server (Next.js without a static export, Astro's `@astrojs/node`, SvelteKit's `adapter-node`, a server-rendered React Router or Remix app) | Builds it as a **container**: from its `Dockerfile`, or one g1t writes for it. |
104| A `Dockerfile` or `Containerfile`, and no framework above | Builds the image and runs it as a **container**. |
105| A Node.js, Python, Go, Ruby or PHP server, with no `Dockerfile` | Writes a Dockerfile for it, shows it in the build's log, and runs it as a **container**. See [Docker deployments](#docker-deployments). |
106| A `build` script in `package.json` | Installs dependencies, runs `npm run build`, and serves the output as a **static site**. |
107| An `index.html` and nothing to build | Serves it as it is. |
108| Nothing g1t recognises | Fails, saying to add a `Dockerfile`. |
109
110**Settings → Deployments** shows what g1t detected at the project's last
111finished build and what it was built with, such as **Static site ·
112Astro**, **Workers project**, **Container · Dockerfile** or **Container ·
113Flask, with a Dockerfile g1t wrote**, and what running it costs. Before
114the first build it says **Detected at the first build**. The framework is
115named at the top of the build's log, as `Detected Astro: framework=astro.`
116**Build as**, under the same settings, overrides what is detected.
117
118Dependencies are installed by the lockfile that is there: `npm ci`,
119`pnpm install --frozen-lockfile`, `yarn install` or `bun install`, and
120`npm install` with no lockfile.
121
122### Frameworks
123
124g1t knows a framework by the packages in `package.json`
125(`dependencies` and `devDependencies`) and by its config file. The build
126command is the package's own `build` script when it has one, else the
127command in the table. A **Build command** or **Output directory** of
128your own, under **Settings → Deployments**, always wins over the table.
129
130| Framework | Found by | Builds with | Serves |
131| --- | --- | --- | --- |
132| Vite, and React, Vue, Svelte, Solid or Preact on Vite | `vite` | `vite build` | `dist`, or `build.outDir` from `vite.config` |
133| Astro | `astro` | `astro build` | `dist`, or `outDir` |
134| Astro with `@astrojs/cloudflare` | `astro`, `@astrojs/cloudflare` | `astro build` | A Workers project: the Worker and config the adapter writes |
135| Next.js with `output: 'export'` | `next`, and `output: 'export'` in `next.config` | `next build` | `out` |
136| Next.js with OpenNext | `next`, `@opennextjs/cloudflare` and a `wrangler.jsonc` | `opennextjs-cloudflare build`, then `wrangler deploy --dry-run` | A Workers project |
137| SvelteKit with `@sveltejs/adapter-static` | `@sveltejs/kit` and the adapter | `vite build` | `build`, or the adapter's `pages` |
138| SvelteKit with `@sveltejs/adapter-cloudflare` | `@sveltejs/kit` and the adapter | `vite build` | A Workers project: `.svelte-kit/cloudflare/_worker.js` and the files beside it |
139| Nuxt | `nuxt` | `npm run generate` if there is one, else `nuxi generate` | `.output/public` |
140| React Router in SPA mode | `@react-router/dev`, and `ssr: false` in `react-router.config` | `react-router build` | `build/client` |
141| Remix in SPA mode | `@remix-run/dev`, and `ssr: false` in `vite.config` | `remix vite:build` | `build/client` |
142| Gatsby | `gatsby` | `gatsby build` | `public` |
143| Docusaurus | `@docusaurus/core` | `docusaurus build` | `build` |
144| Eleventy | `@11ty/eleventy` | `eleventy` | `_site`, or `dir.output` |
145| Angular | `@angular/core` and `angular.json` | `ng build` | `dist/<project>/browser`, from `angular.json` |
146| Create React App | `react-scripts` | `react-scripts build` | `build` |
147| Hugo | `hugo.toml`, `hugo.yaml` or `hugo.json`; or a Hugo `config.toml` and no `package.json` | `hugo --minify` | `public`, or `publishDir` |
148| Jekyll | `_config.yml` and a `Gemfile` | `bundle install`, then `bundle exec jekyll build` | `_site`, or `destination` |
149| mdBook | `book.toml` | `mdbook build` | `book`, or `build.build-dir` |
150
151- **Frameworks that need a server run as containers.** A Next.js app
152 without `output: 'export'`, an Astro site with `@astrojs/node`, a
153 SvelteKit app with `adapter-node`, and a React Router or Remix app that
154 renders on the server are built from their `Dockerfile`, or one g1t
155 writes ([Docker deployments](#docker-deployments)). A static export or
156 the framework's Cloudflare adapter costs less, since it runs nothing
157 while no one visits; the build's log says so. One g1t cannot run as a
158 server, such as SvelteKit with `adapter-auto`, fails before anything is
159 installed, saying what to change or to add a `Dockerfile`. An **Output
160 directory** of your own, or **Build as** Static site, builds it as a
161 static site instead.
162- **A framework's Worker.** Astro's and SvelteKit's Cloudflare adapters,
163 OpenNext, and `@cloudflare/vite-plugin` with a Wrangler config build a
164 Worker. g1t runs the build, then bundles what it wrote with
165 `wrangler deploy --dry-run`, reading the config the build wrote
166 (`.wrangler/deploy/config.json`) when there is one. A `_worker.js`,
167 `_routes.json` or `.assetsignore` at the top of the static files, and
168 the names `.assetsignore` lists, are never served.
169- **Hugo and mdBook** use the `hugo` or `mdbook` on the machine's `PATH`.
170 Without one, the build fetches Hugo 0.166.0 (the extended edition, with
171 npm) or builds mdBook 0.5.4 with cargo, which takes a minute or two.
172 Jekyll's gems are installed into `vendor/bundle` in the project.
173
174A project that is none of these is served from the first of these that
175exists after the build: `dist`, `build`, `out`, `public`, `_site`,
176`.output/public`. Set **Output directory** to choose another.
177
178### Static sites
179
180- A site without a `404.html` is treated as a single-page app: an address
181 that matches no file serves `index.html`. With a `404.html`, that page
182 is served instead, with status 404.
183- `_headers` and `_redirects` files in the output are honored, in
184 [Cloudflare's format](https://developers.cloudflare.com/workers/static-assets/headers/).
185- Up to 20,000 files, and 25 MiB per file: Cloudflare's limits.
186
187### Workers projects
188
189- Your Worker's `fetch` handler runs as written, and its static assets
190 are served under the binding name your config gives them. Cron triggers
191 (`triggers.crons`) are not scheduled, so a `scheduled` handler never
192 runs; the deployment says so in its warnings.
193- Each request your Worker answers may use up to 50 ms of CPU time
194 (waiting on the network does not count) and make up to 50 requests of
195 its own (`fetch` calls and the like). A request that goes over either
196 is stopped and answered with a 503 page that says so. Static assets are
197 served without running your Worker, and count toward neither.
198- `vars` are deployed as plain-text bindings (or JSON, for objects). Rows
199 of the project's [secrets and variables](/guides/secrets-and-variables/)
200 available to Deployments are bound too, and replace a `var` of the same
201 name: secrets as secret bindings.
202- **Not provisioned yet:** D1, KV, R2, Durable Objects, Queues, service
203 bindings, Vectorize, Hyperdrive, Workers AI and Workflows. A project that
204 declares any of them still deploys, without them, and its deployment
205 lists each one it left out. Code that needs them should check that the
206 binding is there.
207
208### Docker deployments
209
210Any app that runs in a container deploys as one, whatever its framework or
211age: give it a `Dockerfile`, or let g1t write one. It runs on
212[Cloudflare Containers](https://developers.cloudflare.com/containers/)
213behind the app's Worker, at the same addresses, custom domains and
214previews as any other app.
215
216:::caution[Not switched on yet]
217Docker deployments are built but off on g1t.page until Cloudflare's
218container APIs are checked against g1t's account. Until then a container
219build fails with "Docker deployments are not switched on for g1t.page
220yet", and a project that is a container only for having a `Dockerfile`,
221beside a site g1t could serve, is built as that site, as before.
222:::
223
224What a project is built as is decided in this order:
225
2261. **Build as**, under **Settings → Deployments**: **Static site**,
227 **Workers** or **Docker** is what it is built as, whatever else it has.
2282. **Auto** (the default) finds it from its files:
229 1. A Wrangler config is a Workers project.
230 2. A framework that builds a static site or a Worker is that, even
231 beside a `Dockerfile`: it costs nothing while no one visits.
232 3. A framework set up to need a server is a container, from its
233 `Dockerfile`, or one g1t writes for it.
234 4. A project with no framework g1t knows is a container when it has a
235 `Dockerfile` (or `Containerfile`, or the path set as its
236 **Dockerfile**), or is a server g1t recognises.
237 5. Otherwise a static site or plain HTML, as above; with nothing to
238 serve, the build fails, saying to add a `Dockerfile`.
239
240g1t writes a Dockerfile for these, when there is none:
241
242| The project has | Runs with |
243| --- | --- |
244| `package.json` with a `start` script, or a server package (`express`, `fastify`, `koa`, `@nestjs/core`, `@hono/node-server` and others) and a main file | `npm start`, or `node <main>`, on `node:22-slim` (or the major release in `.nvmrc` or `.node-version`), after `npm run build` when there is one |
245| Next.js, Astro with `@astrojs/node`, SvelteKit with `adapter-node`, React Router or Remix | `next start`, `node ./dist/server/entry.mjs`, `node build`, `react-router-serve` or `remix-serve`, after the build |
246| `requirements.txt`, `pyproject.toml` or `Pipfile`, and `manage.py` (Django) | `gunicorn <project>.wsgi` on `python:3.12-slim` |
247| The same, with Flask and an `app.py`, `main.py`, `server.py`, `wsgi.py` or `asgi.py` | `gunicorn <module>:app` |
248| The same, with FastAPI or Starlette | `uvicorn <module>:app` |
249| The same, with neither | `python <module>.py` |
250| `go.mod` and one `package main`, at the top or in one folder of `cmd/` | The program, built with `golang:1.25` into an image with nothing else |
251| A `Gemfile` with Rails | `rails server`, after `assets:precompile`, on `ruby:3.3-slim` |
252| A `Gemfile` and a `config.ru` | `rackup` |
253| `composer.json`, and `artisan` with `public/index.php` (Laravel), or an `index.php` | Apache with PHP 8.3, after `composer install` |
254
255The Dockerfile g1t writes is printed whole in the build's log. To change
256how the app is built or run, commit it as `Dockerfile` and edit it. It
257leaves `.git`, `node_modules`, `.venv` and `__pycache__` out of the image.
258
259What a container gets, and what it must do:
260
261- **Listen on `0.0.0.0` at `PORT`.** `PORT` is the port its Dockerfile's
262 first `EXPOSE` names (in its last stage), else `8080`. The first request
263 waits up to 90 seconds for it to answer there; one that never does gets
264 a 503 page saying so.
265- **The project's [secrets and variables](#secrets-and-variables)** are
266 its environment variables, set by g1t when it starts, never baked into
267 the image. The image's build gets none of them: a step that needs one
268 belongs in the app's start.
269- **One instance, which sleeps.** It starts on the first request, and
270 stops after five minutes without one; asleep, it costs nothing, and the
271 next request starts it again in a few seconds. Its disk starts from the
272 image each time: keep data in a database or a bucket.
273- **Its size** is **Container size** under **Settings → Deployments**:
274 `lite` (the default) to `standard-4`, as [What it costs](#what-it-costs)
275 lists. The image may be as large as the size's disk, 2 GB for `lite`;
276 a larger image fails the build, saying how large it was.
277- **Built for `linux/amd64`**, with Docker, in the build's sandbox (or the
278 workflow job's Docker, for [`g1t-runner deploy`](#deploying-from-a-workflow)),
279 with network access as any build has. **Build command** is not used:
280 the Dockerfile builds it.
281
282What Deployments cannot run yet is on
283[What g1t can't do yet](/about/limitations/#deployments).
284
285## Previews of branches
286
287A preview is built when a pull request is opened, when it is marked ready,
288and on every push to it, including an agent's. Pull requests from forks,
289which is how g1t's agents work, are built from the fork.
290
291The pull request shows the deployment as a check named **g1t / deploy**:
292
293| State | Means |
294| --- | --- |
295| Pending, "Building" | The build is running. The link opens its log. |
296| Passed, "Preview is live" | The link opens the preview. |
297| Failed, "Deployment failed" | The link opens the build log and the reason. |
298
299A newer push replaces a build that is still running for the same pull
300request. Previews are marked `noindex`, so search engines leave them alone.
301
302The **Deployments** page lists the same failure of one app, repeated, as a
303single row with how many times it happened and when it last did. The row
304opens the newest of them.
305
306## Production
307
308Each push to the default branch, which is each merge on a protected
309branch, builds and replaces production. The **Deployments** page shows the
310live address, the commit it runs and when it went up.
311
312### The production screenshot
313
314When production goes live, g1t takes a screenshot of its home page, as a
3151280 by 800 browser window sees it, and shows it on the project's
316[overview](/guides/projects/#the-overview). Choosing it opens the site.
317
318- One screenshot is taken per production deploy. g1t waits up to 12
319 seconds for the page to settle, then takes it as it is.
320- The screenshot is of the app's `g1t.page` address. A custom domain
321 serves the same app, so it looks the same.
322- Until it is ready, or if it could not be taken, the overview shows the
323 project's uploaded image, if it has one, or else a plain frame with the
324 address. A failed screenshot is tried again when the overview is next
325 opened, at most every five minutes.
326- Only people with a role on the project's repository see it, like the
327 rest of its deployments; anyone else who can see the project sees its
328 uploaded image or the frame.
329- With **Use the uploaded image instead of a screenshot** on, the
330 uploaded image shows instead.
331- Screenshots are not charged.
332
333An app deployed elsewhere gets a screenshot of its production address
334too, and any project can show an uploaded image instead. See
335[production's picture](/guides/projects/#productions-picture).
336
337## Deploying from a workflow
338
339A [workflow](/guides/actions/) job can build and deploy the project
340itself, with g1t's own action, `g1t/deploy`. Use it when the build needs
341steps of yours first (generated code, a test that must pass, a tool the
342default build lacks), or to deploy from a branch without a pull request.
343The app, its address, its checks and its custom domains are the same as
344for the builds g1t starts by itself.
345
346```yaml
347# .g1t/workflows/deploy.yml
348name: Deploy
349
350on:
351 push:
352 branches: [main]
353 pull_request:
354
355permissions:
356 contents: read
357 deployments: write
358
359jobs:
360 deploy:
361 runs-on: ubuntu-latest
362 steps:
363 - uses: actions/checkout@v5
364 - uses: actions/setup-node@v4
365 with:
366 node-version: 24
367 - run: npm ci
368 - run: npm test
369 - id: deploy
370 uses: g1t/deploy@v1
371 with:
372 build-command: npm run build
373 - run: echo "Live at ${{ steps.deploy.outputs.url }}"
374```
375
3761. Turn Deployments on for the project, as in
377 [Turn on deployments](#turn-on-deployments).
3782. Add a workflow like the one above. The job needs `deployments: write`
379 (or `pages: write`) in its `permissions:`.
3803. Push. The run on `main` deploys production; a pull request's run
381 deploys its preview.
382
383What the run builds:
384
385| The run | Builds |
386| --- | --- |
387| On the project's production branch (a push, `workflow_dispatch` or `schedule` there) | Production |
388| For a pull request (`pull_request`, `pull_request_target`) | That pull request's preview, at its branch's address, or `pr-<number>` for one from a fork |
389| On another branch | That branch's preview |
390| For a tag | Nothing: the step fails and says to run the workflow on a branch |
391
392`g1t/deploy` takes these inputs:
393
394| Input | Default | |
395| --- | --- | --- |
396| `project` | The repository's one project with Deployments on | The project's slug. Needed when the repository has several. |
397| `root-dir` | The project's **Root directory** | Where the project is in the checkout. |
398| `build-command` | The project's **Build command**, else what the build finds | What builds it, run after the install. |
399| `output-dir` | The project's **Output directory**, else the first folder found | The folder a static build writes. |
400| `sha` | The run's commit | The commit the `g1t / deploy` check goes on. |
401| `token` | `${{ github.token }}` | The job's own token. No other token can open a build. |
402
403Its outputs are `url` (the app's address), `deployment-id` (`dpl_…`) and
404`kind` (`production` or `preview`).
405
406The action calls
407[`POST /repos/{owner}/{repo}/deployments/builds`](/reference/api/deployments/start-deployment-build/),
408which checks the plan and the spend limit as any build does, sets the
409commit's `g1t / deploy` check to pending, and answers with a token for
410that build alone and the project's
411[variables and secrets](#secrets-and-variables) for production or
412previews. The action masks the token and every secret in the log, then
413runs `g1t-runner deploy` in the checkout, which installs, builds and
414uploads exactly as [What deploys](#what-deploys) says; its output is in
415the step's log and on the deployment's page. When the build fails, or
416g1t does not take what it built, the step fails and the deployment says
417why. When the job ends before the build has finished, because it was
418cancelled, failed in an earlier step or timed out, the deployment fails
419as the job ends, saying how it ended, rather than staying **Building**.
420
421- **Secrets.** A pull request from a fork or from someone without the
422 Write role, and a run that had to be
423 [approved](/guides/actions/#pull-requests), build without
424 the project's secrets, as previews of them always have. Their token is
425 read-only, so they cannot open a build at all unless the workflow runs
426 on `pull_request_target`.
427- **Cost.** The build runs in the job, so its time is the job's
428 [Actions time](/guides/usage-and-billing/), free on a
429 [self-hosted runner](/guides/self-hosted-runners/), and is not charged
430 again as build seconds. Requests, CPU time and custom domains are
431 charged as for any app.
432- **g1t's own builds still run.** A push to the production branch and a
433 pull request's push still start g1t's build too. Whichever starts
434 later replaces the other, so the app ends up at the newest commit
435 either way.
436- **On your own machines**, the action uses the `g1t-runner` on `PATH`, or
437 downloads the newest release and checks it against its `SHA256SUMS`. It
438 runs on Linux and macOS, with `curl` and `jq` or `python3`.
439
440`g1t/detect` says what a build of the checkout would find, without
441building anything: its outputs are `kind` (`workers`, `static`, `html` or
442`none`), `install`, `build`, `output-dir` and `framework` (such as
443`astro` or `nextjs`, empty when there is none). A framework set up to
444need a server is `none`, and the step's log says what to change. Use
445them to cache, or to fail early:
446
447```yaml
448- id: detect
449 uses: g1t/detect@v1
450- if: steps.detect.outputs.kind == 'none'
451 run: |
452 echo "::error::Nothing here would be served: add an index.html or a build script."
453 exit 1
454```
455
456### Build with a workflow
457
458Turn on **Build with a workflow** under **Settings → Deployments** and the
459project's builds run as a workflow g1t manages for it, instead of as
460builds Deployments starts by itself. Nothing else changes: the same
461previews and production, at the same addresses, with the same
462**g1t / deploy** check, which is now the check of the workflow's job.
463
464- The workflow is listed on the repository's **Actions** page, named
465 `g1t`, with its runs, logs and re-runs like any other. It is not a file
466 in the repository: it is made from the project's settings, and follows
467 them when they change.
468- It calls g1t's reusable deploy workflow,
469 `g1t/workflows/.g1t/workflows/deploy.yml@v1`, which checks out the
470 commit and runs [`g1t/deploy`](#deploying-from-a-workflow). As g1t
471 improves how projects are built, that workflow improves for every
472 project that uses it.
473- A pull request from a fork runs it with a read-only token, as on
474 GitHub, and still gets its preview, without the project's build
475 secrets.
476- To change it, commit a workflow named `g1t` to `.g1t/workflows/`: that
477 file runs in its place. Start from the one the **Actions** page shows,
478 or call the reusable workflow yourself:
479
480```yaml
481# .g1t/workflows/g1t.yml
482name: g1t
483
484on:
485 push:
486 branches: [main]
487 pull_request:
488
489permissions:
490 contents: read
491 deployments: write
492
493jobs:
494 g1t:
495 name: g1t
496 uses: g1t/workflows/.g1t/workflows/deploy.yml@v1
497 with:
498 project: web
499 secrets: inherit
500```
501
502Its jobs are [workflow jobs](/guides/actions/#machine-sizes): on g1t's
503machines they are charged as Actions time, at cost plus 20% like every
504build, and on a [self-hosted runner](/guides/self-hosted-runners/) they
505cost nothing.
506
507## Custom domains
508
509Production can be served at a domain of your own, such as `example.com` or
510`www.example.com`, as well as at its address on g1t.page. A domain belongs
511to the project, not to a build: every redeploy is served on it with nothing
512to change.
513
514### Add a domain
515
5161. Open the project's **Settings → Domains**,
517 `g1t.sh/<workspace>/<repo>/settings/domains`.
5182. Enter the domain and choose **Add domain**. Leave **Also add the www
519 (or apex) twin** checked to add both `example.com` and
520 `www.example.com` at once: the one you typed serves the app, and the
521 other redirects to it with a `308`, path and query kept.
5223. Add the DNS records the page lists, at whoever manages the domain's
523 DNS. Each name and value has a copy button.
5244. Wait. The page follows the domain from **Waiting for DNS** to
525 **Issuing certificate** to **Active** by itself; **Check now** asks
526 again at once.
527
528Custom domains need the workspace's g1t plan, and are $0.12 a month each
529(their cost plus 20%), as many as you like. Adding or removing one needs
530the Admin [role](/guides/access-and-roles/) on the project's repository.
531
532### A subdomain, such as www
533
534Add one record:
535
536| Type | Name | Target |
537| --- | --- | --- |
538| `CNAME` | `www` | `domains.g1t.page` |
539
540The same goes for any subdomain, such as `app.example.com` (name `app`).
541
542### The apex, such as example.com
543
544DNS does not allow a plain `CNAME` at the apex of a domain, so use your
545provider's flattened form of one, pointed at `domains.g1t.page`:
546
547| DNS provider | Record |
548| --- | --- |
549| Cloudflare DNS | `CNAME` at `@` (Cloudflare flattens it) |
550| Amazon Route 53 | Not supported by alias records to other zones; use the www pairing below |
551| DNSimple, NS1, Namecheap, Porkbun, Gandi | `ALIAS` at `@` |
552| DNS Made Easy, Constellix | `ANAME` at `@` |
553
554If your provider has no flattened `CNAME`, `ALIAS` or `ANAME`, add
555`www.example.com` on g1t instead, and set up your provider's forwarding (or
556any redirect) from `example.com` to `www.example.com`.
557
558### Verification and certificates
559
560Pointing the domain at `domains.g1t.page` is what proves it is yours: once
561the record is seen, the certificate authority checks the domain over HTTP
562and issues a certificate, renewed by itself before it expires. Visitors are
563always served over HTTPS, TLS 1.2 or newer.
564
565The page may also list a `TXT` record named `_cf-custom-hostname.<domain>`.
566It proves ownership before traffic moves, which is useful when the domain
567is serving a site elsewhere today: add the `TXT` first, wait for
568**Issuing certificate** or **Active**, then change the `CNAME`. Any other
569`TXT` records listed are for the certificate and are needed as shown.
570
571DNS changes can take from a few minutes to an hour to be seen. A domain
572that stays at **Waiting for DNS** usually has a record with a typo, a
573leftover `A` or `AAAA` record beside the new one, or (on Cloudflare DNS) a
574`CAA` record that does not allow the certificate's authority.
575
576### Remove a domain
577
578Choose **Remove** beside it. It stops serving the project at once, and any
579domain redirecting to it goes too. Your DNS records are left as they are.
580
581## When apps come down
582
583Nothing keeps running unasked. An app comes down, and stops costing
584anything, when:
585
586| | |
587| --- | --- |
588| Its pull request is merged or closed | That preview, at once. |
589| No one visits a preview for the project's **idle days** | That preview, at the next sweep (every 10 minutes). The default is 7 days. |
590| You choose **Take down** on the Deployments page | That app, at once. |
591| You turn off previews or production | All of that kind, at once. |
592| You choose **Turn off deployments** | Every app of the project, at once, and no more builds. |
593| The workspace's plan ends or its payment fails | Every app of the workspace, at the next sweep. |
594| The repository is [deleted](/guides/managing-repositories/) | Every app of its projects, at once. Custom domains stay set up and serve nothing. If you restore the repository, production is built again and its domains serve it; previews come back with their next push. When it is purged, its custom domains are removed. |
595
596A preview that came down comes back with the next push to its pull
597request, or **Redeploy** on the Deployments page.
598
599## Who can do what
600
601What you can do with a project's deployments follows your
602[role](/guides/access-and-roles/) on its repository:
603
604| | Needs |
605| --- | --- |
606| See its deployments, their build logs, previews and domains | Read |
607| **Redeploy**, take an app down | Write |
608| Turn deployments on or off, change their settings | Admin |
609| Add, verify and remove custom domains | Admin |
610
611A public repository's deployments, and their build logs, can be seen by
612anyone, signed in or not, the same as its code. Its settings and domains
613still need Admin. A private repository's deployments are seen only by
614people with a role on it.
615
616## Settings
617
618Under the project's **Settings → Deployments**,
619`g1t.sh/<workspace>/<repo>/settings/deployments`:
620
621| Setting | Default | |
622| --- | --- | --- |
623| Production | On | Deploy the default branch on every push. |
624| Previews | On | A preview for every branch with an open pull request. |
625| Build as | Auto | **Auto** (found from its files), **Static site**, **Workers** or **Docker**; see [Docker deployments](#docker-deployments). |
626| Dockerfile | `Dockerfile` or `Containerfile` | For a container: its Dockerfile's path, inside the project. |
627| Container size | `lite` | For a container: the instance it runs at, with its price an hour awake. |
628| Build command | The project's own | Runs instead of `npm run build`, or before bundling a Workers project. Not used for a container. |
629| Output directory | Found by itself | What a static site serves. |
630| Idle days | 7 | 1 to 90. A preview no one visits this long comes down. |
631| Build with a workflow | Off | Build through the deploy workflow g1t manages for the project; see [build with a workflow](#build-with-a-workflow). |
632
633## Logs, traces and errors
634
635Every request an app answers shows under the project's
636[Observability](/guides/observability/) within seconds: its status,
637duration, CPU time and the lines it wrote, with its exceptions grouped into
638errors and its numbers by deployment. Nothing to set up. Each request
639reaches the app with an `x-g1t-request-id` header, its id there, and a
640`traceparent`, and the app is deployed with OpenTelemetry's
641`OTEL_EXPORTER_OTLP_*` variables set, so an OpenTelemetry SDK in it sends
642its spans to the project without configuration.
643
644## Secrets and variables
645
646Builds and running apps read the project's
647[secrets and variables](/guides/secrets-and-variables/) that are
648available to Deployments, and the workspace's that reach it:
649
650| | Reads |
651| --- | --- |
652| A production build, and production | Each key's Production row, else its row for all environments. |
653| A preview build, and the preview | Each key's Preview row, else its row for all environments. |
654
655The build gets them as environment variables, with secrets hidden in its
656log. The running app gets them as bindings, `env.KEY`, put in place by g1t
657rather than the build; a container gets them as its environment variables
658when it starts, and its image's build gets none. Secrets reach a preview only when the pull
659request's author has Write or higher on the repository, whether a member
660or an [outside collaborator](/guides/access-and-roles/#outside-collaborators),
661or it is g1t working on its own. For a pull request g1t made, whoever
662asked for it is the one whose role counts. A preview of anyone else's pull request, such as
663one from a fork or by someone with Read or Triage, is built and runs with
664variables only, no secrets.
665
666For example, a `STRIPE_KEY` secret with a Production row holding the live
667key and a Preview row holding the test key gives every preview the test
668key.
669
670## What it costs
671
672Deployments come with the
673[g1t plan](/guides/usage-and-billing/#the-g1t-plan), $20 a month per
674workspace with $10 of usage included. There are **no quotas**: as many
675projects and previews as you like, and no count of builds, requests or
676domains ever stops or pauses an app. What deployments cost g1t is metered
677from the first unit, at Cloudflare's price plus 20%, and paid from the
678plan's $10 of included usage first; past it, it is charged up to your
679[spend limit](/guides/usage-and-billing/#limits).
680
681| | Costs g1t | You pay |
682| --- | --- | --- |
683| Projects, previews and the apps behind them | Next to nothing | Not charged |
684| Builds g1t runs, by the second, whether they succeed or fail (a [workflow's build](#deploying-from-a-workflow) is its job's Actions time instead) | About $0.001 a minute | About $0.0012 a minute |
685| Requests, to all of the workspace's apps | $0.30 a million | $0.36 a million |
686| CPU time your code spends computing (waiting on the network is not counted) | $0.02 a million ms | $0.024 a million ms |
687| Custom domains, with their certificates (a www/apex pair is two) | $0.10 a month | $0.12 a month, by the most the workspace had at once that month |
688| A container, while it is awake, by the minute (see the sizes below) | From $0.0129 an hour (`lite`) | From $0.0155 an hour |
689| Data a container sends | $0.05 a GB, at most | $0.06 a GB |
690
691A container's price is its size's memory, disk and vCPU while it is awake,
692with the Durable Object in front of it, at Cloudflare's
693[Containers prices](https://developers.cloudflare.com/containers/platform/pricing/)
694plus 20%. Cloudflare bills CPU only as it is used; g1t charges every vCPU
695as busy while the container is awake, so a container that mostly waits
696costs g1t less than it is charged. Data sent is charged at the most a GB
697costs anywhere ($0.05; $0.025 in North America and Europe), since g1t
698cannot tell where it went. Each minute it is awake is charged from its
699start, so a container woken by one request, and stopped five minutes after
700it, is charged five minutes.
701
702| Size | vCPU | Memory | Disk, and the largest image | An hour awake | Awake all month (730 hours) |
703| --- | --- | --- | --- | --- | --- |
704| `lite` | 1/16 | 256 MiB | 2 GB | $0.0155 | $11.28 |
705| `basic` | 1/4 | 1 GiB | 4 GB | $0.0404 | $29.46 |
706| `standard-1` | 1/2 | 4 GiB | 8 GB | $0.0956 | $69.77 |
707| `standard-2` | 1 | 6 GiB | 12 GB | $0.1616 | $117.95 |
708| `standard-3` | 2 | 8 GiB | 16 GB | $0.2708 | $197.68 |
709| `standard-4` | 4 | 12 GiB | 20 GB | $0.4880 | $356.24 |
710
711**Settings → Deployments** shows these with the price book's prices of the
712day. A container that is visited all day never sleeps, and costs the last
713column; one visited for an hour a day costs about a thirtieth of it.
714
715Builds are charged when they finish. Requests, CPU time, custom domains
716and container time are counted through the month, so your limit and
717**Billing** follow them as they happen, and charged once, on the first
718sweep after the month ends, as one line: *Deployments in 2026-10:
7191,240,000 requests, 3,100,000 CPU ms, 2 custom domains, 41.5 container
720hours and 2.3 GB sent*.
721
722A static site, or plain HTML, runs no code when it is visited: its files
723are served as they are, so serving it adds no requests or CPU time. Its
724builds are metered. A Workers project's code runs on every request, so its
725requests and CPU time are metered too. A container's Worker is metered the
726same way, and its container by the minute while it is awake.
727
728An app that no one visits costs nothing. Previews still come down when
729their pull request closes and after their idle days, to keep the list of
730what is up short.
731
732The only things that stop a workspace's apps are its own spend limit
733(apps pause with a notice, and come back once there is room; a paused
734container is stopped and starts no more) and the plan ending (they come
735down).
736
737Deployments used to be a $5 plan of their own. A Deployments subscription
738bought before keeps working until its current period ends, and is not
739renewed.
740
741### Seeing what you use
742
743- **Usage** shows **Deployments** with *Builds*
744 (build minutes and their cost), *Requests & CPU* and *Custom domains*,
745 in dollars, beside the rest of the workspace's usage. Requests and CPU
746 time are counted from Cloudflare's analytics every 10 minutes.
747- The **statement** on Billing lists every build (*Building acme/web to
748 production (48 s), paid by your plan's included usage*) and every
749 month's requests, CPU time and custom domains.
750- Each build's page shows how long it ran.
751
752## Turn it off
753
754- **For a project:** **Turn off deployments** under its **Settings →
755 Deployments**. Every app comes down at once. Turning it on again
756 rebuilds production.
757- **For the workspace:** when an owner ends the g1t plan on Billing,
758 deployments keep working until the date shown; then every app comes
759 down and nothing more is charged.
760
761Usage from the month that is under way is still charged once it ends.
762
763## How it works
764
7651. A pull request opens, or someone pushes. The deployments service hears
766 of it, checks that the workspace's plan is on, and reserves what the
767 build may cost (30 minutes of sandbox time) with billing. If billing
768 refuses (no paid plan, the spend limit reached, compute paused), the
769 build does not start: the deployment's status is **Skipped** with the
770 reason and what to do, and **Redeploy** shows the same reason.
7712. It starts a build in a sandbox of its own, the same machines that run
772 [GitHub Actions](/guides/actions/) and agents. The sandbox checks out
773 the commit with a read-only token that expires in 30 minutes. Its
774 network is restricted to the project's allowed domains, the package
775 registries, GitHub and Cloudflare's API, as
776 [workflow jobs'](/guides/actions/#what-a-job-can-reach) are, and it
777 stops at 30 minutes.
778 When it stops, what it cost is settled against what was reserved.
7793. The sandbox builds, then lists its files. g1t opens an upload with
780 Cloudflare for exactly those files and hands the sandbox a key that can
781 upload them and nothing else. Files Cloudflare already has are skipped.
7824. The sandbox sends the Worker's code to g1t, which puts the app in
783 g1t's [Workers for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/)
784 namespace, under the name in its address. A container build goes as
785 [below](#containers).
7865. A request to `*.g1t.page` reaches g1t's dispatcher, which runs the app
787 by the name in the hostname. Nothing else is looked up. A request to a
788 custom domain reaches the same dispatcher through Cloudflare for SaaS;
789 it looks the hostname up once, in a key-value store kept at the edge.
790
791Your code never holds a Cloudflare credential, and apps are served from
792`g1t.page`, not `g1t.sh`, so they share no cookies or origin with the site
793you sign in to.
794
795### Containers
796
7971. The build finds it is a container, and asks g1t where to push its
798 image (the build's `registry` step). g1t mints a login to Cloudflare's
799 registry, `registry.cloudflare.com`, that can **push only** and lasts
800 **15 minutes**, and names the app's own repository,
801 `<account>/g1t-<app>`, with the build's id as its tag. Asked before
802 anything is built, so a build that could not push is not run.
8032. The sandbox starts a Docker Engine of its own (as a workflow job's),
804 builds the image for `linux/amd64`, checks its size against the
805 container size's disk, pushes it with that login, and reports the
806 image by its digest.
8073. g1t runs only an image by digest in that app's repository, and asks
808 the registry, with a pull login of its own, that the digest is there.
809 It puts the app's Worker in the namespace: g1t's own code, never the
810 project's, with a Durable Object class that holds the container, the
811 project's secrets and variables, and a token for the app to report its
812 running time with. Then it creates the app's Container application
813 (at most one instance, at the project's size, attached to that class),
814 or rolls it out to the new image.
8154. A request reaches the app's Worker through the dispatcher, as any
816 app's does. The Worker hands it to the app's Durable Object, which
817 starts the container if it is asleep, with the environment, waits for
818 it to answer on its port, and passes the request to it. Every minute it
819 is awake, the object charges the next minute ahead and reports it to g1t
820 (`POST /deployments/apps/<app>/meter` on the API, with its token); five
821 minutes after the last request it stops the container.
8225. A preview's container comes down with its pull request, its
823 application and Worker deleted together.
824
825Why it is built this way, from Cloudflare's documentation and Wrangler's
826own code (October 2026):
827
828- **Workers for Platforms runs containers.** A user Worker in a dispatch
829 namespace may bind Containers, as a Durable Object
830 ([WfP bindings](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/bindings/)).
831 Wrangler's `deploy --dispatch-namespace` deploys containers by finding
832 the Durable Object namespace of the class in that dispatch namespace and
833 creating a Container application attached to it
834 (`POST /accounts/{id}/containers/applications`, with
835 `durable_objects.namespace_id`), then rolling out new images
836 (`…/applications/{id}/rollouts`). So each app is a Worker of its own,
837 with a class of its own, isolated as every app is, and the dispatcher
838 routes to it unchanged. A single host Worker of g1t's could not run every
839 app's image: a Durable Object starts only the images its Worker's
840 configuration names.
841- **The default scheduling policy.** It takes one image and size for the
842 whole application, set through the API, which suits one app per
843 application; the `durable_object` policy is in beta, and needs the
844 image map uploaded with the Worker.
845- **Registry logins.** Cloudflare mints registry credentials with
846 `POST /accounts/{id}/containers/registries/registry.cloudflare.com/credentials`
847 and `{ "permissions": ["push"], "expiration_minutes": 15 }`, as Wrangler
848 does for its own pushes. They cannot be held to one repository, so g1t
849 holds the build to its app's: it names the repository, runs nothing but a
850 digest in it, and never gives a build a login that can pull, so no build
851 can read another app's image. g1t's own Cloudflare token never reaches
852 the sandbox.
853- **Sizes and limits.** Containers' instance types are `lite` (1/16 vCPU,
854 256 MiB, 2 GB) through `standard-4` (4 vCPU, 12 GiB, 20 GB); an image
855 may be as large as its instance's disk, and an account holds 50 GB of
856 images ([limits](https://developers.cloudflare.com/containers/platform/limits/)).
857- **Scale to zero.** Containers are billed every 10 ms while running, and
858 stop being billed once asleep. The Durable Object stops its container
859 itself after five minutes without a request, rather than leaving it to
860 the inactivity timeout, since the object's own minute alarm, which
861 meters it, would otherwise keep it awake.
862
863## Troubleshooting
864
865| You see | Do |
866| --- | --- |
867| "Deployments come with the g1t plan … and `<workspace>` does not have it" | An owner starts the g1t plan under Billing. |
868| A deployment **Skipped**, saying it reached its spend limit or that new compute is paused | The build was refused before it started. Follow the link in the message: raise the spend limit, prepay, or answer the spike, on Billing. |
869| "Stopped: unusual CPU use" | The build looked like it was mining. Contact support if it was a real build; see [abuse and mining](/guides/guardrails/#abuse-and-mining). |
870| "403" from a host in the build log | The build's network is restricted. Add the host to the project's allowed domains under **Settings → Guardrails**. |
871| "found nothing to serve" | Add a `Dockerfile`, a `build` script, an `index.html`, or a Workers config; or set **Output directory**. |
872| "Docker deployments are not switched on for g1t.page yet" | The project is a container, which g1t.page does not run yet. Set **Build as** to Static site or Workers if it can be built as one. See [Docker deployments](#docker-deployments). |
873| "**Build as** is Docker, but this project has no Dockerfile" | Add a `Dockerfile`, or set **Dockerfile** to its path. |
874| "The image is 2.4 GB, more than the 2.0 GB an app of its size (lite) can hold" | Make the image smaller (a slim base image, a multi-stage build), or choose a larger **Container size**. |
875| A container app says "The app did not start" | It did not answer on its port within 90 seconds. Make it listen on `0.0.0.0` at `PORT`, or `EXPOSE` the port it uses in its Dockerfile. Its output is in the build's log only while it builds; run the image locally with `docker run -e PORT=8080 -p 8080:8080` to see it start. |
876| "the output directory `x` does not exist after the build" | The build wrote elsewhere: check its log, then fix **Output directory**. |
877| "`d1_databases` is not provisioned on g1t.page yet" | The app deployed without that binding. See [Workers projects](#workers-projects). |
878| "`triggers.crons` (1 schedule) is not set up on g1t.page yet" | The app deployed, but its `scheduled` handler never runs. See [Workers projects](#workers-projects). |
879| A preview page says "This preview is not up" | It came down (see [When apps come down](#when-apps-come-down)). Push, or choose **Redeploy**. |
880| "Custom domains are being switched on" | Custom domains are not on for g1t.page yet. Domains you add are kept, and set up by themselves once they are. |
881| A domain stays at **Waiting for DNS** | Check the record against the one listed, remove other `A`/`AAAA` records for the same name, then choose **Check now**. |
882| A domain says "This domain is not set up" | It points at g1t, but no project has added it. Add it under **Settings → Domains**. |
883| "This pull request's commit no longer exists" | The pull request's head was force-pushed over, or its branch deleted, after the build was asked for. Push to the pull request again, or close it. g1t does not try that commit again. |
884| g1t/deploy says "The job's token needs `deployments: write`" | Add `deployments: write` to the workflow's or the job's `permissions:`. A pull request's run from outside gets a read-only token and cannot deploy. |
885| g1t/deploy says "Name the project to build" | The repository has several projects with Deployments on: give `project:`. |
886| g1t/deploy says "g1t.page deploys branches and pull requests" | The run is for a tag. Run the workflow on a branch, such as on a push to `main`. |
887| "The workflow job was cancelled before the build finished" | The job that opened the build (`g1t/deploy`) was cancelled, or ended another way the message names, before `g1t-runner deploy` reported. Run the job again. |
888| "The build did not finish in 45 minutes" | The build stopped reporting: a build stops at 30 minutes, and one not heard from after 45 is failed. Make the build faster, or build less for previews with **Build command**. |
889
890## Running your own g1t
891
892Deployments need a Workers for Platforms namespace and a zone for apps:
893
8941. Create the namespace: `npx wrangler dispatch-namespace create g1t-deployments`.
8952. Add a proxied wildcard DNS record (`*`, `AAAA`, `100::`) on the apps'
896 zone, and set the zone in `services/pages/wrangler.jsonc`.
8973. Create an API token with **Workers Scripts: Edit** and **Account
898 Analytics: Read**, and store it:
899 `npx wrangler secret put CLOUDFLARE_API_TOKEN` in `services/deployments`.
9004. Deploy `services/deployments`, `services/pages`, and the runner.
901
902Docker deployments also need the token to have **Containers: Edit**, a
903Workers Paid plan with Containers, and `CONTAINERS` set to `on` in
904`services/deployments/wrangler.jsonc`; see
905[Docker deployments](/guides/deploy-to-cloudflare/#docker-deployments)
906for what to check first.
907
908Custom domains also need Cloudflare for SaaS on the apps' zone. Turn it on
909under the zone's **SSL/TLS → Custom Hostnames**, give the token **SSL and
910Certificates: Edit** on the zone, and run `scripts/setup-custom-domains.sh`:
911it creates the `g1t-domains` KV namespace, the fallback origin's DNS record
912(`domains`, `AAAA`, `100::`, proxied) and sets it as the fallback origin.
913The dispatcher's `*/*` route on the zone, in `services/pages/wrangler.jsonc`,
914is what brings custom domains' traffic to it.
915
916Without a card processor configured, every feature is on and nothing is
917charged.