My Minimal Git Hosting Setup

In this tutorial you will learn how to install “your own GitHub” on a Debian/Ubuntu server. By the end you will be able to clone repositories like you do on GitHub:

git clone git@github.com:repo-name.git

becomes

git clone git@git.example.org:repo-name.git

The tutorial will cover the following parts:

  • Setup git on your server to manage repositories
  • Compile and configure stagit, to render repositories as static html pages
  • Setup nginx as the webserver
  • Configure fcgiwrap for anonymous https cloning of repositories
  • Add a logo to the stagit pages

If not mentioned differently all the commands should be ran as root or with sudo.

Setup git on your server

At its core, a git server is just a server that provides SSH access for the git user and stores repositories as plain files.

Install git

You probably have this already installed, if not:

apt install git

Create a git user

Create a git user with /vaŕ/git/ as their home directory. This is also where the repositories will be stored.

useradd -m git -d /var/git -s /bin/bash

Set up SSH login for the git user

This assumes you already have SSH login setup to your server and you want to use the same keys to push to git repositories.

# Create the .ssh directory
mkdir /var/git/.ssh
# Copy the keys from your current user
cp ~/.ssh/authorized_keys /var/git/.ssh/
# Fix permissions
chown git:git -R /var/git/.ssh

To check the setup run:

ssh git@git.example.com

Create your first repository

Create the repo with the .git ending. Login as the git user or change to it with su -l git and run:

git init --bare repo-name.git

Pushing to your repository

On your local machine:

# Create the directory and navigate into it
mkdir repo-name
cd test-repo
# Initialize a new local repository
git init
# Link the local repository to the remote
git remote add origin git@git.example.org:repo-name.git
# Create a README and make the initial commit
echo "# README" > README.md
git add README.md
git commit -m "initial commit"
# show the current branch
git branch --show-current
# Push and set the upstream tracking branch
git push -u origin master

The -u flag sets the upstream tracking branch, so all git push and git pull commands in the future will work without extra arguments.

If you are migrating an existing repository, complete the stagit repository setup below before pushing.

Setup stagit

stagit is a simple static page generator for git repositories. You will need to build it from source and create a simple script to generate the html pages when a git repo is updated.

Install dependencies

apt install libgit2-dev gcc make git

Clone the stagit repo and build it:

git clone git://git.codemadness.org/stagit
cd stagit
make
make install

Create the Generation script

This script will be used to create the static files for each repo. Save it to /usr/local/bin/stagit-gen:

#!/bin/sh
REPODIR="/var/git"
WEBROOT="/var/www/git"

