Hosting a Remote Git Repository at Home

November 23, 2020

My GitHub account has become cluttered with many private Git repositories over the years. I have repositories for schoolwork, tutorials, notes for my Dungeons and Dragons campaign; pretty much any text-based project gets put in a Git repository. I love being able to roll back changes and easily share work between my machines. Unfortunately, now my GitHub account is covered with private repositories, and logging in to GitHub every time I want to create a repository is a pain.

I recently converted an old gaming PC into a home server, and I’ve always heard Git is pretty easy to self-host. So, I thought why not give it a try. After an afternoon of tinkering I now have a setup I enjoy using. I can create a new repository with gits init newrepo. Then newrepo is available as a remote for all my machines. I can also gits ls to list all the repositories on the server and delete repositories with gits rm newrepo.

Here’s how I did it. Self-hosting a Git remote is fairly easy as far as these things go. You will need to know how to log into a server with SSH and the basics of Git, and need a server running Linux to host on. For this post I’m going to use example.com as the server name. My server is running Fedora Server, but the following should work on pretty much any Linux distribution.

Basic Setup

The core functionality of a git server is quick to set up. A Git remote repository is essentially just a folder containing a bunch of bare git repositories, which can be accessed over SSH. A bare git repository is everything in the .git folder of a normal repository. There is no working tree in a bare git repository, so you can’t make comments or edit files, it just stores all the git metadata.

First, we need to create a user on the server to handle git transactions.

$ sudo useradd -m git
$ sudo su git

Then add the necessary folders for other users to connect via SSH.

$ mkdir -m 700 ~/.ssh/
$ touch ~/.ssh/authorized_keys
$ chmod 600 ~/.ssh/authorized_keys

Now would also be a good time to pick a directory to house your git repositories. The git documentation uses /srv/git, but I prefer to use the git user’s home directory. This keeps everything in one place on the file system and makes for nicer syntax when interacting with the remote. To grant a user access to your Git server, copy their public ssh key to /home/git/.ssh/authorized_keys. Once you’ve added a user’s key you’re done. Yep, it’s just that easy.

To create a new repository, log in to your server as git and create a new bare repository like so:

$ ssh [email protected]

# As git
$ mkdir ~/newrepo.git
$ git init --bare ~/newrepo.git

This will create a new directory and initialize it as a bare git repository. Any user with a key in /home/git/.ssh/authorized_keys can clone the repository with

$ git clone [email protected]:newrepo.git

Create an Interface with git-shell

While all the basic Git functionality is available, we still need to log in to the server and create git repositories manually. It would be great to make the UX a bit nicer. Luckily, Git comes with git-shell, which is designed for this purpose.

To utilize git-shell, create a folder git-shell-commands in the git user’s home directory on the server. When a git-shell session is started, this folder will be used as a PATH, where any scripts in the directory will be available as commands. I’ve created a repository with the commands for my git interface. Feel free to copy the commands, or even easier, clone the repository into your git home directory.

$ cd
$ git clone https://git.sr.ht/~jakesco/git-shell-commands

The repository contains 4 files.

  1. help: Prints basic usage info. This is run automatically when git-shell starts.
  2. ls: Lists all git repositories in the git home directory.
  3. init: Create a new repository with the parameter name.
  4. rm: Remove a repository matching the parameter name.

Note that these are simply shell scripts. You can define arbitrary commands and save them in this folder for use by git-shell. For example, here is the init command:

#!/bin/sh

if [ $# -ne 1 ]; then
    echo "usage: init <name>"
    exit 1
fi

base_dir="$HOME"
new_repo="$1"

case $new_repo in
    *\.git)
	;;
    *)
	new_repo="${new_repo}.git"
	;;
esac

if [ -d "$new_repo" ]; then
    echo "$new_repo already exists"
    exit 1
fi

mkdir "$base_dir/$new_repo"
git init --bare "$base_dir/$new_repo" >/dev/null 2>&1\
    && echo "Initialized empty Git repository: git@$(uname -n):$new_repo"

After copying the git shell commands, our home directory should look like this:

$ tree -a /home/git
.
├── git-shell-commands
│   ├── help
│   ├── init
│   ├── ls
│   └── rm

└── .ssh
    └── authorized_keys

All that’s left to do is change the git user’s default shell to git-shell.

$ sudo chsh git -s $(which git-shell)

Now when any user logs into the server as git they will be presented with git-shell like so:

Welcome to git-shell on example.com!

Available Commands:
ls: list all repositories.
init <name>: Initialize new Git repository with <name>
rm <name>: Delete Git repository with <name>

git>

You can also access these commands via SSH with

$ ssh [email protected] <command>

You can make an alias for an even easier interface

$ alias gits='ssh [email protected]'

Now you can manage the git server with gits init, gits rm, and gits ls.

Additional Security

This setup is fine for a single user home server setup, but if you want to open this up to the internet or have multiple users, we should lock down a few things. First, depending on your server’s SSH configuration, anyone with access can use their connection to create an ssh tunnel. Simply prepend any SSH keys in /home/git/.ssh/authorized_keys with no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty. So keys in the authorized_keys file should look like this:

$ cat /home/git/.ssh/authroized_keys \
no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty \
ssh-rsa AAA...<key content> Description

Second, I like to disable password log in for utility users like git.

$ sudo passwd -l git

Now git can only be accessed via SSH.

And there you have it, a fully functional private git server.

References