It is always a good idea to keep your projects under version control. Once you get into the habit of using Git, you might warm to the idea of pushing your repository to a web-hosted service such as Github or Gitlab. They are undoubtedly useful for collaboration, sharing and much more.

But for some projects, whether personal or business, using a hosted service is not acceptable (remember, “the cloud” is just someone else’s computer). Even so, you can still get a nice web-based front-end for your Git repos. You will just have to self-host it. Ideally on hardware that you own, but in lieu of that a VPS will do.

My own motivation for self-hosting a Git server is that I would like to have access to the capabilities such a service offers while I work on my thesis, which I’m not comfortable hosting in the cloud (not until it is done, anyway).

Currently, your choice of front-ends boils down to Gitlab CE, Gogs or Gitea.1 Gitea is a fork of Gogs. Both of them are very light on system resources. Gitlab Community Edition is a monster in comparison. For a non-professional coder like myself, Gitea will be just fine.

The rest of this post describes how I setup Gitea on a server of mine. You can have a look at the finished product at https://git.chepec.se (it’s not much to look at). Before jumping off, I should note that there are already several “how to setup Gitea” guides published by others, so this will not be a step-by-step instruction. For that, I can recommend any of the following:

MySQL database for Gitea

# install mysql from ubuntu repos
sudo apt install mysql-server mysql-client libmysqlclient-dev 
# check the installed mysql version
mysql --version
# secure the mysql installation
sudo mysql_secure_installation
# login to mysql as root
sudo mysql -u root -p

While logged in as MySQL root user, create the user and database for Gitea.

CREATE USER 'gitea'@'localhost' IDENTIFIED BY 'somelongandcomplexpassword';
CREATE DATABASE gitea DEFAULT CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
GRANT ALL ON gitea.* TO 'gitea'@'localhost';
FLUSH PRIVILEGES;

We explicitly set UTF8 for the Gitea database above because the Gitea docs explicitly recommend using InnoDB and UTF8 with MySQL. Recent versions of MySQL should default to using InnoDB (instead of MyISAM), but we can check that ourselves by (still logged in as MySQL root):

mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.00 sec)

Apache vhost with Let’s Encrypt certificate

This virtualhost configuration is for Apache v2.4.X. Created the file /etc/apache2/sites-available/git.chepec.se.conf, with the following contents:

<VirtualHost *:80>
  ServerAdmin email@chepec.se
  ServerName git.chepec.se
  ServerAlias gitea.chepec.se

  Redirect permanent / https://git.chepec.se/
  
  ErrorLog ${APACHE_LOG_DIR}/git.chepec.se_error.log
  CustomLog ${APACHE_LOG_DIR}/git.chepec.se_access.log combined
</VirtualHost>

<VirtualHost *:443>
  ServerAdmin email@chepec.se
  ServerName git.chepec.se
  ServerAlias gitea.chepec.se

  SSLEngine on
  SSLCertificateKeyFile   /etc/letsencrypt/live/git.chepec.se/privkey.pem
  SSLCertificateFile      /etc/letsencrypt/live/git.chepec.se/cert.pem
  SSLCertificateChainFile /etc/letsencrypt/live/git.chepec.se/chain.pem

  ProxyPreserveHost On
  ProxyRequests Off
  ProxyPass / http://192.168.1.444:3000/
  ProxyPassReverse / http://192.168.1.444:3000/
        
  ErrorLog ${APACHE_LOG_DIR}/git.chepec.se_error.log
  CustomLog ${APACHE_LOG_DIR}/git.chepec.se_access.log combined
</VirtualHost>

Before enabling this site, we need to have DNS records setup for the defined sub-domains. Once the DNS records have propagated, we can use certbot to fetch our Let’s Encrypt certificates.

# this one-liner stops the Apache server, fetches the certs, and restarts Apache
sudo certbot certonly --authenticator standalone --pre-hook "systemctl stop apache2.service" --post-hook "systemctl start apache2.service" --email email@chepec.se -d git.chepec.se

With that out of the way, it’s time to enable the Apache site configuration:

sudo a2ensite git.chepec.se.conf

When everything else is setup, you can let the Apache config take effect by issuing sudo systemctl reload apache2.service.

Running Gitea as a systemd job

[Unit]
Description=Gitea (Git with a cup of tea)
After=syslog.target
After=network.target
After=mysqld.service

