Git Server Setup

If you can, please read my plea for help and if you can, please help.

This guide is not yet finished.

This guide assumes some familiarity with how to use a UNIX shell account, using SSH to connect to a server, and using a text editor from a command line interface to modify a text file.

  1. UNIX Shell Basics
  2. SSH Basics
  3. VIM Basics

I am assuming most people in need of a git server have at least some experience with those concepts. If not, they are a world of fun to learn.

Personal Git Server

If you are afraid of the command line, this page is not for you. However if you are afraid of the command line, chances are high that you do not have much use for a git server.

Some of this is obvious to veteran *nix admins but for the new admin, it may not be so obvious, so that is why I spell a lot of it out.

The ssh related stuff will be broken out into a separate guide at some point.

This does not tell you how to run a multi-user git server. I have no experience doing that, and therefore have no place instructing others on what is the best way to do that.

This guide is for the geek who wants his own git server for personal projects that are not shared with the public and/or personal projects that are shared with the public.

Convention Notes

Where you see [root@git ~]# that indicates you should be logged in as the root user on the VM you want to use a git server. The ~ will not always be squiggle if the command needs to be executed in a particular directory.

Where you see [awonder@git ~]$ that means you should be logged in as whatever username you plan to use for normal unprivileged login to the system. Replace awonder with your chosen standard username.

Where you see [git@git ~]$ that means you should be logged in as the user git which will be created for your public git repositories.

Where you see [username@yourworkstation ~]$ that means you are logged in at whatever computer you use to actually write your code, not the git server.

The hostname represents whatever hostname you are going to use for your git server.

For those of you who use Microsoft Windows for your code development, I apologize that these instructions are heavily biased towards users of UNIX derivative operating systems. I only use UNIX derivative operating systems, I do not currently have a Windows PC, you will need to adapt these instructions where needed.

These instructions never make use of the sudo command. The only place where it would be needed is on the server, and these instructions are written with CentOS 7 in mind. CentOS 7 does ship with sudo but it does not use the configuration that OS X and Ubuntu made popular.

If you like the behavior in Ubuntu and OS X, you can set up CentOS 7 that way (just add your user to the wheel group) and adapt the parts of these instructions that require root privilege to use sudo, but most CentOS 7 users I know choose not to.

The OS X and Ubuntu sudo defaults are probably good for end user machines but they are not really optimal for a server in my opinion, which you are free to disagree with. Please though, no sudo configuration holy wars in the comments.

Commands will be in a pre block with a very dark (but not quite black) background and Charteuse color text and include the shell prompt.

Linode VM

For code management, it really is best to have a dedicated virtual machine. This will prevent bugs in other services from allowing hackers in that can potentially poison your code.

I highly recommend Linode, they are a very good company. I have been with them for many years and have no regrets. A $10 a month linode is sufficient unless you have a lot of code to manage. A $10 a month linode gives you two gigs of storage, of which the operating system uses very little.

If you choose Linode, I would greatly appreciate it if you would use this link:

That referral will help me out.

When you create a new VM image at Linode, the default (currently) is Debian 9. That is a fine high quality distribution but I actually recommend CentOS 7 simply because CentOS is what I have used for quite some time. These instructions assume CentOS 7.

Set the Hostname

When you log in, there will be a non-descriptive hostname that makes things difficult when you have multiple terminal windows open with connections to multiple different servers. Change the hostname to the fully qualified domain name you are using with that host.

[root@li219-112 ~]# hostnamectl set-hostname

After that, the non-descriptive li219-112 will instead appear as git when you log in. Much more descriptive. Obviously use your hostname. If you need to register a domain, I highly recommend namecheap. Whatever registrar you use, unless you run your own DNS I would recommend using linode to run your DNS. It was free last time I used it, and it is very resilient to DoS attacks.

Update the System

Linode does a pretty good job of keeping the CentOS 7 images fairly modern, but there are almost always updates since the last time they generated a CentOS 7 image. Apply them as soon as you have first connected:

[root@git ~]# yum update

The number of updates will likely be small, and Linode has wicked fast connection to a CentOS update mirror, this does not take very long.

Enable EPEL and the HAVEGED Daemon

