frieren
a self-hosted git server in one binary — everyone reads, only the owner writes
docs: README with pixel banner
d549b6ca66a8370f2d43c1e28c789c6c7e3e4ff6
justin06lee · Aug 18, 2026, 10:11 PM (5h ago)
README.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ assets/frieren.svg | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+)
| @@ -0,0 +1,87 @@ | |||
| 1 | + | <div align="center"> | |
| 2 | + | ||
| 3 | + | <img src="assets/frieren.svg" alt="frieren" width="460" /> | |
| 4 | + | ||
| 5 | + | # frieren | |
| 6 | + | ||
| 7 | + | **A self-hosted git server in one binary — everyone can read, only you can write.**<br> | |
| 8 | + | *Your code, on your machine, outliving every platform.* | |
| 9 | + | ||
| 10 | + | </div> | |
| 11 | + | ||
| 12 | + | --- | |
| 13 | + | ||
| 14 | + | frieren is a small alternative to GitHub for people who want their repositories to live on hardware they own. It serves two audiences at once: git clients speak the smart HTTP protocol against it (`git clone`, `git fetch`, `git push`), and browsers get a read-only web UI — repository list, file trees, blobs with line numbers, commit log, diffs, branches and tags. | |
| 15 | + | ||
| 16 | + | The access model is deliberately tiny. Anyone who can reach the server can browse and clone everything. Pushing requires a single owner token, sent as the password over HTTP basic auth. There are no accounts, no signup, no permissions matrix — one writer, the world as readers. | |
| 17 | + | ||
| 18 | + | It is built from fundamentals: the Go standard library and the `git` binary, nothing else. frieren owns HTTP, authentication, and rendering; the wire protocol and object storage are delegated to git's own `upload-pack` and `receive-pack` plumbing — the same architecture real forges use. | |
| 19 | + | ||
| 20 | + | ## Quick start | |
| 21 | + | ||
| 22 | + | Requires Go 1.22+ and git. | |
| 23 | + | ||
| 24 | + | ```sh | |
| 25 | + | make # builds and installs frieren to ~/.local/bin | |
| 26 | + | ||
| 27 | + | frieren token # generate an owner token, keep it secret | |
| 28 | + | FRIEREN_TOKEN=<token> frieren serve | |
| 29 | + | ``` | |
| 30 | + | ||
| 31 | + | The server listens on `:7420` and stores bare repositories under `./repos`. Publish a project to it: | |
| 32 | + | ||
| 33 | + | ```sh | |
| 34 | + | git remote add frieren http://localhost:7420/myproject.git | |
| 35 | + | git push frieren master | |
| 36 | + | ``` | |
| 37 | + | ||
| 38 | + | git asks for credentials: any username, the token as the password. A push to a repository that doesn't exist yet creates it — `frieren init <name> [description]` also works on the server. Then open http://localhost:7420 in a browser. | |
| 39 | + | ||
| 40 | + | To avoid retyping the token, let git store it once: `git config credential.helper osxkeychain` (macOS) or `git config credential.helper store` (Linux). | |
| 41 | + | ||
| 42 | + | ## Configuration | |
| 43 | + | ||
| 44 | + | Flags to `frieren serve`, each with an environment fallback: | |
| 45 | + | ||
| 46 | + | | Flag | Env | Default | | | |
| 47 | + | |---|---|---|---| | |
| 48 | + | | `-addr` | `FRIEREN_ADDR` | `:7420` | listen address | | |
| 49 | + | | `-root` | `FRIEREN_ROOT` | `./repos` | directory of bare repositories | | |
| 50 | + | | `-token` | `FRIEREN_TOKEN` | *(unset)* | owner token; without one the server is read-only for everyone | | |
| 51 | + | ||
| 52 | + | Repository descriptions shown in the UI come from the standard `description` file inside each bare repository. | |
| 53 | + | ||
| 54 | + | ## Deploying on your own machine | |
| 55 | + | ||
| 56 | + | Build for the target machine and copy the binary over — it's fully static: | |
| 57 | + | ||
| 58 | + | ```sh | |
| 59 | + | GOOS=linux GOARCH=amd64 go build -o frieren . | |
| 60 | + | ``` | |
| 61 | + | ||
| 62 | + | `deploy/frieren.service` is a hardened systemd unit (dedicated user, read-only filesystem except the repo root, token loaded from an env file its comments show how to create). `deploy/Caddyfile` puts automatic HTTPS in front: | |
| 63 | + | ||
| 64 | + | ``` | |
| 65 | + | git.example.com { | |
| 66 | + | reverse_proxy 127.0.0.1:7420 | |
| 67 | + | } | |
| 68 | + | ``` | |
| 69 | + | ||
| 70 | + | Point DNS at the machine, run Caddy, and `https://git.example.com` is your forge. If you'd rather not expose it publicly, run it inside a Tailscale network instead — every device of yours can reach it, nobody else can. | |
| 71 | + | ||
| 72 | + | Since this machine becomes the source of truth, back the repo root up somewhere else on a schedule, e.g. a nightly cron: | |
| 73 | + | ||
| 74 | + | ```sh | |
| 75 | + | rsync -a /srv/frieren/repos/ backup-host:frieren-repos/ | |
| 76 | + | ``` | |
| 77 | + | ||
| 78 | + | ## What it deliberately isn't | |
| 79 | + | ||
| 80 | + | No issues, no pull requests, no user accounts, no markdown rendering yet — it hosts and shows git repositories, and stops there. The single-writer model is the point: if you need collaborators with write access, you want a full forge like Forgejo. | |
| 81 | + | ||
| 82 | + | ## Development | |
| 83 | + | ||
| 84 | + | ```sh | |
| 85 | + | make test # end-to-end suite: real clone/push roundtrips against a live server | |
| 86 | + | make build # binary in dist/ | |
| 87 | + | ``` |
| @@ -0,0 +1,87 @@ | |||
| 1 | + | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 440" role="img" aria-label="frieren"> | |
| 2 | + | <style> | |
| 3 | + | @media (prefers-reduced-motion: no-preference) { | |
| 4 | + | .tw { animation: tw 3.6s ease-in-out infinite } | |
| 5 | + | .t1 { animation-delay: -0.6s } | |
| 6 | + | .t2 { animation-delay: -1.2s } | |
| 7 | + | .t3 { animation-delay: -1.8s } | |
| 8 | + | .t4 { animation-delay: -2.4s } | |
| 9 | + | .t5 { animation-delay: -3.0s } | |
| 10 | + | .glow { animation: tw 2.4s ease-in-out infinite } | |
| 11 | + | @keyframes tw { | |
| 12 | + | 0%, 100% { opacity: 1 } | |
| 13 | + | 50% { opacity: 0.3 } | |
| 14 | + | } | |
| 15 | + | } | |
| 16 | + | </style> | |
| 17 | + | <rect width="660" height="440" fill="#0C0C0F"/> | |
| 18 | + | ||
| 19 | + | <!-- constellation edges: a git graph drawn in the night sky --> | |
| 20 | + | <g stroke="#2E3644" stroke-width="2"> | |
| 21 | + | <line x1="305" y1="165" x2="385" y2="145"/> | |
| 22 | + | <line x1="385" y1="145" x2="465" y2="125"/> | |
| 23 | + | <line x1="465" y1="125" x2="545" y2="105"/> | |
| 24 | + | <line x1="385" y1="145" x2="445" y2="195"/> | |
| 25 | + | <line x1="445" y1="195" x2="525" y2="175"/> | |
| 26 | + | <line x1="525" y1="175" x2="545" y2="105"/> | |
| 27 | + | </g> | |
| 28 | + | ||
| 29 | + | <g shape-rendering="crispEdges"> | |
| 30 | + | <!-- commit stars: mainline in gold, a branch in frost, merging back --> | |
| 31 | + | <rect x="300" y="160" width="10" height="10" fill="#E8C170"/> | |
| 32 | + | <rect x="380" y="140" width="10" height="10" fill="#E8C170"/> | |
| 33 | + | <rect x="460" y="120" width="10" height="10" fill="#E8C170"/> | |
| 34 | + | <rect x="540" y="100" width="10" height="10" fill="#E8C170"/> | |
| 35 | + | <rect x="440" y="190" width="10" height="10" fill="#5EEAD4"/> | |
| 36 | + | <rect x="520" y="170" width="10" height="10" fill="#5EEAD4"/> | |
| 37 | + | ||
| 38 | + | <!-- faint field stars --> | |
| 39 | + | <rect class="tw" x="230" y="40" width="4" height="4" fill="#39404E"/> | |
| 40 | + | <rect class="tw t1" x="280" y="90" width="4" height="4" fill="#39404E"/> | |
| 41 | + | <rect class="tw t2" x="350" y="60" width="4" height="4" fill="#39404E"/> | |
| 42 | + | <rect class="tw t3" x="420" y="40" width="4" height="4" fill="#39404E"/> | |
| 43 | + | <rect class="tw t4" x="500" y="50" width="4" height="4" fill="#39404E"/> | |
| 44 | + | <rect class="tw t5" x="580" y="60" width="4" height="4" fill="#39404E"/> | |
| 45 | + | <rect class="tw t2" x="610" y="140" width="4" height="4" fill="#39404E"/> | |
| 46 | + | <rect class="tw t4" x="260" y="140" width="4" height="4" fill="#39404E"/> | |
| 47 | + | <rect class="tw t1" x="60" y="70" width="4" height="4" fill="#39404E"/> | |
| 48 | + | <rect class="tw t3" x="120" y="130" width="4" height="4" fill="#39404E"/> | |
| 49 | + | ||
| 50 | + | <!-- ground --> | |
| 51 | + | <rect x="0" y="380" width="660" height="60" fill="#13161C"/> | |
| 52 | + | <rect x="60" y="370" width="10" height="10" fill="#1C2F2B"/> | |
| 53 | + | <rect x="120" y="370" width="10" height="10" fill="#1C2F2B"/> | |
| 54 | + | <rect x="190" y="370" width="10" height="10" fill="#1C2F2B"/> | |
| 55 | + | <rect x="470" y="370" width="10" height="10" fill="#1C2F2B"/> | |
| 56 | + | <rect x="560" y="370" width="10" height="10" fill="#1C2F2B"/> | |
| 57 | + | ||
| 58 | + | <!-- the mage, watching the history hold --> | |
| 59 | + | <!-- hair --> | |
| 60 | + | <rect x="120" y="250" width="30" height="10" fill="#E6E9F2"/> | |
| 61 | + | <rect x="110" y="260" width="50" height="10" fill="#E6E9F2"/> | |
| 62 | + | <rect x="100" y="270" width="70" height="10" fill="#E6E9F2"/> | |
| 63 | + | <rect x="100" y="280" width="20" height="10" fill="#E6E9F2"/> | |
| 64 | + | <rect x="150" y="280" width="20" height="10" fill="#E6E9F2"/> | |
| 65 | + | <rect x="100" y="290" width="20" height="10" fill="#E6E9F2"/> | |
| 66 | + | <rect x="150" y="290" width="20" height="10" fill="#E6E9F2"/> | |
| 67 | + | <rect x="100" y="300" width="10" height="50" fill="#E6E9F2"/> | |
| 68 | + | <rect x="160" y="300" width="10" height="50" fill="#E6E9F2"/> | |
| 69 | + | <!-- face --> | |
| 70 | + | <rect x="120" y="280" width="10" height="10" fill="#3E8E7E"/> | |
| 71 | + | <rect x="130" y="280" width="10" height="10" fill="#F2D8C2"/> | |
| 72 | + | <rect x="140" y="280" width="10" height="10" fill="#3E8E7E"/> | |
| 73 | + | <rect x="120" y="290" width="30" height="10" fill="#F2D8C2"/> | |
| 74 | + | <!-- cloak --> | |
| 75 | + | <rect x="110" y="300" width="50" height="30" fill="#2E4A5C"/> | |
| 76 | + | <rect x="110" y="330" width="10" height="10" fill="#2E4A5C"/> | |
| 77 | + | <rect x="120" y="330" width="30" height="10" fill="#E8C170"/> | |
| 78 | + | <rect x="150" y="330" width="10" height="10" fill="#2E4A5C"/> | |
| 79 | + | <rect x="110" y="340" width="50" height="20" fill="#2E4A5C"/> | |
| 80 | + | <!-- boots --> | |
| 81 | + | <rect x="110" y="360" width="20" height="20" fill="#191D26"/> | |
| 82 | + | <rect x="140" y="360" width="20" height="20" fill="#191D26"/> | |
| 83 | + | <!-- staff --> | |
| 84 | + | <rect x="180" y="240" width="10" height="140" fill="#6B4A35"/> | |
| 85 | + | <rect class="glow" x="180" y="220" width="10" height="20" fill="#E8C170"/> | |
| 86 | + | </g> | |
| 87 | + | </svg> | |
| 0 | 88 |