Running Mercurial with FastCGI in nginx

logo-droplets-200 Mercurial is a so called DRCS (Distributed Revision Control System). I have been using Subversion for a couple of years, both at work and for my own projects. Now I thought it was about time to try something different.

But first, why do I want to switch from SVN to Mercurial? Basically the most appealing argument for me was the fact, that with Mercurial I am able to work offline with my repository. Besides that, I always had issues with the way SVN was handling tags and branches. Especially merging changes from a branch back into the trunk was always a pain. I did not need to use that functionality often but when I did, I always ended up doing it twice, because I could not remember which way to do it right.

So, in this article I will describe my setup of Mercurial served via FastCGI behind the nginx webserver. The approach is similar to the integration of PHP into nginx. You need the spawn-fcgi tool from the lighttpd distribution. The following steps should work on a recent Debian or Ubuntu distribution.

The first step is to install Mercurial and some necessary libraries to create the fcgi-wrapper for Mercurial:

aptitude install mercurial python-flup

Now you can already start defining your Mercurial repositories. Here are some steps to create a small example repository:

cd /tmp
hg init hgtest
cd hgtest
echo "Hello world." > readme.txt
hg add readme.txt
hg commit -m "Initial commit"

You now have a Mercurial repository with a single file. The command hg log should show you a single changeset with our commit comment.

Now, let’s start configuring nginx to integrate Mercurial. We will use FastCGI talk connect Mercurial to nginx. User authentication will be done via nginx. It is a good idea to use HTTPS for communication with Mercurial, but we will focus on a standard HTTP setup.

Let’s setup a virtual host for Mercurial. Open /etc/nginx/sites-available/your_domain_name and add the following server definition:

server {
        listen 80;
        server_name YOUR_MERCURIAL_DOMAIN;
 
        location / {
                auth_basic "Secure Login";
                auth_basic_user_file /tmp/mercurial_users;
                fastcgi_pass 127.0.0.1:9001;
                fastcgi_param SCRIPT_FILENAME /tmp$fastcgi_script_name;
                fastcgi_param PATH_INFO $uri;
                fastcgi_param REMOTE_USER $remote_user;
                include fastcgi_params;
        }       
}

The above settings will setup a new virtual host, where all traffic is redirected to the Mercurial FastCGI wrapper. It is important that you forward the PATH_INFO and REMOTE_USER variables. Mercurial will not work correctly without these.

Now reload the nginx configuration:

/etc/init.d/nginx reload

And create the password file:

htpasswd -c /tmp/mercurial_users MYLOGIN

Mercurial uses a central configuration file. In this file we can specify locations for our mercurial repositories. The following file will enable all mercurial repositories found in the /tmp directory. It also changes the theme to gitweb which is a bit clearer than the default theme. Create the file /tmp/hgweb.config with the following contents:

[collections]
/tmp = /tmp
 
[web]
style = gitweb
baseurl =

Mercurial uses a second configuration file for each repository where you may specify details about the repository and security settings like who may push changes into the repository. The configuration should be placed into the file /tmp/hgtest/.hg/hgrc and could look like this:

[web]
contact = YOUR NAME
description = DESCRIPTION OF PROJECT
style = gitweb
push_ssl = false
allow_archive = bz2 gz zip
allow_push = LOGIN_NAME

Now we need to grab the following script from the Mercurial repository and place it into the /tmp directory: http://selenic.com/repo/hg/raw-file/tip/contrib/hgwebdir.fcgi. Now edit this file and replace the line WSGIServer(hgwebdir('hgweb.config')).run() with WSGIServer(hgwebdir('/tmp/hgweb.config')).run().

The last step is to set the correct filesystem rights for your repository:

chown -R www-data.www-data /tmp/hgtest

That’s it. Now we can start the FastCGI process via:

spawn-fcgi -a 127.0.0.1 -p 9001 -u www-data -g www-data -f /tmp/hgwebdir.fcgi -P /var/run/fastcgi-mercurial.pid -C 1

