People ditching GitHub after recent issues are looking to self-hosting Forgejo as an alternative.

Despite some (poorly disclosed) security issues, I think there's a more fundemental reason why you shouldn't use Forgejo.

If you're just a single person trying to publish your independent coding projects, why do you need: CI, Issue Tracking, Pull Requests, Multi-user account management, etc?

just use cgit.

How do git forges (like GitHub) even work?

A git forge is basically just a directory of git repositories, served over http or ssh, with a fancy web frontend.

On the server, since the repos don't need to store mutable state (local commits, staged files, etc) they're stored in a special format that's basically just the .git directory of a normal git repository.

This is called a bare repository.

What is cgit?

cgit is essentially just a web-based frontend for these bare repos.

While full-on software forges (like GitHub or Forgejo) give you many extra features, cgit is just the read-only frontend, with nothing else included.

Interested?

cgit Setup Guide

The cgit setup process is fairly involved, but if you were planning on self-hosting Forgejo, then you can handle it.

I'll be using Caddy as the webserver for this setup, but this could be modified for NGINX and Apache if you prefer xml configuration files and manual https certificates ;)

To keep everything organized, I'll run cgit under a user called "git". Our git repos will be stored in this directory.

$ useradd -r -d /var/lib/cgit -s /usr/bin/git-shell git

fcgiwrap

cgit is served as a cgi application.

A long time ago in a galaxy far far away, web servers had builtin support for cgi, but now its often delegated to fcgiwrap.

So our first step will be setting up fcgiwrap.

  1. Install fcgiwrap with a package manager. This will create two systemd files: fcgiwrap.service and fcgiwrap.socket.

  2. Open fcgiwrap.service in a text editor ($ systemctl edit fcgiwrap.service) and set the User and Group to be our "git" user.

  3. Open fcgiwrap.socket ($ systemctl edit fcgiwrap.socket) and add "SocketUser=caddy" and "SocketGroup=caddy" under the [Socket] section. (replace with the correct Caddy user for your system)

cgit

Since cgit is a cgi application, it does not run as its own service. It is simply a script that gets executed by Caddy and piped into fcgiwrap. As such, the setup is pretty simple:

  1. Install cgit with a package manager. This will place the cgit files at /var/www/htdocs/cgit

  2. Make sure that location is owned by "git", but is readable by all users:

    $ chown -R git /var/www/htdocs/cgit && chmod -R 764 /var/www/htdocs/cgit

Caddy

To complete the setup, add this block to the Caddyfile to serve cgit.

git.my.tld {
    @assets path /cgit.css /cgit.js /favicon.svg /robots.txt
    handle @assets {
        root * /var/www/htdocs/cgit
        file_server
    }

    reverse_proxy unix//run/fcgiwrap-cgit.sock {
        transport fastcgi {
            env SCRIPT_FILENAME /var/www/htdocs/cgit/cgit.cgi
        }
    }
}

Lock down SSH access

Using git-shell blocks normal shell access and allows only git-style SSH commands.

  1. Create an SSH directory for the git user: $ install -d -o git -g git -m 700 /var/lib/cgit/.ssh

  2. Add your public key to /var/lib/cgit/.ssh/authorized_keys with mode 600.

Create some repos

Create a bare repository:

$ sudo -u git git init --bare /var/lib/cgit/example

Push to it from your local machine:

$ git remote add origin git@git.my.tld:example
$ git push -u origin main

cgitrc

A clean default /etc/cgitrc file might look like this:

css=/cgit.css
logo=
virtual-root=/

enable-index-owner=0
enable-http-clone=1
scan-path=/var/lib/cgit

root-title=git.my.tld
root-desc=Personal git repositories
snapshots=tar.gz zip

Tradeoffs

You lose integrated issues, pull requests, built-in CI, orgs, and a social graph.

In return, you get a tiny stack, fewer moving parts, lower attack surface, lower resource use, and a setup you can understand.

For my personal projects, that trade is acceptable.