| 1 | # Ferrenium |
| 2 | |
| 3 | A read-only Git repository browser, like cgit, as a single Rust daemon. It |
| 4 | serves every bare repository under one directory and keeps no state on disk. |
| 5 | |
| 6 | Pages: repo index, summary + README, tree, file (highlighted), raw, log |
| 7 | (path filter, message search), commit + diff, `.patch`, blame, refs, file |
| 8 | finder, `.tar.gz`/`.zip` snapshots, Atom feed. `git clone` over HTTPS |
| 9 | (read-only). |
| 10 | |
| 11 | Themeable without rebuilding: your own stylesheet, images and fonts, plus a |
| 12 | favicon, logo and accent color (see [Appearance](#appearance)). |
| 13 | |
| 14 | ## Install |
| 15 | |
| 16 | On a Linux host with systemd (x86_64 or aarch64, glibc 2.28+): |
| 17 | |
| 18 | ```sh |
| 19 | curl -fsSL https://github.com/krolyxon/ferrenium/releases/latest/download/install.sh | sudo FERRENIUM_DOMAIN=git.example.com sh |
| 20 | ``` |
| 21 | |
| 22 | This: |
| 23 | |
| 24 | - downloads the latest release binary, verifies its checksum and installs it to `/usr/local/bin/ferrenium` |
| 25 | - creates the `git` user if it doesn't exist (home `/home/git`, shell `git-shell`) |
| 26 | - writes `/etc/ferrenium/ferrenium.toml` (never overwritten on later runs) |
| 27 | - installs `/etc/systemd/system/ferrenium.service` and runs `systemctl enable --now ferrenium` |
| 28 | |
| 29 | Variables: `FERRENIUM_DOMAIN` (fills `base_url` and clone URLs in a new |
| 30 | config), `FERRENIUM_USER` (default `git`), `FERRENIUM_VERSION` (e.g. |
| 31 | `v0.1.0`, default latest). |
| 32 | |
| 33 | Run the same command again to upgrade: it replaces the binary and unit |
| 34 | and restarts the service. To remove it: `… | sudo sh -s -- --uninstall`. |
| 35 | This keeps `/etc/ferrenium` and the repositories. |
| 36 | |
| 37 | ### The service |
| 38 | |
| 39 | It's a normal system service. The unit has `User=git`, so systemd starts it |
| 40 | as the repo owner. You don't need `su git` or `systemctl --user`. |
| 41 | |
| 42 | ```sh |
| 43 | sudo systemctl status ferrenium |
| 44 | sudo systemctl reload ferrenium # after editing the config |
| 45 | sudo journalctl -u ferrenium -f |
| 46 | sudo systemctl edit ferrenium # overrides; survive upgrades |
| 47 | ``` |
| 48 | |
| 49 | The unit is sandboxed: the filesystem is read-only, there are no |
| 50 | capabilities, a syscall filter applies, and only localhost may connect. |
| 51 | |
| 52 | ### Reverse proxy |
| 53 | |
| 54 | Ferrenium listens on `127.0.0.1:8080` (plain HTTP/1.1). Caddy: |
| 55 | |
| 56 | ```caddy |
| 57 | git.example.com { |
| 58 | reverse_proxy 127.0.0.1:8080 |
| 59 | } |
| 60 | ``` |
| 61 | |
| 62 | If Caddy runs in Docker, use `network_mode: host` so it can reach |
| 63 | `127.0.0.1`. |
| 64 | |
| 65 | ### Repositories |
| 66 | |
| 67 | Put bare repositories anywhere under `/home/git` (up to 3 levels deep). |
| 68 | Ferrenium picks up new repos and pushes within about a second. |
| 69 | |
| 70 | ```sh |
| 71 | sudo -u git git init --bare /home/git/project.git |
| 72 | git push git@git.example.com:project.git main # after adding your key to /home/git/.ssh/authorized_keys |
| 73 | ``` |
| 74 | |
| 75 | ## Configuration |
| 76 | |
| 77 | Edit `/etc/ferrenium/ferrenium.toml`. Every key is optional and unknown |
| 78 | keys are errors. The file lists all keys with their defaults |
| 79 | (`ferrenium example config` prints the same). To validate the config and |
| 80 | list the repositories it finds: |
| 81 | |
| 82 | ```sh |
| 83 | ferrenium check --config /etc/ferrenium/ferrenium.toml |
| 84 | ``` |
| 85 | |
| 86 | `listen`, `base_path`, `compression`, `cache.size_mb` and the `limits.*_parallel`/`max_requests` |
| 87 | settings need a restart; everything else applies on reload. |
| 88 | |
| 89 | Per-repository metadata comes from the first source that sets it: |
| 90 | |
| 91 | 1. `[[repo]]` in the config (`name`, `description`, `section`, `owner`, `hidden`, `readme`, `default_branch`) |
| 92 | 2. the repo's git config: `ferrenium.{description,owner,section,hidden,readme}`, or the gitweb/cgit keys |
| 93 | 3. the repo's `description` file |
| 94 | |
| 95 | ```sh |
| 96 | sudo -u git git -C /home/git/secret.git config ferrenium.hidden true |
| 97 | ``` |
| 98 | |
| 99 | ### Appearance |
| 100 | |
| 101 | These keys are optional. Files are read when the config is loaded or reloaded, |
| 102 | and relative paths are resolved against the config file's directory, e.g. |
| 103 | `/etc/ferrenium/`. |
| 104 | |
| 105 | | Key | | |
| 106 | |---|---| |
| 107 | | `site.title` | name in the top bar and page titles | |
| 108 | | `site.favicon` | `.ico`, `.png`, `.svg`, `.webp`, `.gif` or `.jpg`, up to 1 MiB | |
| 109 | | `site.logo` | image shown before the title in the top bar (same formats) | |
| 110 | | `site.custom_css` | stylesheet loaded after the built-in one | |
| 111 | | `site.assets_dir` | directory of images, fonts and stylesheets the custom CSS can load | |
| 112 | | `site.header` / `site.footer` | HTML inserted below the top bar / in the footer | |
| 113 | | `ui.theme` | default theme: `auto`, `dark` or `light` | |
| 114 | | `ui.accent` | accent color as hex, e.g. `"#d71921"` | |
| 115 | | `ui.code_font` | font family for code (must be installed on the client or loaded by `custom_css`) | |
| 116 | |
| 117 | The custom stylesheet overrides the built-in one, so it only needs the rules |
| 118 | you want to change: design tokens, buttons, backgrounds, layout. Files in |
| 119 | `assets_dir` are served at `/-/site/` (images, fonts and `.css` only; hidden |
| 120 | files and paths leading outside the directory are refused), and the |
| 121 | stylesheet refers to them with relative URLs: |
| 122 | |
| 123 | ```toml |
| 124 | [site] |
| 125 | custom_css = "style.css" # /etc/ferrenium/style.css |
| 126 | assets_dir = "assets" # /etc/ferrenium/assets/ |
| 127 | ``` |
| 128 | |
| 129 | ```css |
| 130 | @font-face { font-family: "Inter"; src: url("site/inter.woff2") format("woff2"); } |
| 131 | :root { --font-body: "Inter", sans-serif; --interactive: light-dark(#0b57d0, #8ab4f8); } |
| 132 | body { background: url("site/bg.jpg") center / cover fixed; } |
| 133 | .segmented { border-radius: 4px; } |
| 134 | ``` |
| 135 | |
| 136 | Edits to the stylesheet, favicon and logo need `systemctl reload ferrenium`; |
| 137 | files in `assets_dir` are read on each request. Images from other `https://` |
| 138 | sites work too, but fonts must come from `assets_dir` (the content security |
| 139 | policy allows only same-origin fonts). Inline `<script>`/`<style>` in |
| 140 | `header`/`footer` is blocked for the same reason; put styles in `custom_css`. |
| 141 | |
| 142 | Most of the look is driven by these CSS variables, so overriding a few of |
| 143 | them on `:root` goes a long way. Colors are written as |
| 144 | `light-dark(LIGHT, DARK)` to follow the theme switch; a single color applies |
| 145 | to both themes. |
| 146 | |
| 147 | | Variable | Used for | |
| 148 | |---|---| |
| 149 | | `--black` | page background | |
| 150 | | `--surface`, `--surface-raised` | panels, menus, code blocks | |
| 151 | | `--border`, `--border-visible` | hairlines, button and field outlines | |
| 152 | | `--text-display`, `--text-primary`, `--text-secondary`, `--text-disabled` | text, brightest to dimmest; syntax highlighting uses the same scale | |
| 153 | | `--accent`, `--accent-subtle` | active markers, highlights and removed lines in diffs (`ui.accent` sets both) | |
| 154 | | `--interactive` | links | |
| 155 | | `--success` | added lines in diffs | |
| 156 | | `--error` | `[ERROR: …]` messages | |
| 157 | | `--font-body`, `--font-mono`, `--font-display`, `--font-code` | type families | |
| 158 | | `--page-max`, `--gutter` | column width and side padding | |
| 159 | |
| 160 | The full list is in [`assets/css/tokens.css`](assets/css/tokens.css). For |
| 161 | anything else, use the browser's inspector to find the element's class and |
| 162 | override its rules. |
| 163 | |
| 164 | ### Limits |
| 165 | |
| 166 | Defaults are sized for a small VPS: |
| 167 | |
| 168 | | Key | Default | | |
| 169 | |---|---|---| |
| 170 | | `limits.git_parallel` | CPUs, max 8 | concurrent git work | |
| 171 | | `limits.heavy_parallel` | 2 | concurrent blame / archive / clone / log search | |
| 172 | | `limits.heavy_wait_secs` | 10 | queue time for those before `503` | |
| 173 | | `limits.max_requests` | 512 | in-flight requests before `503` | |
| 174 | | `limits.request_timeout_secs` | 30 | until the response starts | |
| 175 | | `limits.diff_files` / `diff_lines` | 300 / 10000 | larger diffs are truncated | |
| 176 | | `ui.max_blob_kb` / `max_highlight_kb` | 1024 / 256 | larger files: raw link / no highlighting | |
| 177 | | `cache.size_mb` | 64 | in-memory cache | |
| 178 | |
| 179 | `/robots.txt` keeps crawlers off the expensive pages. Memory use at idle is |
| 180 | about 10 MB. |
| 181 | |
| 182 | ## Building from source |
| 183 | |
| 184 | Requires Rust 1.89+. |
| 185 | |
| 186 | ```sh |
| 187 | cargo build --release # target/release/ferrenium |
| 188 | ferrenium example config|systemd|caddy |
| 189 | ``` |
| 190 | |
| 191 | ## Development |
| 192 | |
| 193 | ```sh |
| 194 | cargo test # needs the git CLI |
| 195 | cargo test --release |
| 196 | cargo clippy --all-targets -- -D warnings |
| 197 | cargo run -- serve --config dev.toml |
| 198 | tools/gen-languages/gen.py # regenerate the Linguist language table |
| 199 | ``` |
| 200 | |
| 201 | To release: bump `version` in `Cargo.toml`, commit, then push a `vX.Y.Z` tag. |
| 202 | The `release` workflow tests, builds x86_64 and aarch64 binaries, and |
| 203 | publishes them with `install.sh` and `SHA256SUMS`. |
| 204 | |
| 205 | ## License |
| 206 | |
| 207 | GPL-3.0 (see `LICENSE`). Bundled fonts (Doto, Space Grotesk, Space Mono) |
| 208 | are licensed under the OFL; see `assets/fonts/OFL.txt`. Icons come from |
| 209 | Lucide (ISC license). |