| 1 | --- |
| 2 | title: Deployments |
| 3 | description: 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 | |
| 6 | Deployments put your [project](/guides/projects/) on the web. They are off |
| 7 | for every project until you turn them on. Once they are on, every branch |
| 8 | with a pull request gets its own live preview, linked on the pull request, |
| 9 | and the default branch goes to production on every push. Reviewers, and the agents reviewing for you, |
| 10 | click through the change instead of reading a diff. A [mirror](/guides/mirroring/) |
| 11 | deploys nothing while the remote leads it: pushes copied in from the remote |
| 12 | don't build, and its **Deployments** page comes with a takeover. |
| 13 | |
| 14 | Apps 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 |
| 16 | Cloudflare 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 | |
| 23 | A preview's address follows its branch, so it stays the same for every push |
| 24 | to 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 |
| 26 | one another app already has, is shortened or given a short suffix. |
| 27 | |
| 28 | A workspace whose name ends like a domain is written without that last |
| 29 | hyphen: `flagon-io` gives `https://web-flagonio.g1t.page`, not |
| 30 | `web-flagon-io.g1t.page`. Browsers read `flagon-io` in an address as a |
| 31 | copy of `flagon.io`, and warn people who visit flagon.io that the page |
| 32 | looks fake. Apps made before this moved to the new address on their own; |
| 33 | the old one redirects to it. |
| 34 | |
| 35 | When a workspace is [renamed](/guides/workspaces/#rename-a-workspace), or a |
| 36 | repository is [transferred](/guides/transferring-repositories/#deployments) |
| 37 | to another workspace or [renamed](/guides/managing-repositories/), its apps |
| 38 | are built again under addresses with the new name, and the old addresses |
| 39 | redirect to them for 90 days. A rebuild that fails is tried again an hour |
| 40 | later, then two hours after that. After three failures with the same |
| 41 | error, or after one whose commit no longer exists, it is not tried again |
| 42 | until you push or choose **Redeploy**. |
| 43 | |
| 44 | When the [default branch changes](/guides/managing-repositories/), production |
| 45 | is built again from the new default branch. An |
| 46 | [archived](/guides/managing-repositories/) repository's apps keep serving. |
| 47 | |
| 48 | Every build is also a deployment in the repository's one list of |
| 49 | deployments, beside those reported from any CI and those g1t Actions |
| 50 | jobs make, with the source `g1t_page` and the environment `production` or |
| 51 | `preview`. See [Deployments API](/guides/deployments-api/) to read them, |
| 52 | report your own, and hear of them by webhook. |
| 53 | |
| 54 | An app runs only while it answers a request. One nobody visits runs |
| 55 | nothing and costs nothing, and the next visit wakes it in milliseconds. A |
| 56 | container wakes in seconds, and sleeps again after five minutes without a |
| 57 | request. |
| 58 | |
| 59 | Deployments are part of the [g1t plan](/guides/usage-and-billing/#the-g1t-plan), |
| 60 | and are opt-in per project. A project set to deploy on g1t, with |
| 61 | deployments off, says **Deployments are off** on its overview, with **Turn on deployments** for |
| 62 | people with the Admin [role](/guides/access-and-roles/) on its repository. Nothing builds or runs until you turn them on, and one click turns |
| 63 | them off again. |
| 64 | |
| 65 | ## Turn on deployments |
| 66 | |
| 67 | 1. **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. |
| 72 | 2. **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 | |
| 78 | Production starts building at once from the default branch. Every pull |
| 79 | request opened or pushed to from then on gets a preview. |
| 80 | |
| 81 | Deployments are for projects g1t runs. Not every project is one: a |
| 82 | library, a tool or documentation is published rather than deployed, and |
| 83 | an app may be deployed by its own pipeline somewhere else. Only a project |
| 84 | that is [an app or site deployed on g1t](/guides/projects/#what-a-project-is) |
| 85 | is asked to turn deployments on. Any other project's **Deployments** page |
| 86 | still turns them on; doing so makes it an app deployed on g1t, including |
| 87 | one that was deployed elsewhere. A project set to be a library, a tool or |
| 88 | something else cannot have deployments turned on until that setting |
| 89 | changes. |
| 90 | |
| 91 | While payments on g1t are in test mode, no real card is charged: use the |
| 92 | test card `4242 4242 4242 4242` with any future date and any code. |
| 93 | |
| 94 | ## What deploys |
| 95 | |
| 96 | g1t looks at the project's root directory and builds it the way it is meant to be built. |
| 97 | You 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 |
| 111 | finished build and what it was built with, such as **Static site · |
| 112 | Astro**, **Workers project**, **Container · Dockerfile** or **Container · |
| 113 | Flask, with a Dockerfile g1t wrote**, and what running it costs. Before |
| 114 | the first build it says **Detected at the first build**. The framework is |
| 115 | named 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 | |
| 118 | Dependencies 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 | |
| 124 | g1t knows a framework by the packages in `package.json` |
| 125 | (`dependencies` and `devDependencies`) and by its config file. The build |
| 126 | command is the package's own `build` script when it has one, else the |
| 127 | command in the table. A **Build command** or **Output directory** of |
| 128 | your 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 | |
| 174 | A project that is none of these is served from the first of these that |
| 175 | exists 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 | |
| 210 | Any app that runs in a container deploys as one, whatever its framework or |
| 211 | age: give it a `Dockerfile`, or let g1t write one. It runs on |
| 212 | [Cloudflare Containers](https://developers.cloudflare.com/containers/) |
| 213 | behind the app's Worker, at the same addresses, custom domains and |
| 214 | previews as any other app. |
| 215 | |
| 216 | :::caution[Not switched on yet] |
| 217 | Docker deployments are built but off on g1t.page until Cloudflare's |
| 218 | container APIs are checked against g1t's account. Until then a container |
| 219 | build fails with "Docker deployments are not switched on for g1t.page |
| 220 | yet", and a project that is a container only for having a `Dockerfile`, |
| 221 | beside a site g1t could serve, is built as that site, as before. |
| 222 | ::: |
| 223 | |
| 224 | What a project is built as is decided in this order: |
| 225 | |
| 226 | 1. **Build as**, under **Settings → Deployments**: **Static site**, |
| 227 | **Workers** or **Docker** is what it is built as, whatever else it has. |
| 228 | 2. **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 | |
| 240 | g1t 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 | |
| 255 | The Dockerfile g1t writes is printed whole in the build's log. To change |
| 256 | how the app is built or run, commit it as `Dockerfile` and edit it. It |
| 257 | leaves `.git`, `node_modules`, `.venv` and `__pycache__` out of the image. |
| 258 | |
| 259 | What 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 | |
| 282 | What Deployments cannot run yet is on |
| 283 | [What g1t can't do yet](/about/limitations/#deployments). |
| 284 | |
| 285 | ## Previews of branches |
| 286 | |
| 287 | A preview is built when a pull request is opened, when it is marked ready, |
| 288 | and on every push to it, including an agent's. Pull requests from forks, |
| 289 | which is how g1t's agents work, are built from the fork. |
| 290 | |
| 291 | The 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 | |
| 299 | A newer push replaces a build that is still running for the same pull |
| 300 | request. Previews are marked `noindex`, so search engines leave them alone. |
| 301 | |
| 302 | The **Deployments** page lists the same failure of one app, repeated, as a |
| 303 | single row with how many times it happened and when it last did. The row |
| 304 | opens the newest of them. |
| 305 | |
| 306 | ## Production |
| 307 | |
| 308 | Each push to the default branch, which is each merge on a protected |
| 309 | branch, builds and replaces production. The **Deployments** page shows the |
| 310 | live address, the commit it runs and when it went up. |
| 311 | |
| 312 | ### The production screenshot |
| 313 | |
| 314 | When production goes live, g1t takes a screenshot of its home page, as a |
| 315 | 1280 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 | |
| 333 | An app deployed elsewhere gets a screenshot of its production address |
| 334 | too, 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 | |
| 339 | A [workflow](/guides/actions/) job can build and deploy the project |
| 340 | itself, with g1t's own action, `g1t/deploy`. Use it when the build needs |
| 341 | steps of yours first (generated code, a test that must pass, a tool the |
| 342 | default build lacks), or to deploy from a branch without a pull request. |
| 343 | The app, its address, its checks and its custom domains are the same as |
| 344 | for the builds g1t starts by itself. |
| 345 | |
| 346 | ```yaml |
| 347 | # .g1t/workflows/deploy.yml |
| 348 | name: Deploy |
| 349 | |
| 350 | on: |
| 351 | push: |
| 352 | branches: [main] |
| 353 | pull_request: |
| 354 | |
| 355 | permissions: |
| 356 | contents: read |
| 357 | deployments: write |
| 358 | |
| 359 | jobs: |
| 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 | |
| 376 | 1. Turn Deployments on for the project, as in |
| 377 | [Turn on deployments](#turn-on-deployments). |
| 378 | 2. Add a workflow like the one above. The job needs `deployments: write` |
| 379 | (or `pages: write`) in its `permissions:`. |
| 380 | 3. Push. The run on `main` deploys production; a pull request's run |
| 381 | deploys its preview. |
| 382 | |
| 383 | What 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 | |
| 403 | Its outputs are `url` (the app's address), `deployment-id` (`dpl_…`) and |
| 404 | `kind` (`production` or `preview`). |
| 405 | |
| 406 | The action calls |
| 407 | [`POST /repos/{owner}/{repo}/deployments/builds`](/reference/api/deployments/start-deployment-build/), |
| 408 | which checks the plan and the spend limit as any build does, sets the |
| 409 | commit's `g1t / deploy` check to pending, and answers with a token for |
| 410 | that build alone and the project's |
| 411 | [variables and secrets](#secrets-and-variables) for production or |
| 412 | previews. The action masks the token and every secret in the log, then |
| 413 | runs `g1t-runner deploy` in the checkout, which installs, builds and |
| 414 | uploads exactly as [What deploys](#what-deploys) says; its output is in |
| 415 | the step's log and on the deployment's page. When the build fails, or |
| 416 | g1t does not take what it built, the step fails and the deployment says |
| 417 | why. When the job ends before the build has finished, because it was |
| 418 | cancelled, failed in an earlier step or timed out, the deployment fails |
| 419 | as 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 |
| 441 | building 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 |
| 444 | need a server is `none`, and the step's log says what to change. Use |
| 445 | them 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 | |
| 458 | Turn on **Build with a workflow** under **Settings → Deployments** and the |
| 459 | project's builds run as a workflow g1t manages for it, instead of as |
| 460 | builds Deployments starts by itself. Nothing else changes: the same |
| 461 | previews 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 |
| 482 | name: g1t |
| 483 | |
| 484 | on: |
| 485 | push: |
| 486 | branches: [main] |
| 487 | pull_request: |
| 488 | |
| 489 | permissions: |
| 490 | contents: read |
| 491 | deployments: write |
| 492 | |
| 493 | jobs: |
| 494 | g1t: |
| 495 | name: g1t |
| 496 | uses: g1t/workflows/.g1t/workflows/deploy.yml@v1 |
| 497 | with: |
| 498 | project: web |
| 499 | secrets: inherit |
| 500 | ``` |
| 501 | |
| 502 | Its jobs are [workflow jobs](/guides/actions/#machine-sizes): on g1t's |
| 503 | machines they are charged as Actions time, at cost plus 20% like every |
| 504 | build, and on a [self-hosted runner](/guides/self-hosted-runners/) they |
| 505 | cost nothing. |
| 506 | |
| 507 | ## Custom domains |
| 508 | |
| 509 | Production 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 |
| 511 | to the project, not to a build: every redeploy is served on it with nothing |
| 512 | to change. |
| 513 | |
| 514 | ### Add a domain |
| 515 | |
| 516 | 1. Open the project's **Settings → Domains**, |
| 517 | `g1t.sh/<workspace>/<repo>/settings/domains`. |
| 518 | 2. 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. |
| 522 | 3. Add the DNS records the page lists, at whoever manages the domain's |
| 523 | DNS. Each name and value has a copy button. |
| 524 | 4. 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 | |
| 528 | Custom 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 |
| 530 | the Admin [role](/guides/access-and-roles/) on the project's repository. |
| 531 | |
| 532 | ### A subdomain, such as www |
| 533 | |
| 534 | Add one record: |
| 535 | |
| 536 | | Type | Name | Target | |
| 537 | | --- | --- | --- | |
| 538 | | `CNAME` | `www` | `domains.g1t.page` | |
| 539 | |
| 540 | The same goes for any subdomain, such as `app.example.com` (name `app`). |
| 541 | |
| 542 | ### The apex, such as example.com |
| 543 | |
| 544 | DNS does not allow a plain `CNAME` at the apex of a domain, so use your |
| 545 | provider'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 | |
| 554 | If 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 |
| 556 | any redirect) from `example.com` to `www.example.com`. |
| 557 | |
| 558 | ### Verification and certificates |
| 559 | |
| 560 | Pointing the domain at `domains.g1t.page` is what proves it is yours: once |
| 561 | the record is seen, the certificate authority checks the domain over HTTP |
| 562 | and issues a certificate, renewed by itself before it expires. Visitors are |
| 563 | always served over HTTPS, TLS 1.2 or newer. |
| 564 | |
| 565 | The page may also list a `TXT` record named `_cf-custom-hostname.<domain>`. |
| 566 | It proves ownership before traffic moves, which is useful when the domain |
| 567 | is 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 | |
| 571 | DNS changes can take from a few minutes to an hour to be seen. A domain |
| 572 | that stays at **Waiting for DNS** usually has a record with a typo, a |
| 573 | leftover `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 | |
| 578 | Choose **Remove** beside it. It stops serving the project at once, and any |
| 579 | domain redirecting to it goes too. Your DNS records are left as they are. |
| 580 | |
| 581 | ## When apps come down |
| 582 | |
| 583 | Nothing keeps running unasked. An app comes down, and stops costing |
| 584 | anything, 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 | |
| 596 | A preview that came down comes back with the next push to its pull |
| 597 | request, or **Redeploy** on the Deployments page. |
| 598 | |
| 599 | ## Who can do what |
| 600 | |
| 601 | What 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 | |
| 611 | A public repository's deployments, and their build logs, can be seen by |
| 612 | anyone, signed in or not, the same as its code. Its settings and domains |
| 613 | still need Admin. A private repository's deployments are seen only by |
| 614 | people with a role on it. |
| 615 | |
| 616 | ## Settings |
| 617 | |
| 618 | Under 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 | |
| 635 | Every request an app answers shows under the project's |
| 636 | [Observability](/guides/observability/) within seconds: its status, |
| 637 | duration, CPU time and the lines it wrote, with its exceptions grouped into |
| 638 | errors and its numbers by deployment. Nothing to set up. Each request |
| 639 | reaches 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 |
| 642 | its spans to the project without configuration. |
| 643 | |
| 644 | ## Secrets and variables |
| 645 | |
| 646 | Builds and running apps read the project's |
| 647 | [secrets and variables](/guides/secrets-and-variables/) that are |
| 648 | available 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 | |
| 655 | The build gets them as environment variables, with secrets hidden in its |
| 656 | log. The running app gets them as bindings, `env.KEY`, put in place by g1t |
| 657 | rather than the build; a container gets them as its environment variables |
| 658 | when it starts, and its image's build gets none. Secrets reach a preview only when the pull |
| 659 | request's author has Write or higher on the repository, whether a member |
| 660 | or an [outside collaborator](/guides/access-and-roles/#outside-collaborators), |
| 661 | or it is g1t working on its own. For a pull request g1t made, whoever |
| 662 | asked for it is the one whose role counts. A preview of anyone else's pull request, such as |
| 663 | one from a fork or by someone with Read or Triage, is built and runs with |
| 664 | variables only, no secrets. |
| 665 | |
| 666 | For example, a `STRIPE_KEY` secret with a Production row holding the live |
| 667 | key and a Preview row holding the test key gives every preview the test |
| 668 | key. |
| 669 | |
| 670 | ## What it costs |
| 671 | |
| 672 | Deployments come with the |
| 673 | [g1t plan](/guides/usage-and-billing/#the-g1t-plan), $20 a month per |
| 674 | workspace with $10 of usage included. There are **no quotas**: as many |
| 675 | projects and previews as you like, and no count of builds, requests or |
| 676 | domains ever stops or pauses an app. What deployments cost g1t is metered |
| 677 | from the first unit, at Cloudflare's price plus 20%, and paid from the |
| 678 | plan'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 | |
| 691 | A container's price is its size's memory, disk and vCPU while it is awake, |
| 692 | with the Durable Object in front of it, at Cloudflare's |
| 693 | [Containers prices](https://developers.cloudflare.com/containers/platform/pricing/) |
| 694 | plus 20%. Cloudflare bills CPU only as it is used; g1t charges every vCPU |
| 695 | as busy while the container is awake, so a container that mostly waits |
| 696 | costs g1t less than it is charged. Data sent is charged at the most a GB |
| 697 | costs anywhere ($0.05; $0.025 in North America and Europe), since g1t |
| 698 | cannot tell where it went. Each minute it is awake is charged from its |
| 699 | start, so a container woken by one request, and stopped five minutes after |
| 700 | it, 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 |
| 712 | day. A container that is visited all day never sleeps, and costs the last |
| 713 | column; one visited for an hour a day costs about a thirtieth of it. |
| 714 | |
| 715 | Builds are charged when they finish. Requests, CPU time, custom domains |
| 716 | and container time are counted through the month, so your limit and |
| 717 | **Billing** follow them as they happen, and charged once, on the first |
| 718 | sweep after the month ends, as one line: *Deployments in 2026-10: |
| 719 | 1,240,000 requests, 3,100,000 CPU ms, 2 custom domains, 41.5 container |
| 720 | hours and 2.3 GB sent*. |
| 721 | |
| 722 | A static site, or plain HTML, runs no code when it is visited: its files |
| 723 | are served as they are, so serving it adds no requests or CPU time. Its |
| 724 | builds are metered. A Workers project's code runs on every request, so its |
| 725 | requests and CPU time are metered too. A container's Worker is metered the |
| 726 | same way, and its container by the minute while it is awake. |
| 727 | |
| 728 | An app that no one visits costs nothing. Previews still come down when |
| 729 | their pull request closes and after their idle days, to keep the list of |
| 730 | what is up short. |
| 731 | |
| 732 | The 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 |
| 734 | container is stopped and starts no more) and the plan ending (they come |
| 735 | down). |
| 736 | |
| 737 | Deployments used to be a $5 plan of their own. A Deployments subscription |
| 738 | bought before keeps working until its current period ends, and is not |
| 739 | renewed. |
| 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 | |
| 761 | Usage from the month that is under way is still charged once it ends. |
| 762 | |
| 763 | ## How it works |
| 764 | |
| 765 | 1. 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. |
| 771 | 2. 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. |
| 779 | 3. 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. |
| 782 | 4. 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). |
| 786 | 5. 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 | |
| 791 | Your 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 |
| 793 | you sign in to. |
| 794 | |
| 795 | ### Containers |
| 796 | |
| 797 | 1. 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. |
| 803 | 2. 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. |
| 807 | 3. 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. |
| 815 | 4. 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. |
| 822 | 5. A preview's container comes down with its pull request, its |
| 823 | application and Worker deleted together. |
| 824 | |
| 825 | Why it is built this way, from Cloudflare's documentation and Wrangler's |
| 826 | own 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 | |
| 892 | Deployments need a Workers for Platforms namespace and a zone for apps: |
| 893 | |
| 894 | 1. Create the namespace: `npx wrangler dispatch-namespace create g1t-deployments`. |
| 895 | 2. Add a proxied wildcard DNS record (`*`, `AAAA`, `100::`) on the apps' |
| 896 | zone, and set the zone in `services/pages/wrangler.jsonc`. |
| 897 | 3. 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`. |
| 900 | 4. Deploy `services/deployments`, `services/pages`, and the runner. |
| 901 | |
| 902 | Docker deployments also need the token to have **Containers: Edit**, a |
| 903 | Workers 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) |
| 906 | for what to check first. |
| 907 | |
| 908 | Custom domains also need Cloudflare for SaaS on the apps' zone. Turn it on |
| 909 | under the zone's **SSL/TLS → Custom Hostnames**, give the token **SSL and |
| 910 | Certificates: Edit** on the zone, and run `scripts/setup-custom-domains.sh`: |
| 911 | it creates the `g1t-domains` KV namespace, the fallback origin's DNS record |
| 912 | (`domains`, `AAAA`, `100::`, proxied) and sets it as the fallback origin. |
| 913 | The dispatcher's `*/*` route on the zone, in `services/pages/wrangler.jsonc`, |
| 914 | is what brings custom domains' traffic to it. |
| 915 | |
| 916 | Without a card processor configured, every feature is on and nothing is |
| 917 | charged. |