Hosting Django under different locations with Nginx and gunicorn

September, 15 2011

Do you know how Django uses SCRIPT_NAME?

It's not often I host different instances of Django under different locations on the same domain. One legacy install has been setup that way for over a year with Apache2 and mod_wsgi (with Nginx infront). Almost all of my new deployments use virtual hosts with different subdomains for each service.

If you are using virtual hosts with Nginx it looks like this:

server {
    listen   80;
    server_name  service.albertoconnor.ca;

    # other settings

    location / {
        try_files $uri/index.html $uri.html $uri @cluster;
    }

    location @cluster {
        proxy_pass http://service.albertoconnor.ca;
    }
}

Assuming you have defined an upstream server cluster called "service.albertoconnor.ca".

If you are doing something by location on the same domain it might look something more like this:

server {
    listen   80;
    server_name  _;

    # other settings...

    location /a/ {
        proxy_pass http://127.0.0.1:8001/;
    }

    location / {
        proxy_pass http://127.0.0.1:8080/;
        proxy_redirect off;
    }
}

The idea is that something else, let's say Apache2, is running on 127.0.0.1:8080, and Django being served by gunicorn is running on 127.0.0.1:8001. So under /a/ the pages will be generated by Django; anywhere else Apache2 takes over.

If you try this though, things will go horribly wrong. Besides redirects not working because you need some proxy_redirect mojo, your generated links in your templates won't work. They will generate absolute urls like "/foo/bar" instead of "/a/foo/bar". But really how should {% url %} know any better.

Then I got that feeling. You know when you figure out why something doesn't work, but because of that reason you can't explain why other things ever worked in the first place. I have been successfully serving different Django services based on location with Apache2 and mod_wsgi for over a year. How, when I generate urls, could it know to prepend the location?

The answer is that mod_wsgi WSGIScriptAlias command was helping me. Django will actually do the magic prepend for url generation if the SCRIPT_NAME variable is set in the wsgi environment. mod_python also does this by the django.root PythonOption. The SCRIPT_NAME variable is mentioned in the gunicorn FAQ very briefly and without context.

From the FAQ and other hints around the internet I gathered that I really just need to set the header SCRIPT_NAME HTTP header myself and things will start working. Since the SCRIPT_NAME is also taken off of incoming urls I had to alter my proxy_pass a bit. Here is what I ended up with:

    # ...
    location /a/ {
        proxy_pass http://127.0.0.1:8001/a/;
        proxy_redirect http://127.0.0.1:8001/a/ http://$host/a/;
        proxy_set_header SCRIPT_NAME /a;
    }

    location / {
        proxy_pass http://127.0.0.1:8080/;
        proxy_redirect off;
    }
    # ...

I also needed a slightly different proxy_redirect directive than the default. This may not be ideal so suggestions are welcome. I will apply them to this post. You can comment or contact me though this website.

Update Oct 26th, 2011

When using CAS with services I discovered the redirection url has the internal host IP rather than the proxy host name. One way to fix it is to use the X_Forwarded_Host header. In the /a/ block add:

        proxy_set_header X-Forwarded-Host your.host.name.com;

Internally Django has a HttpRequest.get_host method that if the setting USE_X_FORWARDED_HOST is True will use the value of the HTTP-X-FORWARDED-HOST header as the current request's host.

USE_X_FORWARDED_HOST = True

NOTE: The setting is new as of Django 1.3.1 and it is worth reading security advisory, see the section headed "Host header cache poisoning". The up shot of which is in this case seems to be to filter on your specific host and not "server _;" as I did in my example above to avoid a security hole.


Tweet comments, corrections, or high fives to @amjoconn