A Git platform in one Rust binary.

Repositories, merge requests with inline review, issues, wikis, releases, signed webhooks and a CI/CD engine. One process, one PostgreSQL, on your own server.

Open FerrisGit
$docker compose up -d --build

A single binary and a database. No account on a third-party service, no telemetry sent anywhere. See how it is made.

Illustration of one merge request in FerrisGit, from the push to the merge: a push over HTTPS creates the request, a reviewer suggests a change on a line, the author applies it in one click, the reviewer approves, the request is merged. The merge starts the pipeline of the repository: format and clippy run, then test, and all three succeed.

In figures

1
Rust binary serves the REST API, Git over HTTP and the web interface
1
PostgreSQL 18 holds every record; repositories live on disk
5
Cargo crates, in a hexagonal layout
173
REST routes under /api, each documented
3
sign-in factors (TOTP, passkeys, backup codes); MFA is mandatory
2
job engines: Docker runners or Kubernetes Pods

What you get, as the interface shows it

Each tile is a piece of the real product. The interface is in French for now; five languages are planned for 0.2.

src/webhooks/dispatch.rs:48 Open

CA

camille · 2 min

Exponential backoff, so a flapping endpoint stops being hammered.

Suggested change

Review on the diff

A comment stays attached to its line. A reviewer can write a suggestion that the author applies in one click, as a commit. Approvals are counted, conflicts are detected.

Pipelines on every push

After every push, FerrisGit reads the file on the default branch and creates a pipeline. Stages are barriers, needs are dependencies; jobs run in Docker containers through a small runner, or as Kubernetes Pods.

Issues, as a list or a board

Labels, milestones, assignees and four states. Drag a card to change its state, or use the card's menu from the keyboard.

Action Reader Contrib. Maint.
Browse, clone, read
Push, open issues and requests, review
Merge, release, settings, collaborators

Three roles, per repository or per group

A role granted on a group applies to everything below it. The highest role wins.

Signed webhooks

Twelve events on merge requests, issues, pipelines and collaborators. Each delivery carries an HMAC-SHA256 signature of its raw body; the secret is encrypted at rest.

Public pages, no account needed

Visitors browse the catalogue, README, files, commits and releases of public repositories, and clone them. An administrator switch turns the public pages off.

Multi-factor sign-in, for everyone

No account can skip it. Authenticator apps (TOTP), passkeys (WebAuthn) and ten single-use backup codes.

The full feature list

A push becomes a pipeline

Describe the jobs in .ferrisgit-ci.yml at the root of the repository. After every successful push, FerrisGit reads the file on the default branch and creates a pipeline. Flip the switch to see what the scheduler does when a job fails.

.ferrisgit-ci.yml
stages: [lint, test]

jobs:
  format:
    stage: lint
    image: rust:1
    script:
      - rustup component add rustfmt
      - cargo fmt --all -- --check

  clippy:
    stage: lint
    image: rust:1
    script:
      - rustup component add clippy
      - cargo clippy --all-targets -- -D warnings

  test:
    stage: test
    image: rust:1
    needs: [format, clippy]
    script:
      - cargo test --all-targets

Pipeline passed

lint

formatsucceeded
clippysucceeded

test

testsucceeded

format and clippy start together; test declares needs: [format, clippy] and starts once both have succeeded.

Two ways to run a job
Docker runners: a small ferrisgit-runner binary polls the server and runs each job in a container. Or Kubernetes: the server creates one Pod per job in your cluster.
Mistakes are shown
A file that is present but invalid creates a failed pipeline that carries the parser's message, shown in the interface.
Variables and caches
Per-repository CI variables, encrypted at rest with AES-256-GCM (Docker runners). Per-job caches with Kubernetes.

The .ferrisgit-ci.yml reference (in French)

Two minutes to self-host

FerrisGit is one container image with PostgreSQL next to it. Try it with Docker Compose, install the Helm chart on Kubernetes, or run it from the sources.

Docker Compose

Start the stack with Docker Compose
git clone https://github.com/Masmarino/FerrisGit.git
cd FerrisGit
cp .env.example .env
# set POSTGRES_PASSWORD, JWT_SECRET, SETTINGS_ENCRYPTION_KEY and the admin password in .env
docker compose up -d --build

Compose builds the image from the sources. To use the published image instead, replace build: . by image: masmarino/ferrisgit:0.1.1 in docker-compose.yml.

Installation guide (in French)

Helm chart