Some of the software needed is in EPEL package repository, including the HAVEGED daemon which is useful for increasing the entropy of the Linux pRNG.

[root@git ~]# yum install epel-release
[root@git ~]# yum install haveged
[root@git ~]# systemctl enable haveged.service
[root@git ~]# systemctl start haveged.service

When installing haveged it will ask you if it is okay for RPM to import the EPEL GPG key. Let it.

Optional: Disable SELinux

By default, the CentOS 7 image has SELinux enabled. SELinux can add protection but it also frequently gets in the way. Very rarely is there an actual attack that it will protect you from that you would not also be protected from just by practicing other safe practices.

If you want to spend time learning SELinux, feel free to do so, I personally find it to be more of a hassle than a benefit. I disable it. I have attempted to run servers with it enabled in the past, and usually it starts out fine, but what always ends up happening is there is some issue I need to solve where SELinux is preventing the solution and I can not figure out how to get SELinux to allow the needed action, resulting in extreme frustration and temptation to bust my monitor as search engines are not finding the solution I need. So I stopped trying.

I suspect SELinux is something that really does require a paid instructor to properly learn (and the syntax has changed several times since I tried learning it) which I do not have the budget for, so I just disable it. Haven’t been hacked yet, following best practices has always protected me.

To disable SELinux in CentOS 7, edit the file /etc/sysconfig/selinux and change the line




The change will not take effect until the next time you restart the server, but I recommend doing that after adding users below.

Add Users

When you first set up the VM only the root user has been created. For both a private git no one but you can access and a public git anyone can access, it is a good idea to have two users. For the private git, use your normal *nix username (e.g. if your name is Alice Wonder you might use awonder) and for the git for public code, create a user named git.

On CentOS systems where normal system users start with a UID/GID of 1000 I give special function users, like git, a UID/GID somewhere between 900 and 999.

[root@git ~]# useradd awonder
[root@git ~]# passwd awonder
Changing password for user awonder.
New password: 
Retype new password: 
passwd: all authentication tokens updated successfully.
[root@git ~]# groupadd -g 950 git
[root@git ~]# useradd -g 950 -u 950 git
[root@git ~]# passwd git
Changing password for user git.
New password: 
Retype new password: 
passwd: all authentication tokens updated successfully.

A good password should be at least twelve characters long and should be generated in a random fashion to avoid brute force login attacks.

If you disabled SELinux, now is a good time to reboot the virtual machine so it will take effect. You can do that through the Linode console.

OpenSSH Key Pairs and Server Hardening

Please follow the instructions on the OpenSSH Primer page to generate SSH keypairs on your workstation (not the git server)if you do not already have them, and follow the instructions on that page to install the public key in both the user account you will use for your private git repositories (referred to as the awonder user in this document) and the git user account on the git server, so that you can use Public Key Authentication to connect to those accounts via SSH.

Also you should follow the guide on that page for securing the OpenSSH daemon on the git server.

Git by itself does not offer any encryption. This guide uses OpenSSH to secure git commits and git clone of the private repositories, and will use HTTPS to secure git clone of the public repositories. It is therefore important to have OpenSSH securely configured.

SSH Port Adjustment

If you set up your SSH daemon to use a custom port, the easiest thing to do is configure your ssh client to use that port automatically so you do not have to manually specify it as part of the git origin.

If your workstation (not your server) does not have the file ~/.ssh/config then create it. Otherwise append to it. Add the following:

  Port 2112

Obviously use your git server hostname, and replace 2112 with your custom port number.

The ~/.ssh/config file must only be readable by your user or OpenSSH will refuse to use it:

[username@yourworkstation ~]$ chmod 0600 ~/.ssh/config

This makes life easier and you do not have to specify the port every time you connect as the ssh client will use the port defined there instead of the default of port 22.

Install Git

Properly setting up SSH was important because git by itself does not use encryption, we have to tunnel git through a protocol that does to securely use git over a network. Now we can get to the effing point of this article.

[root@git ~]# yum install git

That will pull in git and all of it’s dependencies.

Now on the server with your regular user account, create a private repo. For this example, I will use a search engine I’ve been working on that is currently on my github in a private repo. Initiate the repo as your normal user on the server, I’m using awonder for this example.

