Skip to content

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

g1t.sh
340 linesCodeBlameRaw
1---
2title: Run g1t yourself
3description: Start the core forge on your own machine with Docker Compose.
4---
5
6g1t is MIT licensed. You can run the core forge on your own machine:
7accounts, workspaces, repositories, git over HTTP, issues and pull
8requests, the site to browse them, and the REST API and MCP server. Your
9repositories are plain bare git repositories on a Docker volume.
10
11This is an early version. It is for trying g1t out and for small teams on
12a private network, not yet for an installation on the open internet.
13
14## What works and what is off
15
16| Feature | Self-hosted |
17| --- | --- |
18| Sign up, sign in, email confirmation | Works. Mail goes to the bundled Mailpit inbox. |
19| Workspaces, members, access tokens | Works |
20| Repositories: create, push and clone over HTTP, browse code, commits | Works. A fresh clone's pack is kept in the bundled object store, so the next clone of the same commit is served from there. |
21| Issues, comments, labels | Works |
22| Pull requests from a branch or from a fork, merged onto the default branch | Works, when the pull request is up to date with the default branch. Bringing one up to date first needs g1t's agent, which is off. |
23| The merge queue | Takes pull requests and shows them waiting. Testing and landing them needs g1t's agent, which is off: take a pull request out of the queue, or turn the queue off, to merge it. |
24| [The REST API](/reference/api/), OAuth and [MCP](/reference/mcp/) | Work, on a port of their own: `http://localhost:8789`, with the MCP server at `http://localhost:8789/mcp` |
25| [Container images](/guides/containers/): `docker login`, push and pull at your `PUBLIC_URL` | Works, kept in the bundled object store, with no limit on a layer's size or on pulls |
26| Site search | Works |
27| A status page of your own | Works, at `http://localhost:8788` ([below](#the-status-page)) |
28| Webhooks, integrations | Work, retries included |
29| Sign in with GitHub, import from GitHub | Off until you register a GitHub App of your own ([below](#sign-in-with-github-and-import-from-github)). |
30| g1t's agent: changes, plans and reviews | Off |
31| Context hub search | Off |
32| Deployments on `g1t.page` | Off |
33| [Observability](/guides/observability/): logs, traces, metrics and errors sent with OpenTelemetry | Works. Send OTLP to `http://localhost:8789/otlp` with a project's ingest token. |
34| Billing | Off. Nothing is charged, and no usage limit stops work. |
35| Site analytics | Off. Only g1t.sh sends page analytics (to HeyCatch, as its [privacy policy](https://g1t.sh/policies/privacy#how-the-site-is-used) describes); your installation sends none. |
36| [The phone app](/guides/mobile/) | Works: Chat, notifications, Code. Point it here from your download page's QR code ([below](#the-phone-app)). |
37| Git over SSH and the `g1t` CLI | Not available yet |
38| Scheduled jobs | Run on their schedules inside the g1t container: webhook retries, purging deleted repositories, the packages sweep, security sweeps, audit log retention, access request summaries and emptying artifacts left in the trash for 30 days. Actions schedules (`on: schedule`) are not run. |
39
40What hosted g1t cannot do yet either is on
41[What g1t can't do yet](/about/limitations/). What runs inside the
42installation, and what stands in for each part of g1t.sh, is in
43[How a self-hosted g1t runs](/guides/self-hosting-architecture/).
44
45## Before you start
46
47- Docker with Compose v2 (`docker compose version`).
48- About 4 GB of free disk space for the images.
49- Ports 8787, 8788, 8789 and 8025 free on your machine.
50
51## Start g1t
52
531. Get the source:
54
55 ```sh
56 git clone https://g1t.sh/flagon-io/g1t.git
57 cd g1t
58 ```
59
602. Build and start it. The first build compiles every service and takes a
61 while:
62
63 ```sh
64 docker compose -f deploy/self-host/docker-compose.yml up --build -d
65 ```
66
673. Open [http://localhost:8787](http://localhost:8787) and create an
68 account.
694. Open the Mailpit inbox at [http://localhost:8025](http://localhost:8025)
70 and enter the code from the confirmation email on the page the site
71 shows you, or follow the link in the same email. Set `IDENTITY_KEY`
72 (the setup does) so codes are kept as keyed hashes.
735. Create a workspace, then a repository.
74
75## Push and clone
76
77The remote is the site's address, then the workspace and repository:
78
79```sh
80git remote add origin http://localhost:8787/<workspace>/<repo>.git
81git push -u origin main
82```
83
84Git asks for a username and password: use your g1t username and password,
85or an access token, as described in [Git](/guides/git/#authentication).
86Public repositories clone without signing in:
87
88```sh
89git clone http://localhost:8787/<workspace>/<repo>.git
90```
91
92## Use the API and MCP
93
94The API answers at `http://localhost:8789`, the same routes as
95`https://api.g1t.sh` (see the [API reference](/reference/api/)). Make an
96access token under **Settings → Access tokens**, then:
97
98```sh
99curl -H "Authorization: Bearer $G1T_TOKEN" http://localhost:8789/user
100```
101
102The MCP server is at `http://localhost:8789/mcp`. Connect an agent to it
103as [Bring your own agent](/guides/bring-your-own-agent/) shows, with this
104address in place of `https://mcp.g1t.sh`:
105
106```sh
107claude mcp add --transport http g1t http://localhost:8789/mcp
108```
109
110Applications that sign people in with OAuth find everything at
111`http://localhost:8789/.well-known/oauth-authorization-server`: the issuer
112is the API's address, and people approve on your site, at
113`PUBLIC_URL/oauth/authorize`. The site's clone box, agent setup and
114access token examples show your own addresses.
115
116## Check an installation
117
118`deploy/self-host/smoke.sh` checks an installation from end to end:
119
1201. It signs up a new account, confirms it through Mailpit, makes a
121 workspace and a repository, pushes and clones (twice, the second from
122 the clone pack cache), opens an issue and reads the code back through
123 the site.
1242. It makes an access token and calls the API, the OAuth metadata and the
125 MCP server with it.
1263. It publishes an npm package to your installation's registry and
127 installs it back.
1284. It opens a pull request from a branch and one from a fork, through the
129 API, and merges both onto `main`.
1305. It turns the merge queue on, merges a pull request into it, takes it
131 out again, and merges it with the queue off.
132
133```sh
134bash deploy/self-host/smoke.sh
135```
136
137To also run every scheduled job once, give it the command that does so
138inside the container:
139
140```sh
141SCHEDULER_ONCE="docker compose -f deploy/self-host/docker-compose.yml exec -T g1t \
142 node deploy/self-host/scheduler.mjs --once /data/generated/schedules.json" \
143 bash deploy/self-host/smoke.sh
144```
145
146It prints `All checks passed` when every step worked. It needs `curl`,
147`git`, `node` and `npm`; `PACKAGES=off` skips the npm package.
148
149## Settings
150
151Set these in the environment, or in a `.env` file next to
152`docker-compose.yml`:
153
154| Variable | Default | What it does |
155| --- | --- | --- |
156| `PUBLIC_URL` | `http://localhost:8787` | The address people use. Links in email, clone addresses and the site's link previews point here. |
157| `G1T_PORT` | `8787` | The port the site is published on |
158| `API_PORT` | `8789` | The port the API and the MCP server are published on |
159| `API_URL` | `PUBLIC_URL`'s host on `API_PORT` | The address of the API, as people and applications reach it. It is also the OAuth issuer. Set it when the API is behind a proxy, for example `https://api.git.example.com`. |
160| `MCP_URL` | `API_URL/mcp` | The address of the MCP server. |
161| `USERCONTENT_URL` | `PUBLIC_URL/-/usercontent` | Where [raw files](/guides/git/#raw-files) and avatars are served. Set it to a host of its own (another domain, not a subdomain of `PUBLIC_URL`'s, for example `https://files.example.net`) that your proxy sends to the same port as `PUBLIC_URL`, keeping the `Host` header, so a file in a repository can never reach the site's session cookie. Unset, they are served under `PUBLIC_URL`, still as plain text or images that cannot run script. |
162| `MAILPIT_PORT` | `8025` | The port of the Mailpit inbox |
163| `MAIL_FROM` | `g1t <noreply@localhost>` | The sender of g1t's email |
164| `MAIL_URL` | `http://mailpit:8025` | The Mailpit server g1t sends mail through |
165| `S3_ENDPOINT`, `S3_BUCKET`, `S3_REGION`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | the bundled RustFS, bucket `g1t-packages` | Where packages' files are kept: any S3-compatible store. Change the two keys before first start; RustFS is made with them. |
166| `S3_PUBLIC_ENDPOINT` | (none) | The store's address as clients reach it. When set, large layers are downloaded from it directly with a signed URL. |
167| `PACK_S3_BUCKET` | `g1t-git-packs` | The bucket on the same store that packs for fresh clones are kept in, so the next clone of the same commit is not built again. The bundled store deletes packs after 7 days, and uploads left unfinished after a day; on another store, give the bucket a lifecycle rule that does the same. |
168| `DOCS_S3_BUCKET` | `g1t-docs-files` | The bucket on the same store that images and files added to pages and [artifacts](/guides/artifacts/) are kept in. |
169| `RUSTFS_IMAGE` | `rustfs/rustfs:1.0.1` | The image the bundled object store runs: [RustFS](https://rustfs.com), an S3-compatible server. |
170| `AWS_CLI_IMAGE` | `amazon/aws-cli:2.37.10` | The image `storage-setup` makes the buckets and the packs' lifecycle rule with. |
171| `BACKUP_S3_BUCKET` | `g1t-backups` | The bucket on the same store that nightly repository backups (a `git bundle` of each repository whose branches or tags changed) are kept in. The bundles are cut by g1t's runner, which this installation does not run yet, so the bucket stays empty for now: copy the volumes, as below. |
172| `STATUS_PORT` | `8788` | The port the status page is published on |
173| `STATUS_PROBE_REPO` | (none) | A public repository, `workspace/repo`, whose branches the status page lists every minute as a clone would. Empty: git is not checked. |
174| `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET` | (none) | A [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) widget's site key and secret key, to check that a person fills in the sign-up form. With either unset, sign-up has no such check. |
175| `PHONE_PUSH` | `on` | `off`: send [the phone app](#the-phone-app) no notifications, so their words never pass through Expo's push service. Browser and desktop notifications are not affected. |
176
177## The phone app
178
179[The g1t phone app](/guides/mobile/) works with your installation as it
180does with g1t.sh: the same sign-in (OAuth, approved on your site), Chat
181live, notifications and Code. Nothing is set up for it here.
182
1831. Make the installation reachable from a phone: `PUBLIC_URL` and
184 `API_URL` set to addresses the phone can open, such as
185 `https://git.example.com` and `https://api.git.example.com` behind your
186 proxy. Store builds of the app open `https` addresses only.
1872. People open `PUBLIC_URL/download` and scan the code under **On your
188 phone**, or tap **Open in the g1t app** on the phone. The link is
189 `g1t://connect?server=<PUBLIC_URL>`; you can hand it out yourself.
1903. The app reads `PUBLIC_URL/.well-known/g1t.json` for where the API is,
191 and signs in there.
192
193Notifications reach phones through the push service of g1t's app on Expo,
194which hands them to Google's (Android) or Apple's (iOS): your installation
195sends each one there, with its title and a line of the message, as g1t.sh
196does. No account or key is needed. To send phones nothing, set
197`PHONE_PUSH=off`: the app then tells people your g1t sends no
198notifications, and everything else works.
199
200## The status page
201
202The `status` service runs the same status page as
203[status.g1t.sh](/guides/status/), in a process of its own, so it keeps
204answering when the site does not. Open
205[http://localhost:8788](http://localhost:8788).
206
207Every minute it loads the site's sign-in page from inside Compose, and,
208with `STATUS_PROBE_REPO` set, lists that repository's branches. It keeps
20990 days of history on its own volume, `g1t-status`. Parts an installation
210of your own does not check (the API, MCP, docs, deployments, the model
211proxy and billing) are left off its page.
212
213The links to **Status** in the site's footer and account menu still point
214to status.g1t.sh; pointing them at your own status page is not a setting
215yet.
216
217To deliver email to real inboxes, have Mailpit relay it through your SMTP
218server. The settings are in `docker-compose.yml`, under `mailpit`.
219
220Sign-in cookies are marked `Secure`. Browsers accept them on
221`http://localhost`. On any other address, put g1t behind HTTPS (a reverse
222proxy such as Caddy or nginx with a certificate) and set `PUBLIC_URL` to
223the `https://` address.
224
225## Sign in with GitHub and import from GitHub
226
227g1t.sh's GitHub App works only for g1t.sh. To offer **Continue with
228GitHub** and **Import from GitHub** on your own g1t, register an app of
229your own. Without one, neither button appears.
230
2311. On GitHub, open **Settings → Developer settings → GitHub Apps → New
232 GitHub App** (or the same under an organization's settings).
2332. Fill it in, with `PUBLIC_URL` standing for your g1t's address:
234
235 | Setting | Value |
236 | --- | --- |
237 | Callback URL | `PUBLIC_URL/auth/github/callback` |
238 | Expire user authorization tokens | On |
239 | Request user authorization (OAuth) during installation | Off |
240 | Enable Device Flow | Off |
241 | Setup URL | `PUBLIC_URL/integrations/github/setup` |
242 | Redirect on update | On |
243 | Webhook | On, with the URL `API_URL/hooks/github` and a secret you choose, once GitHub can reach your API. Otherwise off: imports work without it. |
244 | Repository permissions | Contents: Read; Metadata: Read; Issues: Read |
245 | Account permissions | Email addresses: Read |
246
2473. Create it, then on its page note the **App ID**, the **Client ID** and
248 the slug (the last part of its public address,
249 `github.com/apps/<slug>`). Generate a **client secret** and a **private
250 key**, which downloads a `.pem` file.
2514. Set these before starting g1t, in the environment or in `.env`:
252
253 | Variable | Value |
254 | --- | --- |
255 | `GITHUB_APP_ID` | The App ID |
256 | `GITHUB_APP_SLUG` | The slug |
257 | `GITHUB_APP_CLIENT_ID` | The Client ID |
258 | `GITHUB_APP_CLIENT_SECRET` | The client secret |
259 | `GITHUB_APP_PRIVATE_KEY` | The `.pem` file's contents, as downloaded. Line breaks may be written as `\n`. |
260 | `GITHUB_APP_WEBHOOK_SECRET` | The webhook secret, if the webhook is on |
261
2625. Restart g1t: `docker compose -f deploy/self-host/docker-compose.yml up -d`.
263
264g1t makes its own key for the GitHub tokens it keeps (`IDENTITY_KEY`, in
265the `g1t-data` volume) on first start. What the app can do, and what comes
266across from GitHub, is in [GitHub](/guides/github/).
267
268## Customize
269
270[Customize](/guides/customize/) works on a g1t you run yourself:
271marketplaces and plugins are read from public repositories on GitHub and
272from repositories on your installation, and connectors sign in to MCP
273servers from your installation's address. The agents service's
274`CONNECTORS_KEY` seals connectors' keys and OAuth tokens; `start.sh` makes
275it on first start, in `keys.env` beside the other sealing keys, and adds
276it to a `keys.env` from before it existed. Without it, open MCP servers
277work and nothing that signs in can be kept.
278
279## Where your data lives
280
281| Volume | Holds |
282| --- | --- |
283| `g1t_g1t-data` | Accounts, workspaces, issues and every other record, as SQLite files; the keys that seal stored secrets (`keys.env`) |
284| `g1t_g1t-git` | Your repositories, one bare git repository each |
285| `g1t_g1t-objects` | The bundled object store (RustFS): container images' layers and other package files in `g1t-packages`, the `g1t-backups` bucket, the clone packs in `g1t-git-packs`, and files added to pages and artifacts in `g1t-docs-files` |
286| `g1t_g1t-secrets` | The key the site and the git store share |
287
288To back up, stop g1t and copy the volumes:
289
290```sh
291docker compose -f deploy/self-host/docker-compose.yml stop
292docker run --rm -v g1t_g1t-data:/data -v g1t_g1t-git:/git -v "$PWD":/backup \
293 debian:bookworm-slim tar czf /backup/g1t-backup.tgz /data /git
294docker compose -f deploy/self-host/docker-compose.yml start
295```
296
297Keep `keys.env` with the backup. Without it, saved webhook, integration,
298connector and Actions secrets cannot be opened.
299
300## Upgrade
301
302Pull the new source and rebuild. Database changes are applied on start,
303and changes already applied are skipped:
304
305```sh
306git pull
307docker compose -f deploy/self-host/docker-compose.yml up --build -d
308```
309
310### Installations started before 7 October 2026
311
312These kept packages' files and backups in MinIO, in the `g1t_g1t-packages`
313volume. The bundled store is now RustFS, in `g1t_g1t-objects`, and starts
314empty. After the upgrade above, copy the old objects across (use your own
315`S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` if you changed them):
316
317```sh
318docker run -d --name g1t-old-store --network g1t_default \
319 -v g1t_g1t-packages:/data -e MINIO_ROOT_USER=g1t \
320 -e MINIO_ROOT_PASSWORD=g1t-packages-secret pgsty/minio server /data
321docker run --rm --network g1t_default -e AWS_ACCESS_KEY_ID=g1t \
322 -e AWS_SECRET_ACCESS_KEY=g1t-packages-secret -e AWS_DEFAULT_REGION=us-east-1 \
323 --entrypoint sh amazon/aws-cli:2.37.10 -c '
324 for b in g1t-packages g1t-backups; do
325 aws --endpoint-url http://g1t-old-store:9000 s3 sync "s3://$b" "/tmp/$b" &&
326 aws --endpoint-url http://rustfs:9000 s3 sync "/tmp/$b" "s3://$b"
327 done'
328docker rm -f g1t-old-store
329```
330
331The clone packs are not copied: they are a cache, and are made again on
332the next clone. Once your images and packages pull, remove the old volume
333with `docker volume rm g1t_g1t-packages`.
334
335## Stop and remove
336
337```sh
338docker compose -f deploy/self-host/docker-compose.yml down # keeps your data
339docker compose -f deploy/self-host/docker-compose.yml down -v # deletes it
340```