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