[awonder@git ~]$ git init --bare arachnophobia.git
Initialized empty Git repository in /home/awonder/arachnophobia.git/

Now from your workstation, take an up to date clone of your private repo and change the origin to point to your new server.

WARNING: If you have more than just a master branch, see the instructions at and make sure you actually have all the branches in your new git server before nuking the github repo.

To make it all clean, I created a directory called somedir (I am so creative) on my workstation for this process. Every github repo I migrate to my own git server, I will do a checkout in that directory from github, switch the origin to my personal git server, and then delete the repo from github. For the private repos anyway, getting rid of them is necessary to kill the github billing.

[username@yourworkstation somedir]$ git clone
Cloning into 'arachnophobia'...
Username for '': AliceWonderMiscreations
Password for '': 
remote: Counting objects: 378, done.
remote: Total 378 (delta 0), reused 0 (delta 0), pack-reused 378
Receiving objects: 100% (378/378), 194.71 KiB | 0 bytes/s, done.
Resolving deltas: 100% (216/216), done.

[username@yourworkstation somedir]$ cd arachnophobia
[username@yourworkstation arachnophobia]$ git remote set-url origin ssh://
[username@yourworkstation arachnophobia]$ git push
Counting objects: 378, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (153/153), done.
Writing objects: 100% (378/378), 194.71 KiB | 0 bytes/s, done.
Total 378 (delta 216), reused 378 (delta 216)
To ssh://
 * [new branch]      master -> master

The command

git remote set-url origin ssh://

That took the copy of the project I just checked out from github and changed the origin from github to my private git server.

Everything, including my commit history, is now on my git server.

If I want to clone the project, I would just run:

git clone ssh://

After authentication to the ssh server is validated, the repo will be cloned.

Rinse and repeat for all your private repos at github until they all have been migrated. You can then cancel your paid account if you have one.

Do not forget to change the origin on any servers etc. that you currently pull from these repositories on github. That means setting up ssh key pairs on those servers so they can authenticate with the git server and pull.

Initialize New Private Repo

When you want to create a brand-spanking new private repo rather than migrate one from github, the process is very similar on the server. Initiate an empty repository there with the --bare switch.

On your workstation, create a directory for your work, add a file (or files) to it (e.g. your initial, LICENSE, and .gitignore) and initialize it:

[username@yourworkstation newproject]$ git init
[username@yourworkstation newproject]$ git remote add origin ssh://

Then you can add, commit, and push in the normal fashion.

The first time you do a push, you will need to add this switch:

[username@yourworkstation newproject]$ git push --set-upstream origin master

Secure Web Server for Public Git

With repositories that we are going to be making open to the public to clone, we want to still use OpenSSH for our interaction with the repository where we make commits and push. However for public clones of the project, it needs to be accessible over HTTPS for cloning.

It is very important to me that the web interface is NOT given write access to the actual git repository. I only want that via ssh. With ssh it is easy to use public key cryptography to completely restrict write access. A user without a private key corresponding to an authorized public key can not even connect. With HTTPS interfaces to git, for commits either the web server has to have write access to the git repository (very dangerous) or there has to be cgi scripts that allow the web server to perform actions as another user, also very dangerous.

So unlike github which did allow git commits over HTTPS, the instructions here will only result in read access over HTTPS giving only the ability to clone and pull. To push a commit, ssh will be required. That IMHO is the way it should be, and honestly is how I wish github had done things.

Poke a Hole in your Firewall

If you are running the CentOS 7 firewall (which I recommend) permanently poke a hole in it for Port 443:

[root@git ~]# /bin/firewall-cmd --zone=public --add-port=443/tcp --permanent
[root@git ~]# /bin/firewall-cmd --reload

There is no need to permanently poke a hole for Port 80. We will need to temporarily poke a hole for Port 80 when getting a signed TLS certificate, but there is not a valid reason to support a git clone over an insecure protocol, nor is there a valid reason to have port 80 redirect to 443. Just keep port 80 blocked by the firewall by default.

Install Needed Software

You will need to install Apache, PHP, and the Let’s Encrypt Cert Bot.

Optional – Install LibreLAMP

