remuscodes.dk rits

self-hosted git forge · single binary each side

No HTTP.
No YAML.
Just SSH.

rits is a headless git server. Clone, push, browse history, enroll build machines, and run CI — everything over one SSH port, driven from a terminal UI instead of a web app.

git clone git@myserver:demo.git
rits-cli --remote myserver
rits — history
 history  Commits @ main  Branches  Jobs

 8216de0 2026-08-19 remus  commit 8216de0a3bfc4d8a33d46
 e34cfe0 2026-08-18 remus  │ Author: remus
 50cbe45 2026-08-18 ada    │ Date:   2026-08-19 10:27
 12a27bf 2026-08-18 ada    │  Dockerfile.push@main j85c9ea87
 91ee5f9 2026-08-18 remus  │  Dockerfile.tag@v*    jb6ad7a77
  fc289bf 2026-08-18 ada    │
  1bf42a3 2026-08-18 ada    │ ci: enable release checks

450 of 103,412 commits loaded • ↑/↓ select • b branches • esc back

one protocol

The forge is an SSH port.

Other platforms bolt a web application onto git. rits goes the other way: the git wire protocol is the whole interface. Humans authenticate as git with their public keys, build machines as runner — and every feature, from creating a repo to streaming a build log, is an SSH command on port 2222.

operate
One static binary, any setup — systemd on a VPS, k8s, a bare Raspberry Pi, or the bundled docker compose that runs server and example runner together. The only host dependency is git itself.
$ rits-server --root /srv/repos INF serving git over SSH addr=[::]:2222
work
Stock git speaks to it natively — clone, push, pull, fetch, protocol v2. The rits TUI browses repos, pages 100K-commit histories lazily, and creates repos with one keystroke.
$ rits-cli create demo created demo $ git push origin main
enroll
Runners live on your own machines — a VM, a Pi. A one-time token (its hash is all the server keeps) enrolls the runner's own keypair; from then on it holds a persistent channel and jobs stream down it.
$ rits-runner enroll --token ritsr_9f2… enrolled as pi-runner

ci without yaml

The pipeline is a Dockerfile.
The filename is the trigger.

Drop Dockerfile.push@main into .hooks/ and every push to main builds it. The build is the job: its exit code is the verdict, its output is the log, its image is the artifact. No pipeline DSL to learn — you already know this file format.

.hooks/Dockerfile.push@main

vim
FROM golang:1.25 AS build
LABEL rits.runner="docker"
WORKDIR /src
COPY . .
RUN go vet ./... && go test ./...
RUN CGO_ENABLED=0 go build -o /out/app .

FROM alpine:3.22
COPY --from=build /out/app /usr/local/bin/app

what a push sets in motion

rits — jobs
$ git push origin main

j85c9ea87  ✓ success  push  main   push@main
jb6ad7a77  ✗ failed   tag   v2.0   tag@v*
j76d6ef0c  ● running  push  main   push@main
j9c1330a2  ○ queued   push  feat…  push@feature*

artifact: rits/demo/push-main:8216de0
4 jobs • enter view log • r refresh

No credentials in builds

The runner ships the pushed commit as the build context with git archive — jobs never clone, so no key or token ever enters a build container.

Status lives in git

Verdicts are written as git notes under refs/notes/rits-ci — the ✓/✗ next to each commit travels with the repo and works offline in plain git log.

Queued, routed, retried

LABEL rits.runner routes jobs to matching machines. No runner online? The job waits. A runner dies mid-build? The job is requeued, up to three attempts.

roadmap

Built, and what's ahead.

v0

soon

Working today, verified end to end.

  • Full git over SSH — clone, push, pull, protocol v2, ed25519 host keys, public-key auth only.
  • Terminal UI — browse and create repos; commits (lazy-paged), branches, jobs and live build logs as tabs.
  • Self-hosted runners — one-time-token enrollment, persistent SSH channel, labels, live online presence.
  • Dockerfile CI .hooks/Dockerfile.<event>[@glob]; branch and tag triggers; builds as sibling containers, never DinD.
  • Job queue — label routing, sequential per runner, requeue on runner loss, 30-minute timeout.
  • Image artifacts — builds tagged rits/repo/hook:sha, newest three kept, older pruned.
  • Commit status — ✓/✗/● per commit, stored as git notes, fetchable by any clone.
  • One-file deploy — docker compose runs server and runner.

ahead

exploring

Directions under consideration, in rough order.

  • Build artifacts — a job's output won't have to be an image: a .deb, a binary, a coverage report — pushed to the rits server for persistent storage and fetched back over SSH.
  • Continuous deployment — a .sh deploy script that runs after a successful build, entirely in your hands: push the image to a registry, restart pods on staging, rsync to a server.
  • Shell jobs — plain .sh hooks beside the Dockerfiles, same filename triggers, for runners without docker.
  • Merge requests — propose a branch, review the diff and discuss it in the TUI, merge when CI is green — the whole flow over SSH, no web UI.
  • Secrets — per-repo variables, encrypted at rest.
  • Per-key permissions — read-only deploy keys, per-repo access, protected branches.
  • Repo lifecycle — delete, archive, and mirror to or from other remotes.
  • Notifications — job verdicts to webhooks or mail.

Ready to use — self-hosting only. You bring the box, the keys, and the runners. There is no hosted offering, no account system, no telemetry — your repositories never leave hardware you control.