for repo in "$REPODIR"/*.git; do
    dest="$WEBROOT/$(basename "$repo" .git)"
    mkdir -p "$dest"
    ln -sf ../style.css "$dest/style.css"
    ln -sf ../logo.png "$dest/logo.png"
    ln -sf ../favicon.png "$dest/favicon.png"
    cd "$dest" && stagit "$repo"
done

stagit-index "$REPODIR"/*.git > "$WEBROOT/index.html"

Make it executable:

chmod +x /usr/local/bin/stagit-gen

Copy over the default style.css:

cp /usr/local/share/doc/stagit/style.css /var/www/git/style.css

Setup the repository

These steps use the repo-name repository created earlier as an example.

Setup repository metadata

stagit expects the following metadata files inside the repo:

echo "project description"                        > /var/git/myrepo.git/description
echo "Your Name"                                  > /var/git/myrepo.git/owner
echo "https://git.example.org/repo-name.git"      > /var/git/myrepo.git/url

Adjust the repository branch if needed

When initializing a bare repo, git sets HEAD to master by default. If your commits are on a different branch, stagit will not find them since it reads from HEAD to find commits.

On your local machine, check which branch HEAD currently points to:

cat repo-name.git/HEAD
# Example Output: ref: refs/heads/main

Before pushing to the repository you need to match the local branch on the server:

# on the server
git -C /var/git/repo-name.git symbolic-ref HEAD refs/heads/main

Set Up Post-Receive Hooks

Login to the server and link the stagit-gen script to the post-receive hook of the repository. This will regenerate the corresponding html page automatically on every push.

ln -sf /usr/local/bin/stagit-gen /var/git/repo-name.git/hooks/post-receive

You will need to do this manually for each new repo. You can also set up a repository template.

Configure the remote origin of the local repository

Link your local repository to the remote.

git remote set-url origin git@git.example.org:repo-name.git

If you don’t have a remote set yet, use git remote add origin instead.

Push to the Repository

git push -u origin main

Generate the site

This is only needed for the initial generation, or if you need to manually regenerate the site.

Change to the git user with su -l git and run the script:

stagit-gen

Check the output:

ls /var/www/git
ls /var/www/git/repo-name

Configure nginx

Probably also already installed, if not run:

apt install nginx

Obtain a HTTPS certificate

To have our site available via HTTPS you need to obtain a certificate with certbot. Make sure to have git.example.org pointing to your servers IP.

Install certbot:

apt install python3-certbot-nginx

Request the certificate:

certbot certonly --standalone -d git.example.org

Add the nginx configuration

Create /etc/nginx/sites-available/git:

server {
    listen 443 ssl;
    listen [::]:443 ssl ipv6only=on;
    server_name git.example.org;

    ssl_certificate     /etc/letsencrypt/live/git.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/git.example.org/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    root  /var/www/git;
    index index.html;

    # Static stagit pages
    location / {
        try_files $uri $uri/ =404;
    }

    # Git HTTP clone 
    location ~ ^/[^/]+\.git(/.*)?$ {
        root /var/git;
        fastcgi_pass unix:/var/run/fcgiwrap.socket;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
        fastcgi_param GIT_HTTP_EXPORT_ALL "";
        fastcgi_param GIT_PROJECT_ROOT /var/git;
        fastcgi_param PATH_INFO $uri;
    }
}

server {
    listen 80;
    listen [::]:80;
    server_name git.example.org;
    return 301 https://$host$request_uri;
}

Enable the site by linking it to sites-enabled:

ln -s /etc/nginx/sites-available/git /etc/nginx/sites-enabled/

Test the configuration:

nginx -t

Reload the configuration:

systemctl reload nginx

(Optional) Enable https cloning

To enable anyone to clone your repos with git clone https://git.example.org/repo-name.git you will need it install fcgiwrap:

apt install fcgiwrap
# enable auto-start on boot
systemctl enable --now fcgiwrap

Setup the service to run as the git user

For security reasons change the service to run as the git user:

systemctl edit fcgiwrap

Add:

### Editing /etc/systemd/system/fcgiwrap.service.d/override.conf
### Anything between here and the comment below will become the contents of the drop-in file

[Service]
User=git
Group=git

### Edits below this comment will be discarded

Restart:

systemctl restart fcgiwrap

Enable cloning per repo

You will need to allow the cloning for each repo. To do that, just touch the git-daemon-export-ok file in the repo root:

touch /var/git/repo-name.git/git-daemon-export-ok

To automate this, set up a repository template.

Deleting a repository on the server

This is quite straightforward, just do the following:

# Remove the bare repo
rm -rf /var/git/repo-name.git
# Remove the generated static pages
rm -rf /var/www/git/repo-name
# Regenerate the index to remove it from the listing
stagit-gen

Add a logo and favicon to the site

By default stagit has the logo and the favicon saved under /var/www/git/.

I used a 32x32 png image for the logo.

Transfer the logo to the server:

scp logo.png git.example.org:

Copy it to the directory and fix permissions:

sudo mv logo.png /var/www/git/
# Re-use the logo as favicon
sudo cp /var/www/git/logo.png /var/www/git/favicon.png
sudo chown -R git:git /var/www/git/

(Bonus) Git templates

Git has a built-in template workflow we can use to automate the post-receive hook, and the git-daemon-export-ok.

On the server create the template directory:

mkdir -p /var/git/template/hooks

Add the post-receive hook:

ln -sf /usr/local/bin/stagit-gen /var/git/template/hooks/post-receive

Add git-daemon-export-ok:

touch /var/git/template/git-daemon-export-ok

Then configure git globally to use the template:

git config --global init.templateDir /var/git/template