A server with Docker, part 4: Git hosting and static content

The fourth article in a series about the process of creating a flexible, multi-purpose webserver with Docker on a Digital Ocean droplet. By the end of this post the server will be able to serve static HTML content using Nginx and to provide glorious multi-user Git hosting with Gitolite.

May 24, 2014 [ nginx | server | gitolite | git ]

After locking down the server and building a solid foundation in the previous post, we can turn to more practical matters. By the end of this post the server will be able to serve static HTML content with the help of Nginx and to provide glorious Git hosting with Gitolite.

Git Repositories With Gitolite

Gitolite is best described as ‘an access control layer on top of git’. It has succeeded Gitosis as the package of choice to host git repositories for multiple users without the hassle of manually setting up additional unix users or complex web applications. Among others it provides an incredible ease of use, a clean git-based configuration interface, fine-grained access control and all-around awesomeness. The documentation of Gitolite is very detailed and well crafted. The section about Gitolite administration is the place you probably want to consult, first thing after it is up and running.

An obvious prerequisite are the Git packages. We will also need to create a git user on the server which will house Gitolite. All interactions will happen through SSH via this user, regardless of repository and the actual person connecting.

$ apt-get install git git-core
# create the user git with a home directory, a user group,
# a system account and make it use bash as login shell
$ useradd -m -U -r -s /bin/bash git

Gitolite will need to know who to trust from the get-go. For this we will provide it with an initial public key for the first user. Upload your public key to the server from your local machine:

# as usually, HOSTNAME needs to be replaced by the
# appropriate value in your local ~/.ssh/config
$ scp ~/.ssh/id_rsa.pub HOSTNAME:/tmp/

Once again on the server the file needs to be made readable by common people so we can access it later:

# assuming your key is named id_rsa.pub
$ chmod a+r /tmp/id_rsa.pub

With all this, we can finally install and setup Gitolite:

# install gitolite
$ apt-get install gitolite
$ su git; cd
# and tell Gitolite to get going
$ gl-setup /tmp/id_rsa.pub

if the last command asks for choices, it’s the best to go with the default options unless you have valid reasons for deviations in mind. Gitolite is now ready for actual use! Time to clean up and get to know the process of making Gitolite do what we want. The process of administration consists of editing files in an administration repository and pushing the changes back to the server. On your local machine, clone the admin repository from Gitolite in a folder of your choice:

$ git clone git@HOSTNAME:gitolite-admin.git

As usually with clone, this should create a new directory called gitolite-admin which contains two directories. The keydir is meant to contain public keys, with one already present. The part before .pub is the user name that is used in the config file. The conf directory contains the main configuration file gitolite.conf. Currently it specifies two repositories: the one we just cloned and an empty repository called testing. As a warmup, let’s delete the testing repository and change our username in the config file from id_rsa to something more personal.

To make the testing repository unaccessible, just delete the corresponding two lines from the configuration file. To change the name of the user name two steps are needed: the file in the keydir (probably id_rsa.pub) must be renamed to username.pub, and its mention in gitolite.conf must be changed from id_rsa to username (notice the .pub missing in the config file).

To apply these changes, they need to be committed and pushed with git. You probably want to use something like *git gui’ to craft proper, granular commits in the long run, but the following will do as well, executed in the repository directory on your local machine:

$ git commit -am "First steps."
$ git push

As soon as the push is completed, the changes are applied by Gitolite and no user can access testing.git anymore! To remove the testing repository completely, it needs to be deleted it by hand on the server:

$ rm -rf /home/git/repositories/testing.git

A new repository can be created by adding a new entry to the config file. The following two lines result in a new repository called blog that can be only access by the user username:

repo    blog
        RW+ = username

After committing and pushing the admin repository, you can immediately clone the new blog repository from your local machine:

$ git clone git@HOSTNAME:blog.git

No manual creation of repositories on the server and no worries of who can read what! As this example is rather simple, it must be stated that Gitolite really shines when you need to work with a multitude of users, repositories and different access rights.

As stated earlier, you can read more about details of handling Gitolite in the official documentation. As a secondary source of information about Gitolite and Git in general, the freely available Pro Git book is an excellent reference on most Git related topics.

Hosting this Blog

