Git hosting and a container registry in one Rust binary (axum + Astro)
Clone ▾
| 131 B | |
| 253 B | |
| 538 B | |
| 124 KB | |
| 322 B | |
| 2.6 KB | |
| 570 B | |
| 2.9 KB | |
| 5.9 KB |
irongit
Git hosting and a container registry in one binary, built on the astrum
template (axum + Astro). Users and organizations share one namespace; repos
and images are public or private independently; profiles have a contribution
heatmap; ig is the CLI.
What runs where
| Piece | Where it lives |
|---|---|
| Accounts, permissions, metadata, sessions, tokens | Postgres |
| Git repositories (bare) | Server disk, DATA_DIR/repos/{id}.git (by id, so renames never move files) |
| Git LFS objects | R2 lfs/, uploaded and downloaded by clients through presigned URLs |
| Image layers and configs | R2 registry/blobs/, pulls are 307 redirects to presigned URLs |
| Image manifests and tags | Postgres |
| Avatars | R2 avatars/ |
| CLI releases | R2 releases/cli/ |
| Backups (git bundles + pg_dump) | R2 backups/ |
| Logs | DATA_DIR/logs/ (server.*.log, frontend.*.log, hooks.*.log) |
Git transport and browsing go through the system git binary (it must be on
PATH). Pushes run a pre-receive hook (irongit hook pre-receive, the same
binary) that rejects files over MAX_FILE_MB and pushes over the owner's
quota, pointing people at Git LFS.
Develop
Requirements: Rust, bun, git, watchexec, Postgres (local dev uses the
container on 5432, database irongit).
./dev.sh # http://localhost:7878, git SSH on 2222
Configuration is .env at the repo root; every variable is described in
secrets.md (gitignored). The first account registered becomes site admin;
irongit admin promote <user> makes more.
cargo test -p irongit # unit tests
cargo run -p irongit -- admin check-storage # verify R2 credentials
Build
./build.sh
# target/release/irongit server (x86_64 Linux)
# target/x86_64-unknown-linux-musl/release/ig CLI, static
Publish a CLI build so install.sh and ig upgrade serve it:
irongit admin publish-cli target/x86_64-unknown-linux-musl/release/ig --version 0.1.0
The ig CLI
curl -fsSL http://localhost:7878/install.sh | sh # installs ig and docker-credential-ig
ig login # approve in the browser; sets up git and docker helpers
ig repo create api --private
ig repo clone you/api
docker push localhost:7878/you/api:1.0
ig image visibility you/api public
ig ssh-key add # ~/.ssh/id_ed25519.pub by default
ig import github my-github-name # bring a GitHub account over (uses your gh login)
ig upgrade
Full reference at /docs/cli; git, SSH and LFS at /docs/git; the
registry at /docs/registry; GitHub imports at /docs/import.
Importing from GitHub
/new/import (or ig import github OWNER) copies a GitHub user's or
organization's repositories: every branch and tag, Git LFS objects (into R2),
description, default branch, visibility and archived state; optionally the
profile; and, for your own account, your GitHub-verified emails so imported
commits count on your heatmap. It runs as a background job on the server
(backend/src/importer/). GitHub tokens are held in memory only and OAuth
import tokens are revoked when the job ends. Sign in with GitHub and "Connect
GitHub" need GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET (see secrets.md).
Tests
cargo test --workspace # unit tests (server, CLI, shared types)
scripts/e2e/ssh-lfs.sh # SSH, LFS end to end against a running server
scripts/e2e/backup.sh # backup to R2 and restore every bundle
Layout
backend/
migrations/ Postgres schema
src/main.rs startup, subcommands (serve, hook, admin)
src/auth.rs sessions, personal access tokens, extractors
src/perm.rs every access decision (repos, images, orgs)
src/storage.rs R2 over the S3 API (presigned URLs, multipart)
src/git.rs git CLI wrapper used by the web UI
src/git_http.rs smart HTTP (v0, v1, v2)
src/transport.rs spawning upload-pack/receive-pack with hooks and limits
src/hook.rs pre-receive size and quota checks
src/push.rs post-push: events, default branch, heatmap contributions
src/ssh.rs built-in SSH server
src/lfs.rs Git LFS batch API
src/registry/ OCI distribution API (docker push/pull)
src/api/ JSON API for the CLI, device login, CLI releases
src/backup.rs scheduled backups to R2
src/web/ server-rendered pages (maud) poured into the Astro shell
cli/ the ig CLI
shared/ types shared by the API and the CLI
frontend/ Astro: landing, docs, the page shell, CSS and client JS
Deploying
Production is https://git.hygo.ai on the rybbit server (ssh rybbit), as
the irongit service in /opt/hygo/docker-compose.yml: same shared Postgres
(database irongit) and Caddy as the other hygo services, config in
/opt/hygo/irongit.env, data in the irongit-data volume.
./deploy.sh # build the image, docker load it on rybbit, roll irongit, wait for healthy,
# publish the bundled ig to R2 when cli/Cargo.toml's version changed
Pushing main to git.hygo.ai deploys automatically: .githooks/pre-push runs
./deploy.sh first and stops the push if the deploy fails (enable it per
clone with git config core.hooksPath .githooks; skip once with
git push --no-verify or SKIP_DEPLOY=1 git push). It refuses to deploy
uncommitted or untracked files, since the image is built from the folder.
Roll back:
ssh rybbit 'cd /opt/hygo && docker tag irongit:$(cat .irongit.prev) irongit:latest && docker compose up -d --no-deps irongit'
The DNS record is DNS-only (grey cloud): the Cloudflare proxy caps request
bodies at 100 MB, which breaks large docker push layers and big HTTPS git
pushes. Caddy gets its own Let's Encrypt certificate. Git over SSH is
published directly on port 2222 (ssh://git@git.hygo.ai:2222/owner/repo.git).
Admin commands run inside the container, e.g.
ssh rybbit docker exec irongit irongit admin promote <user>.