[Service]
RestartSec=2s
Type=simple
User=gitea
Group=gitea
WorkingDirectory=/var/lib/gitea/
ExecStart=/usr/local/bin/gitea web -c /etc/gitea/app.ini
Restart=always
Environment=USER=gitea HOME=/home/gitea GITEA_WORK_DIR=/var/lib/gitea

[Install]
WantedBy=multi-user.target

There is nothing special here, this systemd configuration is directly from the Gitea docs. I would just like to say a few words regarding the Gitea system user. As you can see, the systemd job specifies that Gitea is running as the user gitea. This should correspond to the user in RUN_USER in the /etc/gitea/app.ini file (unless you are doing something very special, I guess).

Here I would like to point something out that I learned the hard way. The Gitea process should run in a dedicated user account, as defined in systemd or similar and /etc/gitea/app.ini. Otherwise Git over SSH (i.e., pushing/pulling/cloning of your Gitea repos) will be a complete mess, or most likely, not work at all. This was perhaps not obvious from reading the docs, but is pointed out clearly enough in the Gitea forums. Gitea also expects to handle the /home/gitea/.ssh/authorized_keys file by itself, so don’t even bother trying to setup SSH keys for this user yourself. If you did, follow the instructions here to reset this file.

Gitea paths on NFS share: a bit of a challenge

This section is particular to my setup. I would like to point the Gitea repository root path to my RAID6 array on my NAS, mainly for reasons of redundancy (this way I don’t have to worry if the HDD suddenly dies, and also, I like centralising important files, makes backups a little easier to plan).

The RAID array has always been shared over the LAN as an NFS share. Mounting it on the Gitea server is thus very easy, just add the following line to your /etc/fstab file:

192.168.1.111:/media/share   /media/share   nfs   rw,hard,intr   0   0

To help keep things straight I will from now on refer to the Gitea-running machine as the NFS client and the NAS as the NFS server.

The challenging part concerns Linux user permissions. We have already established that Gitea needs to run as its own user. This presented an interesting problem: the Gitea user on the NFS client needs write permissions on the Gitea repository path on the NFS server. How do we achieve that?

As usual when it comes to all things Linux, we have a few alternatives. In my estimation, they are:

In the end, I created the same user with identical uid/gid on both machines, and then simply set that user on the NFS server as the user and group owner of the Gitea repository root tree. It went like this:

# on the NFS server, created the user 'gitea'
$ sudo adduser \
     --system \
     --shell /bin/bash \
     --gecos 'Gitea user' \
     --group \
     --disabled-password \
     --no-create-home \
     gitea
# check the uid/gid
$ id gitea
uid=126(gitea) gid=122(gitea) groups=122(gitea)

Now all we have to do is create a user gitea with the same uid/gid on the NFS client (the NFS server has more existing user accounts, so creating the user there first minimised the risk of the uid/gid clashing with an existing one on the NFS client).

# on the NFS client, created the user 'gitea'
$ sudo adduser \
     --system \
     --shell /bin/bash \
     --gecos 'Gitea user' \
     --group \
     --disabled-password \
     --home /home/gitea \
     gitea
# check the uid/gid
$ id gitea
uid=113(gitea) gid=114(gitea) groups=114(gitea)
# change user id
$ sudo usermod -u 126 gitea
# change group id
sudo groupmod -g 122 gitea

And finally, we need to fix the uid/gid of all files owned by gitea. If you know where all such files are (and in this case we do, since they should only be inside /home/gitea at this point) this can be achieved by simply reassigning ownership (chown -R gitea:gitea /home/gitea). Otherwise, you could use something like find / -uid 113 ! -type l -exec chown gitea:gitea {} \;.

Issuing id gitea on the NFS client should now display the same uid/gid as on the NFS server.

Setting up the user on both machines could be considered a violation of DRY (Don’t Repeat Yourself, although I’m not sure if that applies to systems administration), but it’s also low-maintenance and (at least for me) required the least amount of time to accomplish. I did actually try the nobody/nogroup approach, but that did not give Gitea the required permissions on the NFS path, so I moved on. And this works.


  1. There is actually an alternative to self-hosting: encrypt the contents of your git repo and keep using a hosted service as usual. Hard to do properly on your own, though. But keybase has a neat implementation of that.