How about we create a reason to use new blog repository created earlier? Oh boy, what a transition. To be more precise, let’s host a static HTML project under a certain domain pointing to this server. If you followed the past few posts, the default Nginx hello-world page is all that currently appears when trying to access the domain (or the server IP) in the browser.

A first step is to stop Nginx from greeting everybody. The future content needs a place to live in, preferably someplace that Nginx can access, my choice is the /var/www directory:

$ mkdir -p /var/www
# change the owner of the directory
$ chown -R www-data:www-data /var/www
# and make sure the group sticks for any future content of the directory
$ chmod +s /var/www

www-data is the default user which Nginx workers assume. The folder is world-readable in any case so we could do without the last 2 commands, but signing the web-related content over to this particular group is a good idea in the long run.

As the Nginx configurations are independent of the content quality, the following two commands create some dummy content for the sake of working with a reproducible example. As a side note: Pelican is an excellent Python-based static site generator which I use for this blog.

$ mkdir /var/www/blog
$ echo '<html><head><title>Blog</title></head><body>Hello World!</body></html>' > /var/www/blog/index.html

In case you find html markup repulsing: this page is titled ‘Blog’ and will contain a friendly greeting to everybody who cares to feel addressed. The only thing left to do is to tell Nginx to serve it. This is done through the Nginx configuration files:

# change into the main configuration directory
$ cd /etc/nginx/sites-available

The default file in this directory contains the server entry that is responsible for the default Nginx greeting page. The sites-available directory serves as a kind of configuration staging ground, the files contained here are not activated right away. The content of /etc/nginx/sites-enabled is where active configuration files are located, customary it contains only softlinks to files in the sites-available directory. Let’s deactivate the default page by deleting the link and reload the Nginx service to see the result:

$ rm /etc/nginx/sites-enabled/default
$ service nginx reload

Nothing can be reached anymore and the only thing you see when trying to access the server IP from your browser is an error message. Hooray for broken things! As stated earlier, the default configuration file is worth a thorough examination as it contains useful information, examples and helpful links. To host the blog, we need to create a new server entry. To keep it clean, let’s create a new configuration file called blog in the sites-available directory with the following content:

server {
    listen      80;
    server_name blog.th4t.net;
    index       index.html;
    root        /tmp/www/blog;

I hope the entry is self-explanatory. Instead of the blog.th4t.net domain you should probably specify one pointing to your server or _ to simply catch everything. To activate this entry you need to create a symbolic link in the sites-enabled directory and ask Nginx to kindly reconsider the changed settings:

$ cd /etc/nginx/sites-enabled
$ ln -s ../sites-available/blog
$ service nginx reload

If you access the IP (or domain) associated with the server in your browser once again, you are greeted by the page we crafted earlier, containing a customary tribute to the gods of bits and bytes. Content served, work complete!

As a bonus, let’s add redirection from any th4t.net URL to the canonical blog URL. Just append the following lines to the blog configuration file and reload Nginx. To see the result try to access a bogus subdomain.

# redirect *th4t.net to blog.th4t.net
server {
    # you may add default_server here to catch everything
    # see http://nginx.org/en/docs/http/server_names.html
    listen      80;
    server_name th4t.net;

    # a permanent redirect to the canonical URL
    return  301 http://blog.th4t.net$request_uri;

    # an alternative to the return would be:
    #rewrite ^/(.*) http://blog.th4t.net/$1 permanent;
    # but it is harder to read and frowned upon, as described in
    # http://wiki.nginx.org/Pitfalls

If th4t.net is accessed directly with this entry, Nginx responds with the suggestion to rather visit the blog URL. Because of the way Nginx is performing the server name matching, this new rule will not apply to a blog.th4t.net URL, as the exact match is chosen above the redirecting one. Once again, the online documentation of Nginx is very helpful as a reference and if you want to learn more about its configuration and best practices.

Coming up: in the next post we will get started with building Docker containers! To reduce the chance of making hasty decisions, a serious effort to understand the nature of Docker containers is a good thing to do. Some important details can be considered before we get the chance to learn from mistakes. Interesting directions of inquiry that are independent of the container structure include the ways how containers will be started, kept running, restarted and accessed from the outside. Regarding containers themselves, there are the topic of persistence and the choice to what extent coupling/interaction among single containers is desired. Many things to learn and much fun ahead!

Get emails from me: