News, ideas and randomness

Handling Subdomains in Django

Posted: December 12th, 2008 | Author: Scott Barnham | Filed under: Django | 1 Comment »

This is the first part of our series on some of the more interesting tech we’ve developed for Red Robot Studios. We’re working from webserver up, so we thought that subdomains would be a good place to start.

Subdomains are useful when you want to host multiple sites with the same code and different data. For example, providing websites for clubs where each club has its own subdomain. In Django, you could have a Club model and some associated models holding data. When a user visits alpha.example.com, you want to show the data from the Club model instance for alpha.

DNS Subdomains and Wildcards

You could add individual subdomains using DNS CNAMEs or A records, but if you want to generate objects in your Django app on the fly, have a look at wildcards. To match any subdomain you add “*” as a subdomain. It looks something like:

*.example.com 14400 IN A 208.77.188.166

This will match www.example.com as well as alpha.example.com, beta.example.com, etc.

Webserver Wildcards

The webserver config needs a similar setting so it knows to respond to any subdomain.

In Nginx, it looks something like:

server {
    listen       208.77.188.166:80;
    server_name  example.com *.example.com;
...

In Apache:

<VirtualHost 208.77.188.166>
    ServerName example.com
    ServerAlias *.example.com
...

If you want to use SSL certificates on your subdomains, you need to get a “wildcard subdomain certificate”. They cost more than a regular certificate, but are necessary to provide a valid certificate for any subdomain on your site. We go ours from RapidSSLOnline.

This should be enough config for your Django app to receive requests for any subdomain. Now your Django app needs to respond appropriately for each subdomain.

Getting the Subdomain in Django

There are a few different ways to do it, but we went with a piece of middleware that gets the subdomain from the request and retrieves a matching model.

We have added two additional settings: DOMAIN_MIDDLEWARE_MODEL and DOMAIN_MIDDLEWARE_INSTANCE_NAME to our settings.py so we can specify the model which the middleware queries, and the name which the instance is given when added to our request instance.

DOMAIN_MIDDLEWARE_MODEL = 'core.Club'
DOMAIN_MIDDLEWARE_INSTANCE_NAME = 'club'

The model is assumed to have a field named “slug”, which the middleware uses to match the subdomain against an instance of the model.

Right, so let’s create our middleware:

class DomainMiddleware(object):
    """Gets the correct instance of an application-specific model by matching the
    sub-domain of the request."""

    def __init__(self):
        self.site_domain = Site.objects.get_current().domain
        if self.site_domain.startswith('www.'):
            self.site_domain = self.site_domain[4:]
        self.SUBDOMAIN_RE = re.compile(r'^(?:www\.)?(?P<slug>[\w-]+)\.%s' % re.escape(self.site_domain))
        try:
            app_name, model_name = settings.DOMAIN_MIDDLEWARE_MODEL.split('.', 2)
            self.model = get_model(app_name, model_name)
            self.instance_name = settings.DOMAIN_MIDDLEWARE_INSTANCE_NAME
            assert self.instance_name
        except (AttributeError, AssertionError):
            raise ImproperlyConfigured('DomainMiddleware requires DOMAIN_MIDDLEWARE_MODEL and DOMAIN_MIDDLEWARE_INSTANCE_NAME settings')

In our init method we do some basic setup like creating a regex which will match the subdomain slug, and loading in our model using django.db.models.get_model with the app.model args from DOMAIN_MIDDLEWARE_MODEL.

    def process_view(self, request, view_func, view_args, view_kwargs):
        """If domain is not main site, check for subdomain.

        Get the model from the subdomain slug.
        """
        port = request.META.get('SERVER_PORT')
        domain = request.META.get('HTTP_HOST', '').replace(':%s' % port, '')
        if domain.startswith('www.'):
            domain = domain[4:]
        if domain != self.site_domain:
            match = self.SUBDOMAIN_RE.match(domain)
            if match:
                slug = match.group('slug')
                instance = get_object_or_404(self.model, slug=slug)
            setattr(request, self.instance_name, instance)
        return None

In process_view we grab the subdomain from the HTTP_HOST header of the request, and using get_object_or_404 we load the correct instance of the model and set it as an attribute on our request object with the name given in DOMAIN_MIDDLEWARE_INSTANCE_NAME.

When someone goes to alpha.example.com the middleware picks out alpha and gets the Club instance with slug=alpha and adds it to request to be used in views.

The middleware uses Site.objects.get_current() to get the base URL, so make sure you have Site set up properly or none of your subdomains will match.

The advantage of having the middleware load in the correct instance for you is that your views can simply use the club attribute to access all related data for this club.

def index(request):
    members = request.club.members.approved().order_by('-creation_date')
    return list_detail.object_list(
        request                 = request,
        queryset                = members,
        template_name           = 'club/index.html',
        template_object_name    = 'member'
    )

This is extremely useful when you want to ensure that your data is correctly filtered: you don’t need to have each view filter based on the subdomain, which is pretty error-prone, and if you get it wrong a user’s data would end up on someone else’s page. This way is a lot simpler.


One Comment on “Handling Subdomains in Django”

  1. 1 Damian said at 10:19 am on March 13th, 2015:

    Thanks alot, looked so much for this info!


Leave a Reply