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