mugit
A simple web server for publishing git repositories
- Clone
git clone https://git.highenergymagic.org/mugit.git- Files
- browse the default branch
- Default branch
- master
- Last commit
- 2026-09-06
README.md raw
mugit
mugit is a small, read-only web front end for a directory of bare git repositories, with cloning over HTTPS. It is for the person who keeps their own git server, pushes to it over SSH, and wants the public parts of it to have a web page and a clone URL without running a forge.
What it does:
- lists the public repositories, with descriptions and last-commit dates;
- shows each repository’s README, rendered from Markdown, and lets visitors browse the files on the default branch;
- serves
git cloneandgit fetchover HTTP(S) through git’s own smart protocol; - obtains and renews its own Let’s Encrypt certificate, or runs plain HTTP behind a reverse proxy.
What it does not do:
- issues
- pull requests
- accounts
- pushing over HTTP
- anything else that writes. Repositories are managed with git itself, over SSH or on the machine.
How repositories are published
mugit serves a single directory, /srv/git by default. Each bare
repository in it is a subdirectory named <name>.git. A name is the part
before .git: letters, digits, ., _ and -, not starting with a dot
and not itself ending in .git.
A repository is public when it contains a file named
git-daemon-export-ok, the same convention git-daemon and
git-http-backend use. Everything else in the directory is invisible: a
private repository and a nonexistent one both answer with the same 404,
so the site never confirms which private names exist.
cd /srv/git
git init --bare widget.git
echo "A widget for widgeting" > widget.git/description
touch widget.git/git-daemon-export-ok
The description file, which git creates in every repository, is shown
on the index and the repository page. The README is looked for at the
root of the default branch, under the names README.md,
README.markdown, README and README.txt, in that order of
preference. Relative links and images in a Markdown README resolve into
the site’s own file and raw views.
Bear in mind that publishing a repository publishes all of it: every branch, tag and commit that a clone would fetch, not just the default branch the web pages show.
Building
mugit is a single Rust binary. It needs a Rust toolchain of 1.88 or
later, and the git command on the PATH at runtime for clones, fetches
and raw file downloads (everything else uses a bundled libgit2).
cargo build --release
The binary is target/release/mugit. The stylesheet and favicon are
embedded in it, so it has no files to install alongside it.
Running
Every option is a command line flag or an environment variable of the
same name with a MUGIT_ prefix; mugit --help lists them all with
their defaults. The ones that matter:
| Flag | Environment | Default | Meaning |
|---|---|---|---|
--repos | MUGIT_REPOS | /srv/git | The directory of bare repositories |
--listen | MUGIT_LISTEN | 0.0.0.0:8000 | Address for plain HTTP |
--tls-domain | MUGIT_TLS_DOMAIN | Enables HTTPS with a Let’s Encrypt certificate for this domain | |
--tls-listen | MUGIT_TLS_LISTEN | 0.0.0.0:443 | Address for HTTPS |
--acme-cache | MUGIT_ACME_CACHE | /var/lib/mugit/acme | Where the ACME account key and certificates are kept |
--acme-contact | MUGIT_ACME_CONTACT | Contact email registered with the certificate authority | |
--acme-staging | MUGIT_ACME_STAGING | off | Use the Let’s Encrypt staging environment while testing |
--site-name | MUGIT_SITE_NAME | mugit | Name shown in page headers and titles |
--public-url | MUGIT_PUBLIC_URL | Base URL that clone URLs are built from | |
--page-size | MUGIT_PAGE_SIZE | 50 | Repositories per page of the index |
--git-processes | MUGIT_GIT_PROCESSES | 8 | Git processes serving clones, fetches and raw files at once |
--git-idle-timeout | MUGIT_GIT_IDLE_TIMEOUT | 60 | Seconds a stalled clone or download is tolerated before it is dropped |
--http-idle-timeout | MUGIT_HTTP_IDLE_TIMEOUT | 300 | Seconds a silent connection is held before it is closed; must exceed twice --git-idle-timeout |
Logging goes to standard error. MUGIT_LOG takes a
tracing filter
such as debug or mugit=debug,warn; a release build logs warnings and
errors by default.
Standalone with HTTPS
Give mugit a domain and it takes care of the certificate itself, using the TLS-ALPN-01 challenge. That challenge is answered on port 443, so the HTTPS listener must be reachable from the internet on that port, and the plain listener then does nothing but redirect to HTTPS.
mugit --tls-domain git.example.org --acme-contact you@example.org \
--listen [::]:80 --tls-listen [::]:443
Clone URLs are built from https://<domain> unless --public-url says
otherwise. Try --acme-staging first if you are unsure of the setup:
Let’s Encrypt rate-limits failed attempts against production.
Behind a reverse proxy
Without a TLS domain, mugit speaks plain HTTP on the listen address and
leaves TLS to the proxy in front of it. Set --public-url to the address
visitors use, so that the clone URL shown on each page is right; without
it, mugit falls back to the scheme and Host header of each request.
mugit --listen 127.0.0.1:8000 --public-url https://git.example.org
The proxy should pass requests through unbuffered: git’s smart protocol streams in both directions, and buffering a whole fetch would defeat the bound mugit keeps on memory.
Deploying as a service
A worked example for a Linux host with systemd, where repositories belong
to a git user that people push to over SSH, and mugit runs as its own
user with read-only access to them. Adjust paths and names to taste.
-
Create the service user and give it read access to the repositories through the
gitgroup. It owns nothing under the repository directory.useradd --system --home-dir /var/lib/mugit --shell /usr/sbin/nologin mugit usermod -aG git mugit chmod 750 /srv/gitRepositories created by the
gituser need to be group-readable, which is git’s default; check withls -l /srv/git/*.gitif in doubt. -
Create the state directory for the ACME account key and certificates, and install the binary.
install -d -m 750 -o mugit -g mugit /var/lib/mugit /var/lib/mugit/acme install -d -m 755 /opt/mugit install -m 755 target/release/mugit /opt/mugit/mugit -
Install the unit from
deploy/mugit.service. It runs the binary from/opt/mugit, serves/srv/giton ports 80 and 443, and hardens the process; everything specific to your site goes in/etc/mugit/env:install -m 644 deploy/mugit.service /etc/systemd/system/mugit.service install -d -m 755 /etc/mugit cat > /etc/mugit/env <<EOT MUGIT_TLS_DOMAIN=git.example.org MUGIT_ACME_CONTACT=you@example.org MUGIT_SITE_NAME=git.example.org EOT systemctl daemon-reload systemctl enable --now mugitFor a reverse proxy setup, put
MUGIT_LISTEN=127.0.0.1:8000andMUGIT_PUBLIC_URL=https://git.example.orgin the file instead of the TLS domain. -
Watch the journal for the first certificate:
journalctl -u mugit -f
To upgrade, install the new binary over the old one and
systemctl restart mugit.
Security notes
The visibility rule is the marker file and nothing else, so review a repository’s history before publishing it. The web side sanitises rendered Markdown and sends every page with a Content Security Policy that forbids script, and serves raw files with a sandbox policy and types that browsers will not execute, so a hosted file cannot run on the site’s origin. Git processes are bounded in number and dropped when their client stalls, which is the protection against clients that open connections and never finish. There is no rate limiting beyond that; put a proxy in front if you need it.
Development
cargo test runs the unit tests and the integration tests under
tests/, which build temporary repositories and drive the site through
its HTTP API; one of them runs a real git clone against a live
listener. A local store for trying the site by hand is just a directory
of bare repositories:
mkdir -p local/repos
git clone --bare some/project local/repos/project.git
touch local/repos/project.git/git-daemon-export-ok
cargo run -- --repos local/repos --listen 127.0.0.1:8000
License
This is free and unencumbered software released into the public domain under the Unlicense.