Sam Jeston
History
| 1 | # Openhub |
| 2 | |
| 3 | Openhub is a minimal, self-hostable git server with a familiar web interface. Push over SSH, browse code and history, and review changes with pull requests. That is the whole product: no issue tracker, no CI, no wiki. |
| 4 | |
| 5 | It ships as a single Rust binary with SQLite for storage, so a whole installation is one executable plus one data directory. |
| 6 | |
| 7 |  |
| 8 | |
| 9 | ## Features |
| 10 | |
| 11 | - **Git over SSH**: a built-in SSH server authenticates `git push` and `git fetch` with the public keys users add in their settings. No system `sshd` configuration is needed. |
| 12 | - **Read-only git over HTTP**: anonymous clones of public repositories, and password-authenticated clones of private ones. |
| 13 | - **Code browsing**: file trees with the latest commit for each entry, syntax-highlighted files with linkable line numbers, rendered Markdown READMEs, raw files, and ZIP/tar.gz downloads. |
| 14 | - **History**: paginated commit logs (per branch, tag or path), commit pages with diffs, branches with ahead/behind counts, and tags. |
| 15 | - **Pull requests**: compare branches, open a pull request, discuss it in Markdown comments, and review the commits and the diff. Merge with a merge commit, a squash or a rebase, then delete the branch. Pull requests merged with a plain `git push` are detected automatically. |
| 16 | - **Accounts and access**: public and private repositories, plus collaborators with write access. The first account created is the site administrator. |
| 17 | - **Comfortable to use**: light and dark themes, works on phone-sized screens, and needs no JavaScript for core features. |
| 18 | |
| 19 | <table> |
| 20 | <tr> |
| 21 | <td><img src="docs/screenshots/pull-request.png" alt="Pull request conversation"></td> |
| 22 | <td><img src="docs/screenshots/diff-dark.png" alt="Diff view in the dark theme"></td> |
| 23 | </tr> |
| 24 | </table> |
| 25 | |
| 26 | ## Quick start |
| 27 | |
| 28 | ### With Docker |
| 29 | |
| 30 | ```sh |
| 31 | docker compose up -d |
| 32 | ``` |
| 33 | |
| 34 | Open <http://localhost:3000> and create an account; the first account becomes the administrator. Before deploying, edit `OPENHUB_PUBLIC_URL` in `docker-compose.yml` to the URL people will use. |
| 35 | |
| 36 | ### From source |
| 37 | |
| 38 | You need Rust 1.88 or newer, a C compiler (to build the bundled libgit2 and SQLite), and `git` installed at runtime. |
| 39 | |
| 40 | ```sh |
| 41 | cargo build --release |
| 42 | ./target/release/openhub --public-url http://localhost:3000 |
| 43 | ``` |
| 44 | |
| 45 | By default the web interface listens on port `3000` and the SSH server on port `2222`. Everything is stored in `./data`. |
| 46 | |
| 47 | ### On a Linux server with systemd |
| 48 | |
| 49 | `scripts/deploy-server.sh` installs Openhub on a Linux machine as a systemd service, over SSH. It copies your checkout to the machine, builds it there (installing Rust with rustup the first time) and runs `packaging/linux/install.sh`. The machine needs `git`, `curl` and a C compiler, and your SSH user needs `sudo`. |
| 50 | |
| 51 | ```sh |
| 52 | scripts/deploy-server.sh user@host |
| 53 | ``` |
| 54 | |
| 55 | The service runs as the `openhub` user, keeps its data in `/var/lib/openhub`, starts at boot and restarts whenever it stops. The web interface listens on `127.0.0.1:3000` for a reverse proxy or tunnel on the same machine. SSH listens on port `2222` on every interface. Put your settings in `/etc/default/openhub`, then run `sudo systemctl restart openhub`: |
| 56 | |
| 57 | ```sh |
| 58 | OPENHUB_PUBLIC_URL=https://git.example.com |
| 59 | OPENHUB_DISABLE_REGISTRATION=true |
| 60 | ``` |
| 61 | |
| 62 | To upgrade, run the same command again. See [Operations → Running under systemd](docs/operations.md#running-under-systemd). |
| 63 | |
| 64 | ## Pushing your first repository |
| 65 | |
| 66 | 1. Sign in and open **Settings → SSH keys**. Paste your public key, for example the contents of `~/.ssh/id_ed25519.pub`. |
| 67 | 2. Check that the server accepts the key: |
| 68 | ```sh |
| 69 | ssh -T -p 2222 git@your-host |
| 70 | # Hi alice! You've successfully authenticated, but Openhub does not provide shell access. |
| 71 | ``` |
| 72 | 3. Create a repository with **New**, then push to it: |
| 73 | ```sh |
| 74 | git remote add origin ssh://git@your-host:2222/alice/project.git |
| 75 | git push -u origin main |
| 76 | ``` |
| 77 | |
| 78 | When you push a branch other than the default branch, the server prints a link for opening a pull request. Once a pull request exists, the link points to it instead. |
| 79 | |
| 80 | ## Configuration |
| 81 | |
| 82 | Each option can be set as a command-line flag or as an environment variable. |
| 83 | |
| 84 | | Flag | Environment variable | Default | Description | |
| 85 | | --- | --- | --- | --- | |
| 86 | | `--data-dir` | `OPENHUB_DATA_DIR` | `./data` | Holds the database, repositories and SSH host key | |
| 87 | | `--http-addr` | `OPENHUB_HTTP_ADDR` | `0.0.0.0:3000` | Listen address for the web interface | |
| 88 | | `--ssh-addr` | `OPENHUB_SSH_ADDR` | `0.0.0.0:2222` | Listen address for the SSH server | |
| 89 | | `--public-url` | `OPENHUB_PUBLIC_URL` | `http://localhost:3000` | External URL of the web interface, used for links and clone URLs | |
| 90 | | `--ssh-host` | `OPENHUB_SSH_HOST` | host of the public URL | Host shown in SSH clone URLs | |
| 91 | | `--ssh-port` | `OPENHUB_SSH_PORT` | the SSH listen port | Port shown in SSH clone URLs | |
| 92 | | `--disable-registration` | `OPENHUB_DISABLE_REGISTRATION` | `false` | Turns off sign-ups (the first account can still be created) | |
| 93 | | `--git-bin` | `OPENHUB_GIT_BIN` | `git` | Path to the `git` executable | |
| 94 | |
| 95 | Logging is controlled with `RUST_LOG`, for example `RUST_LOG=openhub=debug`. |
| 96 | |
| 97 | ## Deployment notes |
| 98 | |
| 99 | - **HTTPS**: put Openhub behind a reverse proxy such as Caddy or nginx and set `OPENHUB_PUBLIC_URL=https://git.example.com`. Session cookies are marked `Secure` whenever the public URL uses HTTPS. SSH traffic goes straight to the SSH port, not through the proxy. |
| 100 | - **Public URL must match**: form submissions are only accepted from the origin in `OPENHUB_PUBLIC_URL`, or from one matching the request's `Host` header. If sign-in or other forms fail with "Cross-origin request rejected", check that the public URL matches exactly what appears in the browser's address bar, including scheme and port. |
| 101 | - **Using port 22 for SSH**: forward port 22 to Openhub's SSH port (for example `"22:2222"` in Docker) and set `OPENHUB_SSH_PORT=22`. Clone URLs then take the short form `[email protected]:alice/project.git`. If the host already runs its own `sshd` on port 22, keep Openhub on another port. |
| 102 | - **Host key**: the SSH host key is created on first start at `<data-dir>/ssh_host_ed25519_key`. Keep it with your backups so clients don't get host-key warnings after a restore. |
| 103 | - **Backups**: back up the whole data directory. To copy the database while the server is running, use `sqlite3 openhub.db ".backup backup.db"`; otherwise stop the server first. |
| 104 | - **Registration**: once everyone has an account, set `OPENHUB_DISABLE_REGISTRATION=true`. |
| 105 | |
| 106 | ## How it works |
| 107 | |
| 108 | ``` |
| 109 | browser ──▶ web UI (axum + askama) ──┐ |
| 110 | git (HTTP) ──▶ smart HTTP, read-only ──┼──▶ bare repositories (libgit2 + git) |
| 111 | git (SSH) ──▶ SSH server (russh) ──┘ |
| 112 | │ |
| 113 | └──▶ SQLite: users, SSH keys, repositories, pull requests |
| 114 | ``` |
| 115 | |
| 116 | - Repositories are stored as bare git repositories named by their database ID (`repos/42.git`), so renaming a repository only changes the database. |
| 117 | - Fetches and pushes run the standard `git upload-pack` and `git receive-pack`, so protocol support (including protocol v2) matches the installed git. |
| 118 | - After each push Openhub corrects `HEAD` if the default branch is missing, tracks the new tips of open pull requests, and marks pull requests whose commits reached the base branch as merged. |
| 119 | - Each pull request's head commit is kept at `refs/pull/<n>/head`, so its history survives after the branch is deleted. You can fetch it with `git fetch origin pull/<n>/head`. These refs cannot be pushed to. |
| 120 | - Web merges happen in memory with libgit2. The base branch is only updated if it hasn't moved since the merge started. |
| 121 | |
| 122 | ### Source layout |
| 123 | |
| 124 | | Path | Purpose | |
| 125 | | --- | --- | |
| 126 | | `src/main.rs` | Startup: configuration, database, web and SSH servers | |
| 127 | | `src/ssh.rs` | SSH server, key authentication, git command dispatch | |
| 128 | | `src/git.rs` | Repository access: refs, trees, history, diffs, merging | |
| 129 | | `src/sync.rs` | Post-push bookkeeping for HEAD and pull requests | |
| 130 | | `src/db.rs`, `migrations/` | SQLite schema and queries | |
| 131 | | `src/web/` | HTTP handlers: accounts, code browsing, pull requests, smart HTTP | |
| 132 | | `src/highlight.rs`, `src/markdown.rs` | Syntax highlighting and sanitised Markdown | |
| 133 | | `templates/`, `static/` | HTML templates, stylesheet and script (compiled into the binary) | |
| 134 | |
| 135 | ## Security |
| 136 | |
| 137 | - Passwords are hashed with Argon2id. Session tokens are random, and only their SHA-256 hashes are stored. |
| 138 | - Cookies are `HttpOnly` and `SameSite=Lax`, and cross-origin form posts are rejected. |
| 139 | - Pages are served with a strict Content-Security-Policy, and no inline scripts are used. |
| 140 | - Markdown (READMEs, pull requests and comments) is sanitised with `ammonia`. Raw files are served with a sandboxing CSP so they can never run script on the Openhub origin. |
| 141 | - The SSH server accepts only public-key authentication and runs only `git-upload-pack` and `git-receive-pack`, for repositories the key's owner can access. Shell sessions are refused. |
| 142 | - Private repositories return 404 to anyone without access. |
| 143 | |
| 144 | Openhub doesn't yet rate-limit sign-in attempts. If the server is exposed to the internet, add rate limiting at your reverse proxy. [docs/security.md](docs/security.md) lists the known gaps and hardening advice, including what to change before running an instance with open registration. |
| 145 | |
| 146 | ## Development |
| 147 | |
| 148 | ```sh |
| 149 | cargo test # unit tests |
| 150 | cargo clippy --all-targets # lints (kept warning-free) |
| 151 | scripts/smoke-test.sh # end-to-end: push, browse, pull requests, merges, permissions |
| 152 | cargo run -- --data-dir ./data |
| 153 | ``` |
| 154 | |
| 155 | The smoke test starts a throwaway instance on ports 38080 and 38022 (override with `HTTP_PORT` and `SSH_PORT`) and drives it with `curl`, `ssh`, `ssh-keygen` and `git`. |
| 156 | |
| 157 | How the system works, including the architecture, data model, permissions, git transport, pull request tracking, security model and operations, is documented in [`docs/`](docs/README.md). Coding conventions and known pitfalls for contributors are in [`CLAUDE.md`](CLAUDE.md), with template-specific notes in [`templates/CLAUDE.md`](templates/CLAUDE.md). |
| 158 | |
| 159 | ## Out of scope |
| 160 | |
| 161 | Openhub deliberately covers only git hosting. It has no issues, CI, wikis, releases, forks, organisations or line-level review comments. |
| 162 | |
| 163 | ## License |
| 164 | |
| 165 | MIT. See [LICENSE](LICENSE). |
| 166 | |
| 167 | Icons are from [Lucide](https://lucide.dev) (ISC License). Syntax definitions come from [syntect](https://github.com/trishume/syntect) and [two-face](https://github.com/CosmicHorrorDev/two-face). |