LibreLAMP is a repository of RPM packages for CentOS 7 I maintain that provide a modern LAMP stack built against LibreSSL. You do not need to use LibreLAMP for this, but I recommend it.

To install LibreLAMP:

[root@git ~]# curl -O
[root@git ~]# rpm -ih awel-libre-release-7-3.noarch.rpm

After doing that, the yum utility will pull packages from the LibreLAMP repository when they are available.

Install Apache, PHP, and CertBot

To install the needed software packages:

[root@git ~]# yum install httpd mod_ssl php php-pear certbot

If you are using LibreLAMP, yum will ask you if it is okay to import the AWEL GPG key. Let it.

Obtain Signed TLS Certificate

To get a free TLS certificate, I recommend using the following certbot shell script I wrote:

That script will temporarily poke a hole in your firewall for Port 80 (needed to verify your server with Let’s Encrypt), generate a 3072-bit RSA private key, generate a Certificate Signing Request (CSR), fetch the signed certificate, and create the DANE fingerprints you can use with DNSSEC if you have a DNSSEC signed zone.

Note that certificates from Let’s Encrypt are only valid for three months. So what I do is create them when I need them and then recreate them at some point during every odd-numbered month (January, March, May, July, September, November). That ensures I get fresh certificates before they expire.

In order to simplify maintenance of DANE records if you do choose to use DANE it only generates a new private key if the existing private key either does not exist or is at least 320 days old.

You will need to edit the script before running it so that it will generate a CSR with the proper information.

To download the script to your git server:

[root@git ~]# curl -O
[root@git ~]# mv

OpenSSL Binary

If you opted NOT to use my LibreLAMP packaging then change line 28 from




CSR Details and Certificate Generation

Edit lines 94-97 and line 99 to match your specific details.

Once those edits are done, make sure the apache daemon is not running, and run the script:

[root@git ~]# sh

Replace with your domain name.

The first time the script is run, the certbot utility it uses is interactive and you will need to agree to the terms. You can read them ahead of time here (PDF link):

Assuming everything goes according to plan and you agree to the terms, the certificate for your hostname and a certificate authority bundle will be installed in /etc/pki/tls/eff_certs/ and your private key in /etc/pki/tls/eff_private/.


If your DNS zone is not DNSSEC signed, do not worry about this. For more information on DANE, please see

When the script successfully exits, it will provide two fingerprints you can optionally use when creating DANE records in a DNSSEC signed zone file. It will look something like this:

TLSA from Cert:
3 0 1 D47CF8CBEF7B4EBBC472CD6A946BFF27E2713CF90F06F3A7D9B807060B0C7997

TLSA from PubKey:
3 1 1 D14CA2C5D0C3A8A9B5F3026C19229FC23E4A80FF36988CF4DA6D54B9C1904D67

The first is part of a TLSA record based upon the X.509 certificate. I do not recommend using it, it changes every time you generate a new certificate.

The second is part of a TLSA record based upon the public key itself rather than the certificate. It only changes when a new private key is generated, that is what I recommend using with Let’s Encrypt if you are going to add a DANE fingerprint to your DNS zone. Please note that at this time, no browsers actually enforce DANE without third-party extensions, so in some respects, using DANE is just academic and does not really add security. Yet.

Public Git Repositories

When we created private git repositories, that was done within the home directory of the user. That is appropriate, the data in them is private to that user. However with public git repositories, the data will be served by the Apache daemon and should not be within a user home directory, even with a special user like the git user we created.

So we will create a directory within /srv for these git repositories:

[root@git ~]$ mkdir /srv/git
[root@git ~]$ chown git:git /srv/git
[root@git ~]$ chmod 755 /srv/git

The special git user now has write permission to that directory, and the directory has the correct permissions so that only the special git user has write permission there, but the apache user can still read directories and files within that directory.

If you have SELinux enabled, you probably will have to do some special context label voodoo to allow Apache to serve files in that directory. At least historically that was the case. If you want SELinux and do not know how to do that, use /var/www/html/git instead of /srv/git and you might not have to do any special label voodoo. For these instructions, I will use /srv/git and that is my preference because it makes the full path to the repository shorter.





Multi-User Note