Install the chart on Kubernetes
git clone https://github.com/Masmarino/FerrisGit.git
cd FerrisGit
helm upgrade --install ferrisgit ./helm/ferrisgit \
  --namespace ferrisgit --create-namespace \
  --set image.tag=0.1.1 \
  --set ingress.host=git.example.com \
  --set-string ferrisgit.trustedProxyCidrs=10.42.0.0/16
kubectl -n ferrisgit rollout status deployment/ferrisgit

Left empty, the chart generates the database password, JWT_SECRET, SETTINGS_ENCRYPTION_KEY and the first administrator's password in the ferrisgit-secrets Secret. The release must be called ferrisgit.

Deploying on Kubernetes (in French)

From source

Run the backend and the web application locally
git clone https://github.com/Masmarino/FerrisGit.git
cd FerrisGit
cp .env.example .env          # then set real secrets
./scripts/dev.sh              # PostgreSQL via Compose, cargo run on :8080, ng serve on :4201

Needs rustup, Node.js 26, Docker with Compose and git on the PATH. The script starts PostgreSQL, applies the migrations and runs the backend on port 8080 and the Angular dev server on port 4201.

Installation guide (in French)

How it is made

One process, five crates, a hexagonal layout: the domain knows nothing of the database, the adapters know nothing of the use cases.

Architecture of a FerrisGit instanceIsometric drawing. A Git client and a browser talk to one Rust binary, ferrisgit-api, made of the api, application, domain and infrastructure crates. The binary stores its data in PostgreSQL and in Git repositories on disk, and runs pipeline jobs either through a ferrisgit-runner on a Docker host or as Pods in a Kubernetes cluster. An orange line follows a push to its jobs. Seven numbered circles lead to the tiles of the page.api application domain infrastructure ferrisgit-runner ferrisgit-api: one binary ferrisgit-runner Git client HTTPS, username + token no SSH yet Browser signed in, or anonymous on public pages api REST API, Git over HTTP, web interface domain entities and ports, no I/O application use cases infrastructure adapters: PostgreSQL, git, Docker, Kubernetes, SMTP, webhooks PostgreSQL accounts, issues, pipelines; migrations run at startup Git repositories on disk, wikis included Docker host the runner polls every 5 s by default, one job at a time, in a container Kubernetes cluster one Pod per job, created by the server git push pipeline created jobs polls for jobs

Fig. 1 One instance. The orange line follows a push from the client to the jobs it starts.

  • request or storage
  • a push, to its jobs
  • the runner polls
  • a numbered tile of this page
  1. ferrisgit-domainEntities, value objects and the ports the rest depends on. No I/O.
  2. ferrisgit-applicationThe use cases. Depends on the domain only, tested against in-memory fakes.
  3. ferrisgit-infrastructureAdapters: PostgreSQL (sqlx), Git (gix and the git binary), SMTP, Kubernetes, Argon2, AES-GCM, JWT.
  4. ferrisgit-apiThe axum server: REST API under /api, Git smart HTTP, the Angular bundle. This is the binary.
  5. ferrisgit-runnerThe stand-alone Docker job runner.

The source on GitHub

Security and your data

What protects an instance, named precisely. The data never leaves your server.

Multi-factor authentication is mandatory
An account with no factor enrolled is forced through setup before it can do anything else. TOTP, passkeys and backup codes.
Passwords hashed with Argon2
One salt per password. An unknown username costs as much work as a wrong password, and gets the same answer.
Secrets encrypted with AES-256-GCM
CI variables, webhook secrets, the SMTP password and TOTP secrets, under SETTINGS_ENCRYPTION_KEY.
Tokens for Git, never passwords
Git over HTTPS authenticates with a personal access token, stored hashed: the server cannot read it back.
An audit log
Failed sign-ins, refused Git access, password resets, promotions and MFA resets by an administrator are recorded as events.
No cookies, strict headers
Sessions are JWTs. Every response carries a Content-Security-Policy, X-Frame-Options: DENY, and the sign-in routes are rate-limited per IP.
Your data, on your server
PostgreSQL and a directory of repositories, both yours to back up. The image runs as an unprivileged user on a read-only filesystem.

The security page of the documentation (in French)

Where it is heading

Five versions, none shipped. Items are only ticked once they ship.

  1. 0.2Protected branches, checks, five languagesPlanned
  2. 0.3An analysis engine: secrets and dependenciesPlanned
  3. 0.4Quality gates and a dashboardPlanned
  4. 0.5Code quality: metrics, rules, coveragePlanned
  5. 0.6Security analysis and ArtiFerrisPlanned

Try it, then run your own.

The public instance shows the product. Your instance keeps your code.