ermite
A single-user Git forge in one Go binary. ermite hosts bare repositories, serves clone and push over HTTPS, and shows them in a read-only web interface.
Status: early development. ermite serve serves Git over HTTP (clone, fetch, push), answers GET /-/health, and serves the read-only web: the home list, the summary of a repository (README, clone URL, facts, recent commits) and its refs. Tree, file, commit and live pages are still to come.
Run
export ERMITE_DATA_DIR=/var/lib/ermite
export ERMITE_PUBLIC_URL=https://git.example.com
export ERMITE_ADMIN_TOKEN=$(head -c 32 /dev/urandom | base64)
ermite serve
| Variable |
Default |
Notes |
ERMITE_DATA_DIR |
required |
absolute path of the data root |
ERMITE_PUBLIC_URL |
required |
http(s) base URL, no path |
ERMITE_ADMIN_TOKEN |
required |
at least 32 characters |
ERMITE_ADDR |
:7341 |
listen address |
ERMITE_TRUSTED_PROXIES |
empty |
comma-separated CIDRs |
ERMITE_GIT |
git |
git 2.45 or newer |
ERMITE_SOURCE_URL |
$ERMITE_PUBLIC_URL/ermite |
link to the source (AGPL) |
ERMITE_LOG_LEVEL |
info |
trace, debug, info, warn, error |
ERMITE_LOG_FORMAT |
json |
json or console |
ermite version prints the version (and the server's when ERMITE_URL is set).
Remote CLI
The same binary drives a running server over its API. It reads ERMITE_URL (the server address, http:// or https://) and ERMITE_TOKEN (a bearer token; optional for public reads). A token sent over plain http:// to a host that is not loopback triggers a warning on every call.
export ERMITE_URL=https://git.example.com ERMITE_TOKEN=ermite_...
ermite repo list | show <name> | create <name> [--description …] [--visibility public] | edit <name> [--description …] [--visibility …] [--default-branch …] | delete <name> [--yes]
ermite token create <label> [--preset cli|agent|ci] [--scope repo:read …] [--repo name …] [--expires 30d] # prints the secret once
ermite token list | revoke <id>
ermite version # client and server, warns about missing server features
ermite watch [repo] [--json] # follows events, reconnects and resumes
--json on repo list|show|create|edit, token create|list and version prints stable field names, listed in each command's --help; watch --json prints one object per line. Errors are one line on standard error (ermite: …) and exit status 1. ermite completion bash|zsh|fish prints a shell completion script that completes repository names from the server.
Git over HTTP
git clone http://git.example.com/<repo>.git # public repositories need no credentials
git push http://u:<token>@git.example.com/<repo>.git main
Use the admin token (ERMITE_ADMIN_TOKEN) or a stored token as the password; the username is ignored. Store it in a Git credential helper rather than in the URL. Pushing to a new name creates a private repository. Repository names are lowercase, [a-z0-9._-]. TLS is terminated by your proxy; list its network in ERMITE_TRUSTED_PROXIES so that failed-login limits see the real client address (X-Forwarded-For).
The server keeps its runtime files in $ERMITE_DATA_DIR/run/ (the hook socket and scripts that git calls on each push); the hook socket path must stay under about 100 bytes.
Run with Docker
docker build --build-arg VERSION="$(git describe --tags --always --dirty)" -t ermite .
token=$(head -c 32 /dev/urandom | base64) # keep it: it is the root credential
docker run -d -p 127.0.0.1:7341:7341 -v ermite-data:/data \
-e ERMITE_PUBLIC_URL=http://localhost:7341 -e ERMITE_ADMIN_TOKEN="$token" ermite
The image is alpine with git, runs as uid 10001 and keeps its data in the /data volume. Logs are JSON by default. The build fetches bnema.dev/ermite-* from git.bnema.dev, so the host must reach it (NetBird); the build container uses the host's DNS and routes. Behind another route, pass --build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/ with --network host, as the CI job does.
Develop with compose
docker compose up
Serves on http://localhost:7341 with the development admin token in compose.yaml. The source is mounted and air (go tool air, .air.toml) rebuilds and restarts the server on each change to a .go or .templ file; the module and build caches and the data live in named volumes. docker compose down -v removes them. The dev container runs as root, which maps to your user under rootless Docker.
Build
go build ./cmd/ermite
go test ./...
Develop
Tools are pinned in go.mod:
go tool buf lint
go tool buf generate # proto/ -> gen/
go generate ./... # mockery mocks of the ports
go tool hexcheck -fail-on warn ./...
License
AGPL-3.0-or-later. See LICENSE.
source
1# ermite
2
3A single-user Git forge in one Go binary. ermite hosts bare repositories, serves clone and push over HTTPS, and shows them in a read-only web interface.
4
5Status: early development. `ermite serve` serves Git over HTTP (clone, fetch, push), answers `GET /-/health`, and serves the read-only web: the home list, the summary of a repository (README, clone URL, facts, recent commits) and its refs. Tree, file, commit and live pages are still to come.
6
7## Run
8
9```sh
10export ERMITE_DATA_DIR=/var/lib/ermite
11export ERMITE_PUBLIC_URL=https://git.example.com
12export ERMITE_ADMIN_TOKEN=$(head -c 32 /dev/urandom | base64)
13ermite serve
14```
15
16| Variable | Default | Notes |
17|---|---|---|
18| `ERMITE_DATA_DIR` | required | absolute path of the data root |
19| `ERMITE_PUBLIC_URL` | required | `http(s)` base URL, no path |
20| `ERMITE_ADMIN_TOKEN` | required | at least 32 characters |
21| `ERMITE_ADDR` | `:7341` | listen address |
22| `ERMITE_TRUSTED_PROXIES` | empty | comma-separated CIDRs |
23| `ERMITE_GIT` | `git` | git 2.45 or newer |
24| `ERMITE_SOURCE_URL` | `$ERMITE_PUBLIC_URL/ermite` | link to the source (AGPL) |
25| `ERMITE_LOG_LEVEL` | `info` | `trace`, `debug`, `info`, `warn`, `error` |
26| `ERMITE_LOG_FORMAT` | `json` | `json` or `console` |
27
28`ermite version` prints the version (and the server's when `ERMITE_URL` is set).
29
30## Remote CLI
31
32The same binary drives a running server over its API. It reads `ERMITE_URL` (the server address, `http://` or `https://`) and `ERMITE_TOKEN` (a bearer token; optional for public reads). A token sent over plain `http://` to a host that is not loopback triggers a warning on every call.
33
34```sh
35export ERMITE_URL=https://git.example.com ERMITE_TOKEN=ermite_...
36ermite repo list | show <name> | create <name> [--description …] [--visibility public] | edit <name> [--description …] [--visibility …] [--default-branch …] | delete <name> [--yes]
37ermite token create <label> [--preset cli|agent|ci] [--scope repo:read …] [--repo name …] [--expires 30d] # prints the secret once
38ermite token list | revoke <id>
39ermite version # client and server, warns about missing server features
40ermite watch [repo] [--json] # follows events, reconnects and resumes
41```
42
43`--json` on `repo list|show|create|edit`, `token create|list` and `version` prints stable field names, listed in each command's `--help`; `watch --json` prints one object per line. Errors are one line on standard error (`ermite: …`) and exit status 1. `ermite completion bash|zsh|fish` prints a shell completion script that completes repository names from the server.
44
45## Git over HTTP
46
47```sh
48git clone http://git.example.com/<repo>.git # public repositories need no credentials
49git push http://u:<token>@git.example.com/<repo>.git main
50```
51
52Use the admin token (`ERMITE_ADMIN_TOKEN`) or a stored token as the password; the username is ignored. Store it in a Git credential helper rather than in the URL. Pushing to a new name creates a private repository. Repository names are lowercase, `[a-z0-9._-]`. TLS is terminated by your proxy; list its network in `ERMITE_TRUSTED_PROXIES` so that failed-login limits see the real client address (`X-Forwarded-For`).
53
54The server keeps its runtime files in `$ERMITE_DATA_DIR/run/` (the hook socket and scripts that git calls on each push); the hook socket path must stay under about 100 bytes.
55
56## Run with Docker
57
58```sh
59docker build --build-arg VERSION="$(git describe --tags --always --dirty)" -t ermite .
60token=$(head -c 32 /dev/urandom | base64) # keep it: it is the root credential
61docker run -d -p 127.0.0.1:7341:7341 -v ermite-data:/data \
62 -e ERMITE_PUBLIC_URL=http://localhost:7341 -e ERMITE_ADMIN_TOKEN="$token" ermite
63```
64
65The image is alpine with git, runs as uid 10001 and keeps its data in the `/data` volume. Logs are JSON by default. The build fetches `bnema.dev/ermite-*` from `git.bnema.dev`, so the host must reach it (NetBird); the build container uses the host's DNS and routes. Behind another route, pass `--build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/` with `--network host`, as the CI job does.
66
67## Develop with compose
68
69```sh
70docker compose up
71```
72
73Serves on `http://localhost:7341` with the development admin token in `compose.yaml`. The source is mounted and [air](https://github.com/air-verse/air) (`go tool air`, `.air.toml`) rebuilds and restarts the server on each change to a `.go` or `.templ` file; the module and build caches and the data live in named volumes. `docker compose down -v` removes them. The dev container runs as root, which maps to your user under rootless Docker.
74
75## Build
76
77```sh
78go build ./cmd/ermite
79go test ./...
80```
81
82## Develop
83
84Tools are pinned in `go.mod`:
85
86```sh
87go tool buf lint
88go tool buf generate # proto/ -> gen/
89go generate ./... # mockery mocks of the ports
90go tool hexcheck -fail-on warn ./...
91```
92
93## License
94
95AGPL-3.0-or-later. See [LICENSE](LICENSE).