gitmote Β· history Β· branches
| Name | Mode | Size |
|---|---|---|
| π .dockerignore | 100644 | 101 |
| π .env.example | 100644 | 1041 |
| π .github/ | 040000 | |
| π .gitignore | 100644 | 33 |
| π .gitmote/ | 040000 | |
| π .golangci.yml | 100644 | 535 |
| π CLAUDE.md | 100644 | 763 |
| π CONTRIBUTING.md | 100644 | 3711 |
| π Dockerfile | 100644 | 1857 |
| π Dockerfile.runner | 100644 | 1609 |
| π LICENSE | 100644 | 11357 |
| π Makefile | 100644 | 5271 |
| π README.md | 100644 | 8001 |
| π cmd/ | 040000 | |
| π docker-compose.dev.yml | 100644 | 1367 |
| π docker-compose.selfdeploy.yml | 100644 | 1202 |
| π docker-compose.yml | 100644 | 1739 |
| π docs/ | 040000 | |
| π go.mod | 100644 | 3615 |
| π go.sum | 100644 | 33948 |
| π internal/ | 040000 | |
| π scripts/ | 040000 | |
| π tasks/ | 040000 |
Your own, personal, GitHub.
A tiny self-hosted git remote. One Go container speaks git's smart-HTTP protocol; repositories live in S3-compatible object storage and mutable metadata (refs, users, keys, access) in s3lite. Use it instead of GitHub for a handful of repos and a few invited people β it is explicitly not trying to be GitHub at scale.
The one interesting trick: git's data is split by mutability β immutable objects/packs to S3, mutable refs and forge metadata to s3lite (plain SQLite, backed to S3) β so the only genuinely hard problem, atomic ref updates, becomes a single SQL transaction.
Status: early implementation β git read/write over smart-HTTP works
(clone/fetch/push with token auth and per-repo ACLs), and gitmote hosts its own
source repo end-to-end: locally against MinIO (make e2e-local) and in
production on Scaleway Object Storage at
gitmote.atmin.net. The design lives in
docs/architecture/.
One container, one bucket. S3 is the single source of truth and the container is disposable: kill it and restart β it restores the whole forge from S3 and continues. Point it at any S3-compatible bucket with credentials:
docker run --rm -p 8080:8080 \
-e GITMOTE_S3_BUCKET=my-gitmote-bucket \
-e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY_ID=β¦ -e AWS_SECRET_ACCESS_KEY=β¦ \
ghcr.io/atmin/gitmote:master
# Published tags: :master (latest on master) and :<git-sha>. There is no :latest.
# Non-AWS S3 (Scaleway, MinIO, R2): add -e GITMOTE_S3_ENDPOINT=https://s3.fr-par.scw.cloud
# Keep the local cache across restarts (optional): -v gitmote-data:/data
On the first run gitmote auto-bootstraps an admin and prints a one-time access
token to the logs behind a SAVE IT NOW banner β grab it there (no setup page).
Then sign in at http://localhost:8080/login by pasting it, and clone/push:
git clone http://admin:<token>@localhost:8080/<repo> # create the repo on the dashboard (/) first
Lost the token? It's never stored (only its hash), so re-mint one β stop the container, then:
docker run --rm -e GITMOTE_S3_BUCKET=β¦ -e AWS_β¦ ghcr.io/atmin/gitmote:master bootstrap -reissue
Nothing else is required: the session cookie key and CI worker secret are generated and persisted on first run. See docs/ops.md for the full env surface, CI, and the Scaleway deployment.
Because the bucket is the forge, you don't need any always-on compute. Keep the
bucket in the cloud, run gitmote locally only when you want to push or pull,
and stop it afterwards β your git remote is localhost, but the durable state
lives in S3. This is the cheapest way to self-host: you pay for object storage,
not a running server.
# Start the forge against your CLOUD bucket, then push/pull, then Ctrl-C.
docker run --rm -p 8080:8080 \
-e GITMOTE_S3_BUCKET=my-gitmote-bucket \
-e GITMOTE_S3_ENDPOINT=https://s3.fr-par.scw.cloud \ # your provider's endpoint
-e AWS_REGION=fr-par \
-e AWS_ACCESS_KEY_ID=β¦ -e AWS_SECRET_ACCESS_KEY=β¦ \
ghcr.io/atmin/gitmote:master
git remote add gitmote http://admin:<token>@localhost:8080/<repo>
git push gitmote main
Point at the same storage root (GITMOTE_S3_BUCKET), endpoint, and region as
before. Objects and metadata share that root, so a later instance either finds the
whole forge or, pointed at the wrong root, sees a clean empty one β never a
half-restored state.
Use your existing token. A populated bucket already has an admin; don't
re-bootstrap it β sign in with the token you saved (or bootstrap -reissue to
mint a fresh one).
Only one writer at a time β and that's enforced, not your job. gitmote takes a
single-writer lease on the storage root, so running a second instance (or a stray
cloud container) can't corrupt anything: whoever holds the lease writes, the other serves
reads and returns a retryable 503 on push. So a local on-demand forge is safe
even if you also run gitmote in the cloud β no coordination, no harm. Details in
docs/ops.md (βSingle writer is a correctness invariantβ).
make dev gives you a running instance in one command: it builds the binaries,
starts MinIO in a container (S3 on :9100), runs gitmote natively on :8080,
and ensures an admin + a gitmote repo (auto-bootstrap), minting a fresh
token each run and printing it. State (the metadata DB and object cache) persists
under data/, so repos and history survive restarts:
make dev # first run prints the token, clone URL, and UI URL
make dev-reset # wipe MinIO + data/ for a clean slate
Sign in to the UI at http://localhost:8080/ by pasting the token, or clone/push straight away with the printed URL. Requires Docker + Docker Compose.
CI works locally too. Push a repo with .gitmote/workflows/*.yml and gitmote
records a run and executes it on the spot β it spawns gitmote-runner as a local
process (the same runner code the cloud path runs on Scaleway, just a local
substrate) which runs the workflow with act.
Install act (brew install act); it uses the same Docker/podman daemon MinIO
does. Watch runs in the UI. gitmote reads .gitmote/workflows (GitHub-workflow
syntax, run by act) β not .github/workflows, so a repo mirrored to GitHub
chooses its forge by where the workflow lives and never double-runs.
make e2e-local brings up a gitmote container plus MinIO with
docker-compose.yml, bootstraps an admin/token/repo, then
pushes this working tree, clones it back, and β after force-recreating the
container β clones again to prove the repo survives on the object store and
persisted refs (not local disk). Requires Docker + Docker Compose.
make e2e-restore additionally exercises the litestream cold-start path (wipes
the metadata DB and restores it from S3).
make e2e-build proves the GITMOTE_CI_ALLOW_BUILDS gate: it runs act in a
privileged docker-in-docker box (a rootful daemon in a box) with the daemon socket
suppressed (a docker build step fails) then mounted (it succeeds), so the safe
default and the opt-in are both verified against a real build. Skips cleanly with
no daemon. Deployment β Scaleway Serverless Containers, single writer,
gitmote.atmin.net β lives in docs/ops.md.
An empty instance has no users, so token auth would be a chicken-and-egg β so the
server auto-bootstraps on first run: when it is the writer and no admin
exists, it creates the admin user and prints a one-time token to the logs (see
the Quickstart). No second command.
You can still bootstrap by hand against the bucket before the server is live
(gitmote bootstrap [-handle β¦] [-repo β¦]), and recover a lost token with
gitmote bootstrap -reissue β both refuse to clobber and print the token once.
Only the token's hash is ever stored, so a lost token is re-minted, never
recovered.
The bare root / is a dashboard: a viewer-scoped repo list (public repos to
anyone, private to those with access, all to an admin) with browse pages for each
repo. Sign in at /login by pasting a token (the same PAT format git uses) β any
user may sign in to see the repos they can access; the server issues a signed,
stateless session cookie keyed by an auto-generated, persisted secret (override
with GITMOTE_COOKIE_KEY).
The things you can't do over git are admin-only: create repos and mint/revoke
tokens from the top-level /users and /tokens pages, and manage a repo from its
/<repo>/settings (visibility, default branch), /<repo>/access (invite
spectators/collaborators), and /<repo>/secrets pages.
git push gitmote β reach out and touch faith. πΆ