This page is primarily intended to help a single developer create a personal git server. If you want multi-user, it is possible. For private repositories just make sure each user has their own user account. For public repositories, just forget about the git user but still create /srv/git – just give each user a subdirectory within it that they own, e.g. /srv/git/awonder and /srv/git/ltorvalds and make sure those users have write permission to those directories.





Initiate Repository

Initiating a public repository is almost identical to initiating a private repository. It just has one extra step. For this example, I will initiate a public repository called FileWrapper for an open source PHP class I wrote.

I will log in as the git user to do this.

[git@git ~]$ cd /srv/git
[git@git ~]$ cd /srv/git
[git@git git]$ git init --bare FileWrapper.git
Initialized empty Git repository in /srv/git/FileWrapper.git/
[git@git git]$ cd FileWrapper.git/
[git@git FileWrapper.git]$ mv hooks/post-update.sample hooks/post-update
[git@git FileWrapper.git]$ chmod a+x hooks/post-update

The extra steps, git already ships with a utility that prepares a git repository for proper cloning by an anonymous user over a HTTP interface. This utility is called update-server-info and it can be activated as a post-update hook that always runs every time a commit is pushed to the server.

After initiating the repository, the extra commands shown activate the post-update hook which is already set up for us to do just that. This is the contents of the sample post-update hook as shipped with git:

# An example hook script to prepare a packed repository for use over
# dumb transports.
# To enable this hook, rename this file to "post-update".

exec git update-server-info

Now all I need to do to migrate that repository from github to my personal git repository is clone the repository from github, change origin, and push:

[username@yourworkstation somedir]$ git clone
Cloning into 'FileWrapper'...
remote: Counting objects: 126, done.
remote: Total 126 (delta 0), reused 0 (delta 0), pack-reused 126
Receiving objects: 100% (126/126), 47.16 KiB | 0 bytes/s, done.
Resolving deltas: 100% (65/65), done.

[username@yourworkstation somedir]$ cd FileWrapper
[username@yourworkstation FileWrapper]$ git remote set-url origin ssh://
[username@yourworkstation FileWrapper]$ git push
Counting objects: 126, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (51/51), done.
Writing objects: 100% (126/126), 47.17 KiB | 0 bytes/s, done.
Total 126 (delta 65), reused 126 (delta 65)
To ssh://
 * [new branch]      master -> master

Again, if you have more than just the master branch, please instead follow the instructions at and verify that all branches are migrated.

The repository has been migrated, and once Apache is set up, it can be cloned over HTTPS by anyone who wants it.

GitList Setup

GitList is a simple PHP web application that allows browsing a git repository and has a similar look and feel to the GitHub interface, but it is not quite as fancy and yes, there are a few bugs.

The GitList homepage is at and at the time of this writing is at version 1.0.0. Unfortunately trying to grab the tarball with curl seems to result in a 302 error, so you will need to download the 1.0.0 tarball with your regular browser and copy it to the remote server.

[username@yourworkstation Downloads]$ scp gitlist-1.0.0.tar.gz
gitlist-1.0.0.tar.gz                                     100% 1953KB  78.1KB/s   00:25    

[username@yourworkstation Downloads]$ ssh -l git
Last login: Sat Jun  9 09:00:59 2018 from 2600:1010:b00f:90e4:3087:d890:629a:ea3d

[git@git ~]$ tar -zxf gitlist-1.0.0.tar.gz 
[git@git ~]$ su
[root@git git]# mv gitlist /srv/

Since this directory will be served by Apache, again, if you have SELinux enabled you probably need to do some voodoo context stuff for it to work.

GitList needs a cache directory the web server can write to. The install instructions say to give that directory 777 permissions but that is effing dangerous, so as the root user:

[root@git git]# cd /srv/gitlist
[root@git gitlist]# mkdir cache
[root@git gitlist]# chown apache:apache cache
[root@git gitlist]# chmod 755 cache

The web server can now write to the cache directory. This, btw, is a security concern I plan to fix in a fork. The cache directory is in a directory the web server serves data from, and it is a bad idea for the web server to have write permission to a directory it also serves from. Lots of commonly used web applications do it (e.g. WordPress) but it is still the wrong thing to do.

GitList Configuration File

GitList ships with a sample configuration file called config.ini-example
Copy that file to config.ini and make a few edits.


