frieren

~ / 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 15:11 (6h ago)

 README.md          | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 assets/frieren.svg | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 174 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..85e03aa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,87 @@
+<div align="center">
+
+<img src="assets/frieren.svg" alt="frieren" width="460" />
+
+# frieren
+
+**A self-hosted git server in one binary — everyone can read, only you can write.**<br>
+*Your code, on your machine, outliving every platform.*
+
+</div>
+
+---
+
+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.
+
+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.
+
+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.
+
+## Quick start
+
+Requires Go 1.22+ and git.
+
+```sh
+make                       # builds and installs frieren to ~/.local/bin
+
+frieren token              # generate an owner token, keep it secret
+FRIEREN_TOKEN=<token> frieren serve
+```
+
+The server listens on `:7420` and stores bare repositories under `./repos`. Publish a project to it:
+
+```sh
+git remote add frieren http://localhost:7420/myproject.git
+git push frieren master
+```
+
+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.
+
+To avoid retyping the token, let git store it once: `git config credential.helper osxkeychain` (macOS) or `git config credential.helper store` (Linux).
+
+## Configuration
+
+Flags to `frieren serve`, each with an environment fallback:
+
+| Flag | Env | Default | |
+|---|---|---|---|
+| `-addr` | `FRIEREN_ADDR` | `:7420` | listen address |
+| `-root` | `FRIEREN_ROOT` | `./repos` | directory of bare repositories |
+| `-token` | `FRIEREN_TOKEN` | *(unset)* | owner token; without one the server is read-only for everyone |
+
+Repository descriptions shown in the UI come from the standard `description` file inside each bare repository.
+
+## Deploying on your own machine
+
+Build for the target machine and copy the binary over — it's fully static:
+
+```sh
+GOOS=linux GOARCH=amd64 go build -o frieren .
+```
+
+`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:
+
+```
+git.example.com {
+	reverse_proxy 127.0.0.1:7420
+}
+```
+
+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.
+
+Since this machine becomes the source of truth, back the repo root up somewhere else on a schedule, e.g. a nightly cron:
+
+```sh
+rsync -a /srv/frieren/repos/ backup-host:frieren-repos/
+```
+
+## What it deliberately isn't
+
+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.
+
+## Development
+
+```sh
+make test    # end-to-end suite: real clone/push roundtrips against a live server
+make build   # binary in dist/
+```
diff --git a/assets/frieren.svg b/assets/frieren.svg
new file mode 100644
index 0000000..1de956b
--- /dev/null
+++ b/assets/frieren.svg
@@ -0,0 +1,87 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 440" role="img" aria-label="frieren">
+  <style>
+    @media (prefers-reduced-motion: no-preference) {
+      .tw { animation: tw 3.6s ease-in-out infinite }
+      .t1 { animation-delay: -0.6s }
+      .t2 { animation-delay: -1.2s }
+      .t3 { animation-delay: -1.8s }
+      .t4 { animation-delay: -2.4s }
+      .t5 { animation-delay: -3.0s }
+      .glow { animation: tw 2.4s ease-in-out infinite }
+      @keyframes tw {
+        0%, 100% { opacity: 1 }
+        50% { opacity: 0.3 }
+      }
+    }
+  </style>
+  <rect width="660" height="440" fill="#0C0C0F"/>
+
+  <!-- constellation edges: a git graph drawn in the night sky -->
+  <g stroke="#2E3644" stroke-width="2">
+    <line x1="305" y1="165" x2="385" y2="145"/>
+    <line x1="385" y1="145" x2="465" y2="125"/>
+    <line x1="465" y1="125" x2="545" y2="105"/>
+    <line x1="385" y1="145" x2="445" y2="195"/>
+    <line x1="445" y1="195" x2="525" y2="175"/>
+    <line x1="525" y1="175" x2="545" y2="105"/>
+  </g>
+
+  <g shape-rendering="crispEdges">
+    <!-- commit stars: mainline in gold, a branch in frost, merging back -->
+    <rect x="300" y="160" width="10" height="10" fill="#E8C170"/>
+    <rect x="380" y="140" width="10" height="10" fill="#E8C170"/>
+    <rect x="460" y="120" width="10" height="10" fill="#E8C170"/>
+    <rect x="540" y="100" width="10" height="10" fill="#E8C170"/>
+    <rect x="440" y="190" width="10" height="10" fill="#5EEAD4"/>
+    <rect x="520" y="170" width="10" height="10" fill="#5EEAD4"/>
+
+    <!-- faint field stars -->
+    <rect class="tw"    x="230" y="40"  width="4" height="4" fill="#39404E"/>
+    <rect class="tw t1" x="280" y="90"  width="4" height="4" fill="#39404E"/>
+    <rect class="tw t2" x="350" y="60"  width="4" height="4" fill="#39404E"/>
+    <rect class="tw t3" x="420" y="40"  width="4" height="4" fill="#39404E"/>
+    <rect class="tw t4" x="500" y="50"  width="4" height="4" fill="#39404E"/>
+    <rect class="tw t5" x="580" y="60"  width="4" height="4" fill="#39404E"/>
+    <rect class="tw t2" x="610" y="140" width="4" height="4" fill="#39404E"/>
+    <rect class="tw t4" x="260" y="140" width="4" height="4" fill="#39404E"/>
+    <rect class="tw t1" x="60"  y="70"  width="4" height="4" fill="#39404E"/>
+    <rect class="tw t3" x="120" y="130" width="4" height="4" fill="#39404E"/>
+
+    <!-- ground -->
+    <rect x="0" y="380" width="660" height="60" fill="#13161C"/>
+    <rect x="60"  y="370" width="10" height="10" fill="#1C2F2B"/>
+    <rect x="120" y="370" width="10" height="10" fill="#1C2F2B"/>
+    <rect x="190" y="370" width="10" height="10" fill="#1C2F2B"/>
+    <rect x="470" y="370" width="10" height="10" fill="#1C2F2B"/>
+    <rect x="560" y="370" width="10" height="10" fill="#1C2F2B"/>
+
+    <!-- the mage, watching the history hold -->
+    <!-- hair -->
+    <rect x="120" y="250" width="30" height="10" fill="#E6E9F2"/>
+    <rect x="110" y="260" width="50" height="10" fill="#E6E9F2"/>
+    <rect x="100" y="270" width="70" height="10" fill="#E6E9F2"/>
+    <rect x="100" y="280" width="20" height="10" fill="#E6E9F2"/>
+    <rect x="150" y="280" width="20" height="10" fill="#E6E9F2"/>
+    <rect x="100" y="290" width="20" height="10" fill="#E6E9F2"/>
+    <rect x="150" y="290" width="20" height="10" fill="#E6E9F2"/>
+    <rect x="100" y="300" width="10" height="50" fill="#E6E9F2"/>
+    <rect x="160" y="300" width="10" height="50" fill="#E6E9F2"/>
+    <!-- face -->
+    <rect x="120" y="280" width="10" height="10" fill="#3E8E7E"/>
+    <rect x="130" y="280" width="10" height="10" fill="#F2D8C2"/>
+    <rect x="140" y="280" width="10" height="10" fill="#3E8E7E"/>
+    <rect x="120" y="290" width="30" height="10" fill="#F2D8C2"/>
+    <!-- cloak -->
+    <rect x="110" y="300" width="50" height="30" fill="#2E4A5C"/>
+    <rect x="110" y="330" width="10" height="10" fill="#2E4A5C"/>
+    <rect x="120" y="330" width="30" height="10" fill="#E8C170"/>
+    <rect x="150" y="330" width="10" height="10" fill="#2E4A5C"/>
+    <rect x="110" y="340" width="50" height="20" fill="#2E4A5C"/>
+    <!-- boots -->
+    <rect x="110" y="360" width="20" height="20" fill="#191D26"/>
+    <rect x="140" y="360" width="20" height="20" fill="#191D26"/>
+    <!-- staff -->
+    <rect x="180" y="240" width="10" height="140" fill="#6B4A35"/>
+    <rect class="glow" x="180" y="220" width="10" height="20" fill="#E8C170"/>
+  </g>
+</svg>