If you're writing code for any purpose, I sincerely hope you are using some kind of source control. I personally am only really familiar with Git, and as such it is my go-to tool for keeping track of my code. Nowadays, there are plenty of hosts for pushing your code to some remote server to keep it in a safe place, but what if you don't want to do that for some reason? Hosting your own git server is certainly an option, and isn't even all that complicated. In this post, I'm going to detail my home git server setup, which is probably quite a bit simpler than most people's home git server setups, but it suits me fine as I'm the only one who uses it. If you're looking for a more complete solution with a web interface (I'll come back to this point in another post), advanced user management controls, or support for Git LFS, then the solution I propose here won't be sufficient for you, and I'd instead advise you to take a look at some other self-hosted options like GitLab or Gitea, both of which are excellent options that I myself have used in the past on my home server. As for me, I'm content with just the basics over SSH.
Before we get started, I'll go ahead and cite my source for the inspiration for this setup, the Pro Git book by Scott Chacon and Ben Straub. There's a lot of great content in there, and it's totally free, so I highly recommend checking it out. I'm also going to be using a simple Debian 9 docker image for the tutorial, and I'd recommend attempting to set up the git server in a similar fashion, using a throway VPS or virtual machine, before attempting to set up a real server to be used as a git server. A simple Raspberry Pi is most likely sufficient for this, though I've yet to test that myself. Without further ado, let's begin.
Setting up the server
I'll start by firing up my test server. I've simply mapped port
22 on my local machine to port
22 on the Docker container for convenience, but you could map any port you so desire. Note that if you wanted to keep using the docker container afterwards and you wanted to persist any of the data you plan to push to the server, you'd need to set up volumes. Also note that using Docker for this is entirely unnecessary, and I don't actually use it myself on my home git server.
$ docker run -p 22:22 -it debian:9 /bin/bash
Once that's ready, we can start setting things up. I like to follow the convention that larger hosts such as GitHub have set by using
git as the SSH user. I won't get into setting up HTTP(S) access in this tutorial, as I myself only use git over SSH.
# useradd -md /home/git git
In case your unfamiliar with the above command, it's just adding a user called
git and creating and assigning a home directory for it at
/home/git. This will be the user we connect to the server with, so we can run commands like
git clone git@my-server. With the
git user created, we'll need to set up SSH access.
# mkdir /home/git/.ssh # chmod 700 /home/git/.ssh
OpenSSH is a bit particular about the access for the
~/.ssh directory, thus it'll need to be set to
700, or read/write/execute for the owner of the directory, and no access for anyone else. Next we'll need the
# touch /home/git/.ssh/authorized_keys # chmod 600 /home/git/.ssh/authorized_keys
Remember to set the permissions for the
authorized_keys file as well, to 600, or read/write for the owner, and nothing for anyone else. If you've done this all as the
root user, don't forget to make the owner
# chown -R git:git /home/git
Then, using vim, nano, emacs, echo, or whatever you fancy, add your public key to the
/home/git/.ssh/authorized_keys file. As a side note, If you happen to have multiple people using the git server, and you wanted to have a little more control over who has access to what, you could create different users on the server for each person you wanted to give access to, and then grant access to repos using the Linux filesystem permissions or something along those lines, but I'll defer to te aforementioned Pro Git book, as I myself don't share my private git server with anyone. Back to the topic at hand, we'll need to make sure we've got both the OpenSSH server and Git installed on the machine
# apt update # apt install -y openssh-server git
With the necessary packages installed, let's fire up the SSH server.
# service ssh start
Then, from another machine (or in my example, the host machine), we can check the connection.
$ ssh git@localhost The authenticity of host 'localhost (127.0.0.1)' can't be established. ECDSA key fingerprint is SHA256:6A3OqYSNXdrt8gPNrdpCxSVP8UHKy1QYhOq7wB9120c. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts. Linux 2caac8f62d55 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. $
And we're in. Time to set some things up.
Customizing the Shell
I don't necessarily want full SSH access for my
git user, as I already have another account on the server for myself, so I'm going to restrict the shell for the
git user. Back at the
root shell for the server, I'll set the default shell for the git user to
git-shell is pretty neat, and I'd encourage you to read more about it, but for this tutorial I'll just cover a few of the basics. First, let's set the shell for the
# usermod -s /usr/bin/git-shell git
Now, if we attempt to SSH in to the machine again, we should get a different message:
$ ssh git@localhost Linux 2caac8f62d55 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Sat Feb 16 20:26:16 2019 from 172.17.0.1 fatal: Interactive git shell is not enabled. hint: ~/git-shell-commands should exist and have read and execute access. Connection to localhost closed.
Note these two lines here:
fatal: Interactive git shell is not enabled. hint: ~/git-shell-commands should exist and have read and execute access.
If you take a look at what
git-shell does, it's basically a super limited version of a shell that only allows you to run preconfigured scripts, placed in the
~/git-shell-commands directory. Back as the
root user, let's set this up now.
# mkdir /home/git/git-shell-commands # chown git:git /home/git/git-shell-commands
I'm going to be adding a couple of files now so that the
git-shell doesn't seem entirely useless. Anything we place in this directory will be offered to the user as an option to run, so I like to have a couple of convenience scripts in there that provide similar functionality to what a web host would offer.
Upon connection, if you have a script called
help in your
~/git-shell-commands directory, it will be run automatically. This would be a good place to put the names and descriptions of all the available commands the user can run. Here's what my
/home/git/git-shell-commands/help script looks like:
#!/usr/bin/env bash cat <<EOF Welcome to the Brawner home private git server. The available commands are listed below: delete [REPOSITORY_NAME] - delete a repository ls - list the repositories mirror [REPOSITORY_URL] - create a mirror of a repository on another server new [REPOSITORY_NAME] - create a new repository rename [OLD_REPOSITORY_NAME] [NEW_REPOSITORY_NAME] - rename a repository EOF
If you add that file, make sure to set the permissions on it correctly:
# chmod +x /home/git/git-shell-commands/*
Then, when you connect to the server via SSH, you get this:
$ ssh git@localhost Linux 2caac8f62d55 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Sat Feb 16 20:28:51 2019 from 172.17.0.1 Welcome to the Brawner home private git server. The available commands are listed below: delete [REPOSITORY_NAME] - delete a repository ls - list the repositories mirror [REPOSITORY_URL] - create a mirror of a repository on another server new [REPOSITORY_NAME] - create a new repository rename [OLD_REPOSITORY_NAME] [NEW_REPOSITORY_NAME] - rename a repository git>
Instead of having our connection dropped, we've now got this
git> prompt where we can enter commands. We haven't added anything just yet, so let's start with the
new command, so that we can create new repos.
Creating New Repositories Remotely
Here's what my version of this looks like:
#!/usr/bin/env bash if [[ -z "$1" ]]; then echo "Please enter a repository name" exit 1 fi if [[ -d "$1.git" ]]; then echo "Repo $1 already exists" exit 1 fi /usr/bin/git init --bare "$1.git" /usr/bin/git -C "$1.git" config http.receivepack true echo "Successfully created repository $1"
It's a pretty simple script, but let's run through it just to make sure it's all clear. I first want to make sure that I've gotten a name for the new repository, and exit the script if it's missing.
if [[ -z "$1" ]]; then echo "Please enter a repository name" exit 1 fi
If I've got a name for it, then I want to make sure I'm not going to try to create a repository that would conflict with another file or folder on the server. Git does this check itself but I like to customize the error message for myself.
if [[ -d "$1.git" ]]; then echo "Repo $1 already exists" exit 1 fi
We then create a new empty repository...
/usr/bin/git init --bare "$1.git"
… and allow for remote push events.
/usr/bin/git -C "$1.git" config http.receivepack true
Lastly we echo out a little success message.
echo "Successfully created repository $1"
Let's add this to the server and give it a shot. Don't forget to add the executable flag to the file, or it won't work.
git> new my-repo Initialized empty Git repository in /home/git/my-repo.git/ Successfully created repository my-repo
We now have an empty git repo we can clone. Back on the host machine, let's give this a clone to see that it's working.
$ git clone git@localhost:my-repo Cloning into 'my-repo'... warning: You appear to have cloned an empty repository.
Awesome! Let's add a file and push it.
$ cd my-repo $ echo "# My Repo - A Git Server Test Repo" > README.md $ git add README.md $ git commit -m "Add README" [master (root-commit) 545d3b7] Add README 1 file changed, 1 insertion(+) create mode 100644 README.md $ git push Counting objects: 3, done. Writing objects: 100% (3/3), 248 bytes | 248.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0) To localhost:my-repo * [new branch] master -> master
Everything seems to be working just fine! We were successfully able to clone, commit, and push. Let's take a look at some of the other commands I like to use on my git server. Creating repos is nice, but it's good to keep track of the ones we have too. For that, I've created the
Viewing Existing Repos
You might think I'm simply wrapping the
ls command that comes built-in, but like any good engineer, I had to overengineer the problem. I don't want to see just any file, I'm looking for git repos. So, here goes:
#!/usr/bin/env bash for repo in $(/usr/bin/find -maxdepth 1 -type d -name "*.git"); do /usr/bin/basename $repo .git; done | sort
This one's a bit shorter, and if you've put that in the right place and set the permissions correctly, running it should produce the following:
git> ls my-repo
We've only got one repo, so the output is a little weak, but you get the idea.
Renaming Repositories Remotely
my-repo sounds a bit generic, and this was a test repo, so let's name it accordingly.
test-repo sounds so much better! For this, I've got the
renamecommand. Here's the script:
#!/usr/bin/env bash if [[ -z "$1" ]]; then echo "Please enter a source repository name" exit 1 fi if [[ -z "$2" ]]; then echo "Please enter a destination repository name" exit 1 fi if [[ -d "$2.git" ]]; then echo "Repo $2 already exists" exit 1 fi mv "$1.git" "$2.git" echo "Successfully renamed repository $1 to $2"
We can break this one down pretty quickly too. We first want to make sure we have the source (or old) repository name...
if [[ -z "$1" ]]; then echo "Please enter a source repository name" exit 1 fi
… as well as the destination (or new) repository name...
if [[ -z "$2" ]]; then echo "Please enter a destination repository name" exit 1 fi
… and we don't want to try to rename a repository to an existing repository name...
if [[ -d "$2.git" ]]; then echo "Repo $2 already exists" exit 1 fi
… but if that's all good, then we go ahead and rename the directory.
mv "$1.git" "$2.git" echo "Successfully renamed repository $1 to $2"
I don't like to repeat myself so I exclude the
.git from my repository and just have the script append it for me. Let's give this one a go.
git> rename my-repo test-repo Successfully renamed repository my-repo to test-repo git> ls test-repo
Another successfully working command.
Cleaning Up Unneeded/Unwanted Repos
With the testing out of the way, it'd be nice to be able to remove this repository. For that, I've got the
#!/usr/bin/env bash if [[ -z "$1" ]]; then echo "Please enter a repository name" exit 1 fi if [[ ! -d "$1.git" ]]; then echo "Repo $1 doesn't exist" exit 1 fi rm -rf "$1.git" echo "Successfully deleted repository $1"
I imagine by now this doesn't need much explanation, so I won't get into the details, except to say that I make sure I have a repo name to delete, and make sure that it exists, and then delete it. You could probably make this a bit more advanced by moving it to some sort of trash bin for 30 days before deleting it, but I'm not that cautious with my personal projects and when I decide it's time for something to go, it's time for it to go. Let's clean up this test repo.
git> delete test-repo Successfully deleted repository test-repo git> ls
Tracking Other Repos
Most hosts like GitHub also allow you to mirror other repositories, and that's a feature I tend to use quite a bit myself. To make this easier on my local git server, I use the
#!/usr/bin/env bash if [[ -z "$1" ]]; then echo "Please enter a repository URL" exit 1 fi /usr/bin/git clone --mirror "$1" echo "Successfully created a mirror of $1"
This one shouldn't need much explanation, so let's give it a run.
git> mirror https://github.com/wbrawner/SimpleMarkdown Cloning into bare repository 'SimpleMarkdown.git'... remote: Enumerating objects: 107, done. remote: Counting objects: 100% (107/107), done. remote: Compressing objects: 100% (57/57), done. remote: Total 1832 (delta 33), reused 75 (delta 31), pack-reused 1725 Receiving objects: 100% (1832/1832), 783.26 KiB | 638.00 KiB/s, done. Resolving deltas: 100% (891/891), done. Successfully created a mirror of https://github.com/wbrawner/SimpleMarkdown git> ls SimpleMarkdown
Now, with mirroring there's an extra step involved. You need to set up a cron job to ensure that your mirrors always stay up-to-date. On most Linux systems, cron should be installed and ready to go. The same is not true for our Docker container though, so we'll need to set that up real quick (and don't forget to persist this should you decide to use the Docker setup permanently).
# apt install cron # service cron start
With cron ready to go, we can add a line to our crontab with this command:
# crontab -eu git
The line to add is as follows:
0 * * * * for repo in $(/usr/bin/find /home/git -maxdepth 1 -type d -name "*.git"); do /usr/bin/git -C $repo remote update; done
Just make sure you've got an empty line after that. This will run through each of your mirrored repositories and pull any updates once per hour.
Congrats, you now have your own private git server! I may write some more posts later about setting up a simple web interface and getting a Git LFS setup to work here as well. Please reach out to me if you see me doing something horribly wrong or inefficient, or if you have other useful scripts that you like to use for your self-hosted git server.