It makes sense to write the above line into /etc/rc.local so that it will start up automatically when you reboot the server.

Further information about configuring your Mercurial server can be found in the Mercurial Wiki.

12 thoughts on “Running Mercurial with FastCGI in nginx

  1. This looks quite interesting and I am eager to give it a try. However, in terms of security I guess you are also using fail2ban for this :)
    Is it sufficient just do create a jail for nginx or is there more one should consider?

    Cheers,
    Michael

  2. Yes, I am indeed using fail2ban to secure my repository. I have defined a new filter file /etc/fail2ban/filter.d/nginx-auth.conf to identify failed login attempts in the nginx access log (note: nginx does not log failed attempts to the error.log like Apache):

    [Definition]
    failregex = <HOST>.*" 401

    This filter is then called from a custom jail definition:

    [mercurial]
     
    enabled = true
    port = https
    filter = nginx-auth
    logpath = /var/log/nginx/access.log
    maxretry = 3

    So far this is working out nice.

  3. I ran into problems where an empty repository list was shown.
    To solve this, you have to edit the file /tmp/hgwebdir.fcgi
    Replace this line at the bottom:
    WSGIServer(hgwebdir('hgweb.config')).run()
    with a direct reference to the hgweb.config file:
    WSGIServer(hgwebdir('/tmp/hgweb.config')).run()

  4. After running

    spawn-fcgi -a 127.0.0.1 -p 9001 -u www-data -g www-data -f /tmp/hgwebdir.fcgi -P /var/run/fastcgi-mercurial.pid -C 1

    I get the following

    spawn-fcgi: child exited with: 2

    Any thoughts?

  5. To answer my own problem.

    The executable bit needs to be set on hgwebdir.fcgi
    (chmod +x hgwebdir.fcgi)

    Otherwise we get:
    spawn-fcgi: child exited with: 2

    Also (I could be incorrect), but instead of:
    chmod -R www-data.www-data /tmp/hgtest

    I think it was meant to be:
    chown -R www-data.www-data /tmp/hgtest

    Thanks for the great post!

  6. Thanks for the how-to! Got things up and running. One question: How do you do public access to the repository while restricting pushes? The problem is illustrated in the nginx config below:


    server {
    listen xx.xxx.xxx.xxx:443;
    server_name hg.curtisjewell.name;
    server_name hg;
    ssl on;
    ssl_certificate /.../hg.chained.crt;
    ssl_certificate_key /.../hg.key;
    access_log /.../hg.log;

    keepalive_timeout 70;

    location / {
    fastcgi_pass 127.0.0.1:xxxxx;

    fastcgi_param PATH_INFO $fastcgi_script_name;
    fastcgi_param QUERY_STRING $query_string;
    fastcgi_param REQUEST_METHOD $request_method;

    fastcgi_param CONTENT_TYPE $content_type;
    fastcgi_param CONTENT_LENGTH $content_length;
    fastcgi_param SERVER_PROTOCOL $server_protocol;
    fastcgi_param SERVER_PORT $server_port;
    fastcgi_param SERVER_NAME $server_name;

    fastcgi_param GATEWAY_INTERFACE CGI/1.1;
    fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;

    fastcgi_param REMOTE_ADDR $remote_addr;
    fastcgi_param REMOTE_PORT $remote_port;
    fastcgi_param REMOTE_USER $remote_user;

    # The commented lines, when commented, make pushing return a 401 because the user is not passed to hgweb.
    # limit_except GET { # do this for all requests but GET requests
    auth_basic "Mercurial repositories on curtisjewell.name";
    auth_basic_user_file /var/hg/hgusers_basic;
    # }
    }
    }

    Mercurial 1.4.3, nginx 0.7.65, by the way.

  7. Hi Curtis, unfortunately I don’t have a solution for this problem. I also tried to get the limit_except approach working, but without success. I don’t need public access to my repository, so I did not investigate further.

Leave a Reply

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

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">