repositories[] = '/home/git/repositories/' ; Path to your repositories


repositories[] = '/srv/git/' ; Path to your repositories





Multi-User Note

For multi-user public repositories, you would want to do:

repositories[] = '/srv/git/awonder';
repositories[] = '/srv/git/ltorvalds';
// etc

I have not tested how well that works.





Finally change

show_http_remote = false ; display remote URL for HTTP
http_host = '' ; host to use for cloning via HTTP (default: none => uses gitlist web host)


show_http_remote = true ; display remote URL for HTTP
http_host = '' ; host to use for cloning via HTTP (default: none => uses gitlist web host)

That is needed for clone button to give the user instructions on how to clone it over HTTPS. Replace with your own domain.

That’s it, now you ready to set up the web server:

Apache Web Server Setup

For the web server, we only need to set up a single virtual host listening on port 443 (the standard HTTPS port).

/srv/gitlist will be the DocumentRoot and an Alias directive will be used to make /git/ and alias to /srv/git/ so that repositories can be cloned over HTTPS.

Here is the Apache configuration file I am using:

<VirtualHost *:443>
Header always set Strict-Transport-Security "max-age=63072000; preload"
DocumentRoot "/srv/gitlist"
SSLEngine on
SSLHonorCipherOrder on
SSLCACertificateFile       /etc/pki/tls/eff_certs/
SSLCertificateFile             /etc/pki/tls/eff_certs/
SSLCertificateKeyFile        /etc/pki/tls/eff_private/
ErrorLog logs/gitlist.error_log
CustomLog logs/gitlist.access_log combined
Alias /git/ /srv/git/

<Directory /srv/gitlist>
Options FollowSymlinks
AllowOverride All
Require all granted

<Directory /srv/git>
Options FollowSymlinks
AllowOverride All
Require all granted

Obviously you will need to change the certificate files and the private key to your own, and the ServerName directive to your own.

The SSLCipherSuite directive is appropriate for the Apache server distributed by LibreLAMP but may need to be adjusted if you chose not to install LibreLAMP but instead are using stock CentOS 7 Apache. For example, I do not believe stock CentOS Apache supports the ChaCha20 ciphers.

Install the apache configuration file as /etc/https/conf.d/gitlist.conf

Start the Apache daemon!

[root@git conf.d]# systemctl start httpd.service

If there were no errors in your configuration, configure it to start at system boot:

[root@git conf.d]# systemctl enable httpd.service

It is always a good idea to check the external TLS security of your server at sslabs. With the above configuration, this is how my git server rates:

Not bad 🙂

To view an example of the GitList web view:

Not many repositories there now, but it will grow. First I have to finish importing all my private repos, then I will import the rest of my public repos from github.

System Updates

Make sure to update your CentOS system appropriately.

I will add a separate page with instructions on how to get CentOS to e-mail you when there are available updates to install, but do not depend on that, it is good to log in once a week and manually check for updates. I do not advocate setting up CentOS 7 to automatically update. The problem is if something goes wrong, you are not logged in to fix it. That actually happened to me once, MySQL failed to restart after an automated update. Years ago, but it always is a potential problem.

The TLS certificate expires every three months. You can set up certbot to automatically update your apache configuration, but I do not like that practice personally.

The shell script I gave here can be used to fetch a new certificate. Make sure to shut down the web server before running it or it will not work.

Remember that it only replaces the private key about once a year. When updating the Apache configuration, do not change the embedded date in the private key path unless it actually has generated a new private key.

Future Vision

I would like to see federation. What I mean by that, developers run their own git repos on their own servers so they are never subjected to workflow issues because a centralized code repository was sold to someone else, but at the same time, developers can belong to a federation of developers with a centralized url that can be browsed much like we love to browse github. It’s just that when we find a project we like and want to clone, we clone it from the server the developer runs.


Create a Table of Contents for this page. For GitList to fix some issues I have it with, and add some features, such as bug tracking.

Figure out how to integrate it with Travis CI for automated build and unit testing. Etc.

Please Comment

Your comments are most welcome.

Leave a Reply

Your email address will not be published. Required fields are marked *

Anonymity protected with AWM Pluggable Unplugged