Git hosting and a container registry in one Rust binary (axum + Astro)
| 1 | # irongit |
| 2 | |
| 3 | Git hosting and a container registry in one binary, built on the astrum |
| 4 | template (axum + Astro). Users and organizations share one namespace; repos |
| 5 | and images are public or private independently; profiles have a contribution |
| 6 | heatmap; `ig` is the CLI. |
| 7 | |
| 8 | ## What runs where |
| 9 | |
| 10 | | Piece | Where it lives | |
| 11 | | --- | --- | |
| 12 | | Accounts, permissions, metadata, sessions, tokens | Postgres | |
| 13 | | Git repositories (bare) | Server disk, `DATA_DIR/repos/{id}.git` (by id, so renames never move files) | |
| 14 | | Git LFS objects | R2 `lfs/`, uploaded and downloaded by clients through presigned URLs | |
| 15 | | Image layers and configs | R2 `registry/blobs/`, pulls are 307 redirects to presigned URLs | |
| 16 | | Image manifests and tags | Postgres | |
| 17 | | Avatars | R2 `avatars/` | |
| 18 | | CLI releases | R2 `releases/cli/` | |
| 19 | | Backups (git bundles + pg_dump) | R2 `backups/` | |
| 20 | | Logs | `DATA_DIR/logs/` (`server.*.log`, `frontend.*.log`, `hooks.*.log`) | |
| 21 | |
| 22 | Git transport and browsing go through the system `git` binary (it must be on |
| 23 | PATH). Pushes run a pre-receive hook (`irongit hook pre-receive`, the same |
| 24 | binary) that rejects files over `MAX_FILE_MB` and pushes over the owner's |
| 25 | quota, pointing people at Git LFS. |
| 26 | |
| 27 | ## Develop |
| 28 | |
| 29 | Requirements: Rust, bun, git, watchexec, Postgres (local dev uses the |
| 30 | container on 5432, database `irongit`). |
| 31 | |
| 32 | ```sh |
| 33 | ./dev.sh # http://localhost:7878, git SSH on 2222 |
| 34 | ``` |
| 35 | |
| 36 | Configuration is `.env` at the repo root; every variable is described in |
| 37 | `secrets.md` (gitignored). The first account registered becomes site admin; |
| 38 | `irongit admin promote <user>` makes more. |
| 39 | |
| 40 | ```sh |
| 41 | cargo test -p irongit # unit tests |
| 42 | cargo run -p irongit -- admin check-storage # verify R2 credentials |
| 43 | ``` |
| 44 | |
| 45 | ## Build |
| 46 | |
| 47 | ```sh |
| 48 | ./build.sh |
| 49 | # target/release/irongit server (x86_64 Linux) |
| 50 | # target/x86_64-unknown-linux-musl/release/ig CLI, static |
| 51 | ``` |
| 52 | |
| 53 | Publish a CLI build so `install.sh` and `ig upgrade` serve it: |
| 54 | |
| 55 | ```sh |
| 56 | irongit admin publish-cli target/x86_64-unknown-linux-musl/release/ig --version 0.1.0 |
| 57 | ``` |
| 58 | |
| 59 | ## The ig CLI |
| 60 | |
| 61 | ```sh |
| 62 | curl -fsSL http://localhost:7878/install.sh | sh # installs ig and docker-credential-ig |
| 63 | ig login # approve in the browser; sets up git and docker helpers |
| 64 | ig repo create api --private |
| 65 | ig repo clone you/api |
| 66 | docker push localhost:7878/you/api:1.0 |
| 67 | ig image visibility you/api public |
| 68 | ig ssh-key add # ~/.ssh/id_ed25519.pub by default |
| 69 | ig import github my-github-name # bring a GitHub account over (uses your gh login) |
| 70 | ig upgrade |
| 71 | ``` |
| 72 | |
| 73 | Full reference at `/docs/cli`; git, SSH and LFS at `/docs/git`; the |
| 74 | registry at `/docs/registry`; GitHub imports at `/docs/import`. |
| 75 | |
| 76 | ## Importing from GitHub |
| 77 | |
| 78 | `/new/import` (or `ig import github OWNER`) copies a GitHub user's or |
| 79 | organization's repositories: every branch and tag, Git LFS objects (into R2), |
| 80 | description, default branch, visibility and archived state; optionally the |
| 81 | profile; and, for your own account, your GitHub-verified emails so imported |
| 82 | commits count on your heatmap. It runs as a background job on the server |
| 83 | (`backend/src/importer/`). GitHub tokens are held in memory only and OAuth |
| 84 | import tokens are revoked when the job ends. Sign in with GitHub and "Connect |
| 85 | GitHub" need `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` (see secrets.md). |
| 86 | |
| 87 | ## Tests |
| 88 | |
| 89 | ```sh |
| 90 | cargo test --workspace # unit tests (server, CLI, shared types) |
| 91 | scripts/e2e/ssh-lfs.sh # SSH, LFS end to end against a running server |
| 92 | scripts/e2e/backup.sh # backup to R2 and restore every bundle |
| 93 | ``` |
| 94 | |
| 95 | ## Layout |
| 96 | |
| 97 | ``` |
| 98 | backend/ |
| 99 | migrations/ Postgres schema |
| 100 | src/main.rs startup, subcommands (serve, hook, admin) |
| 101 | src/auth.rs sessions, personal access tokens, extractors |
| 102 | src/perm.rs every access decision (repos, images, orgs) |
| 103 | src/storage.rs R2 over the S3 API (presigned URLs, multipart) |
| 104 | src/git.rs git CLI wrapper used by the web UI |
| 105 | src/git_http.rs smart HTTP (v0, v1, v2) |
| 106 | src/transport.rs spawning upload-pack/receive-pack with hooks and limits |
| 107 | src/hook.rs pre-receive size and quota checks |
| 108 | src/push.rs post-push: events, default branch, heatmap contributions |
| 109 | src/ssh.rs built-in SSH server |
| 110 | src/lfs.rs Git LFS batch API |
| 111 | src/registry/ OCI distribution API (docker push/pull) |
| 112 | src/api/ JSON API for the CLI, device login, CLI releases |
| 113 | src/backup.rs scheduled backups to R2 |
| 114 | src/web/ server-rendered pages (maud) poured into the Astro shell |
| 115 | cli/ the ig CLI |
| 116 | shared/ types shared by the API and the CLI |
| 117 | frontend/ Astro: landing, docs, the page shell, CSS and client JS |
| 118 | ``` |
| 119 | |
| 120 | ## Deploying |
| 121 | |
| 122 | Production is `https://git.hygo.ai` on the rybbit server (`ssh rybbit`), as |
| 123 | the `irongit` service in `/opt/hygo/docker-compose.yml`: same shared Postgres |
| 124 | (database `irongit`) and Caddy as the other hygo services, config in |
| 125 | `/opt/hygo/irongit.env`, data in the `irongit-data` volume. |
| 126 | |
| 127 | ```sh |
| 128 | ./deploy.sh # build the image, docker load it on rybbit, roll irongit, wait for healthy, |
| 129 | # publish the bundled ig to R2 when cli/Cargo.toml's version changed |
| 130 | ``` |
| 131 | |
| 132 | Pushing `main` to git.hygo.ai deploys automatically: `.githooks/pre-push` runs |
| 133 | `./deploy.sh` first and stops the push if the deploy fails (enable it per |
| 134 | clone with `git config core.hooksPath .githooks`; skip once with |
| 135 | `git push --no-verify` or `SKIP_DEPLOY=1 git push`). It refuses to deploy |
| 136 | uncommitted or untracked files, since the image is built from the folder. |
| 137 | |
| 138 | Roll back: |
| 139 | |
| 140 | ```sh |
| 141 | ssh rybbit 'cd /opt/hygo && docker tag irongit:$(cat .irongit.prev) irongit:latest && docker compose up -d --no-deps irongit' |
| 142 | ``` |
| 143 | |
| 144 | The DNS record is **DNS-only** (grey cloud): the Cloudflare proxy caps request |
| 145 | bodies at 100 MB, which breaks large `docker push` layers and big HTTPS git |
| 146 | pushes. Caddy gets its own Let's Encrypt certificate. Git over SSH is |
| 147 | published directly on port 2222 (`ssh://git@git.hygo.ai:2222/owner/repo.git`). |
| 148 | Admin commands run inside the container, e.g. |
| 149 | `ssh rybbit docker exec irongit irongit admin promote <user>`. |