Self-hosting simplegit
simplegit is one static binary: it serves smart-HTTP git and a
server-rendered web UI. TLS is terminated by a reverse proxy; the binary
listens on plain HTTP and requires the git binary on PATH at runtime.
1. Install (recommended)
scripts/install.sh sets everything up except the account (binary, system
user, data dir, config, proxy example conf, systemd unit):
sudo scripts/install.sh https://git.example.com apache
# or: ... https://git.example.com caddy | none
doas works in place of sudo everywhere in this guide (BSD and alpine
hosts ship it instead); the script only needs to run as root.
The second argument picks the reverse-proxy example conf, with the domain
substituted: apache installs /etc/apache2/sites-available/simplegit.conf
(and enables modules/site), caddy installs the site into the Caddyfile
(or a Caddyfile.simplegit to import), none skips it. The unit is
enabled but not started; the config is written only if absent.
Manual equivalent, if you prefer to do it by hand:
1a. Build
go build -ldflags "-s -w" -o /usr/local/bin/simplegit ./cmd/simplegit
The binary embeds templates and static assets, so there is nothing else to
copy. It needs git (with http-backend) installed on the host.
Stripping symbols/DWARF cuts the binary from ~28 MB to ~19 MB; plain
go build works too, just bigger.
2. Config
Create simplegit.toml (see simplegit.toml.example). Every key is
optional.
data_dir = "/var/lib/simplegit" # bare repos, SQLite DB, uploads
listen_addr = "127.0.0.1:8080" # plain HTTP; the proxy fronts it
base_url = "https://git.example.com" # public URL; builds clone URLs/links
base_url must be the public HTTPS URL, or clone URLs and Secure session
cookies will be wrong.
3. Account and data dir
simplegit adduser -config simplegit.toml <username>
Flags come before the positional username (Go's flag parsing stops at the first non-flag). In a terminal you get a hidden password prompt (asked twice); for scripting, pipe the password on stdin so it never appears in the process list:
printf '%s\n' "$PASSWORD" | simplegit adduser -config simplegit.toml <username>
serve and adduser create the data-dir layout on first run, mode 0700.
If the directory already exists from an older install, tighten it once:
chmod -R go-rwx /var/lib/simplegit. Setting UMask=0077 in the systemd
unit keeps everything the service creates private as well.
4. Run it
simplegit serve -config /etc/simplegit/simplegit.toml
systemd unit
[Unit]
Description=simplegit
After=network.target
[Service]
ExecStart=/usr/local/bin/simplegit serve -config /etc/simplegit/simplegit.toml
Restart=on-failure
User=simplegit
Group=simplegit
[Install]
WantedBy=multi-user.target
Run it as a dedicated user that owns data_dir. It does not need root and
binds only the loopback address the proxy forwards to.
5. Reverse proxy (TLS)
Caddy terminates TLS automatically:
git.example.com {
reverse_proxy 127.0.0.1:8080
}
Two proxy requirements:
- Do not rewrite or buffer git request/response bodies. Pushes stream a packfile both ways.
- Do not pass
X-Forwarded-Forexpectations to the app — the guest-write rate limiter keys on the socketRemoteAddr, so behind a proxy all clients share one bucket. (Trusting the forwarded header is a deliberate future decision, not current behavior.)
nginx: proxy_pass http://127.0.0.1:8080; with proxy_request_buffering off;
and a generous client_max_body_size (pushes and release uploads can be
large).
A request body that reaches the app while a response is being written is handled safely (the CGI stdin is drained before the response), but proxies that buffer or retry bodies are still discouraged.
Apache httpd + certbot
Apache streams request bodies by default, so large pushes work once the timeouts are raised. On Debian/Ubuntu:
apt install apache2 certbot python3-certbot-apache
a2enmod ssl proxy proxy_http headers rewrite
Point the DNS record at the host, then use a hand-owned vhost so certbot
never rewrites your proxy config — /etc/apache2/sites-available/simplegit.conf:
<VirtualHost *:80>
ServerName git.example.com
DocumentRoot /var/www/html
<Location "/.well-known/acme-challenge/">
Require all granted
</Location>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName git.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/git.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/git.example.com/privkey.pem
ProxyPreserveHost On
ProxyRequests Off
# big pushes / release uploads
ProxyTimeout 600s
Timeout 600
# pass %2f through instead of 404ing
AllowEncodedSlashes NoDecode
# never re-encode the git stream
SetEnv no-gzip 1
SetEnv dont-vary 1
# unlimited (the default, but explicit)
LimitRequestBody 0
ProxyPass / http://[IP_ADDRESS]:8080/ retry=0
ProxyPassReverse / http://[IP_ADDRESS]:8080/
ErrorLog ${APACHE_LOG_DIR}/simplegit-error.log
CustomLog ${APACHE_LOG_DIR}/simplegit-access.log combined
</VirtualHost>
a2ensite simplegit && apachectl configtest && systemctl reload apache2
certbot certonly --webroot -w /var/www/html -d git.example.com
(certbot --apache -d git.example.com also works, but writes its own
-le-ssl.conf that you would then have to add the proxy block to; the
certonly route keeps the file yours.) Renewal is automatic via the systemd
timer — verify with certbot renew --dry-run.
Apache-specific gotchas, all load-bearing:
ProxyPreserveHost Onis mandatory, or Apache sendsHost: [IP_ADDRESS]and clone URLs/redirects come out wrong.SetEnv no-gzip 1—mod_deflatere-encoding the streaming git response breaks some clients.ProxyTimeout/Timeoutdefault to 60s and kill big pushes.AllowEncodedSlashes NoDecode— Apache's default 404s any URL with%2f.- Keep comments on their own lines. On a 2.4.66/Ubuntu build, inline
# …comments onProxyTimeout/AllowEncodedSlasheslines madeapachectl configtestfail with "takes one argument" (the comment text parsed as a second argument); the same vhost passed once the comments moved to their own lines. The example above is comment-free on directive lines for that reason. - The rate limiter sees
[IP_ADDRESS].mod_proxysetsX-Forwarded-Forautomatically, but the app deliberately doesn't trust it (see above). - Firewall only 80/443; keep the app on loopback.
6. Dogfood (host simplegit on itself)
# create the repo in the web UI, then:
git remote add origin https://git.example.com/<user>/simplegit.git
git push -u origin main
git tag -a v1.0.0 -m "v1.0.0" && git push origin v1.0.0
Pushing an annotated tag publishes a release automatically: the title is the tag message's first line, the notes the rest. Attach binaries the same way you'd do it on any git host — no SSH, from the work machine:
git push -u origin main
git tag -a v1.0.0 -m "v1.0.0
the notes for this release"
git push origin v1.0.0
SIMPLEGIT_TOKEN=sg_... simplegit release -base https://git.example.com \
-repo <user>/simplegit -tag v1.0.0 simplegit-v1.0.0.tar.xz
simplegit release posts the owner's git token (see Account settings →
tokens) over the server's release API with any number of asset files;
re-running it for the same tag re-attaches/updates assets in place. A
lightweight tag gets a release with the tag name as title and no notes.
7. Local UAT (single machine, no proxy)
To exercise the whole UI and git flows locally, without TLS or a reverse proxy, use the helper script:
scripts/local-uat.sh # default port 8090
scripts/local-uat.sh 9000 # or pick a port
It builds the binary, writes a throwaway config, creates the user
(josie / hunter2), and serves on 127.0.0.1:<port>. The binary and the
whole data dir live in .uat/ (gitignored); delete that directory to reset.
Override with UAT_DIR, UAT_USER, UAT_PASS.
Manual equivalent:
go build -o .uat/simplegit ./cmd/simplegit
mkdir -p .uat/data
printf 'data_dir = ".uat/data"\nlisten_addr = "127.0.0.1:8090"\nbase_url = "http://127.0.0.1:8090"\n' > .uat/sg.toml
printf '%s\n' hunter2 | .uat/simplegit adduser -config .uat/sg.toml josie
.uat/simplegit serve -config .uat/sg.toml
base_url is plain http:// here, so the session cookie is not Secure —
correct for localhost, wrong for production.
Create a repo in the UI, then push with:
git -c credential.helper= push http://josie:hunter2@127.0.0.1:8090/josie/<repo>.git main
8. Backups
Everything lives under data_dir:
repos/<user>/<repo>.git— the bare repositories (authoritative history)simplegit.db(+-wal,-shm) — users, sessions, tokens, issues, PRs, releases metadatauploads/releases/<id>/— release attachments
Back up all three together; the DB and uploads reference each other. SQLite
is in WAL mode, so snapshot with sqlite3 simplegit.db ".backup <dest>" (or
copy the DB plus its -wal) rather than copying a live file blindly.
9. Maintenance (gc)
simplegit gc audits data_dir: DB size, repo counts, release upload sizes,
DB↔disk inconsistencies (repo rows without dirs, asset rows without files),
orphaned upload files/dirs from crashed publishes, and expired sessions.
simplegit gc # read-only report
simplegit gc --clean # also delete orphan uploads + expired sessions
simplegit gc --vacuum # rewrite the DB, checkpoint the WAL
--vacuum is best run while serve is stopped:
systemctl stop simplegit
simplegit gc --clean --vacuum
systemctl start simplegit
gc never deletes bare repo directories that lack a DB row (an out-of-band
git init by hand is not garbage it should auto-remove) — it reports them.