Using Django for (mostly) static sites

2014-02-15 by Senko Rašić

While Django is a great framework for building websites and web applications, it’s often considered as “too big” for fulfilling simple needs, such as mostly static content or single-page sites. While alternatives like static website generators (such as Jekyll or Hyde) or microframeworks (such as Flask) indeed are more lightweight in their default setup, Django isn’t neccessarily too big for the task. If you’re familiar with Django already, it may very well be a better option than having to learn another tool.

This tutorial describes how to set up Django to server a mostly static content site, while still taking advantage of templating system and making it easy to add interaction such as contact or subscribe forms.

Our setup will be a pure-Python system, not dependent on any other software, compatible with both Heroku and the usual Linux server/VPS setup.

We’ll be using virtualenv to keep all the dependencies in one place and avoid polluting our system with them. We’ll also be using virtualenvwrapper because it makes using virtualenv so much easier.

Let’s get started.

Development setup

Let’s make a new virtual environment for our project and install Django in it:

$ mkvirtualenv --no-site-packages mysite
$ workon mysite
$ pip install django

Let’s prepare a repository (I use git here, but any works well) and create a new Django project:

$ django-admin.py startproject mysite

So far, standard Django stuff. Let’s open up mysite/mysite/settings.py and make things more interesting. For our purposes, this is the complete settings.py needed:

import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
SECRET_KEY = '...' # must be unique for your project
DEBUG = TEMPLATE_DEBUG = False
ALLOWED_HOSTS = ['mysite.com'] # must match your site domains
INSTALLED_APPS = (
    'django.contrib.messages',
    'django.contrib.staticfiles',
)
MIDDLEWARE_CLASSES = (
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
ROOT_URLCONF = 'staticsite.urls'
WSGI_APPLICATION = 'staticsite.wsgi.application'
STATIC_URL = '/static/'
STATIC_ROOT = 'staticfiles'

We won’t go over what each of the settings variables does, but do look them all up, it’s only a handful anyways.

Notice there’s no database, no session, and no authentication framework! We don’t need any of them, so we simply stripped them out. We did keep the messages framework and CSRF and clickjacking protection, as they will be useful if we have contact form on the site.

By the way, you may want to start with DEBUG being True, as it will make it easier to spot errors while you set things up. Make sure you switch it back to False before deploy, though!

Next, open up mysite/mysite/urls.py and get rid of the admin-related stuff there (put there by default).

After this, you should have a trimmed down Django site that does absolutely nothing. Test it out:

$ cd mysite
$ python manage.py runserver

Everything seems to be in order, let’s commit that as the initial commit in the repository:

$ git add manage.py mysite/*.py
$ git commit -m 'initial commit'

The static pages app

Now that we’ve got Django up and running, let’s create a Django app that will actually serve our pages:

$ python manage.py startapp pages

We don’t actually need admin support and won’t write any tests for this, so we can get rid of the created-by default unneccessary files:

$ rm pages/admin.py pages/tests.py

We don’t need anything in pages/models.py but Django does require it in order to consider the package to be an app, so the file must be present. It can be blank though:

$ rm pages/models.py
$ touch pages/models.py

We need to add the app to INSTALLED_APPS in settings.py:

INSTALLED_APPS = (
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pages',
)

And make it responsible for all of the URLs that Django will serve, so now mysite/urls.py will look like:

from django.conf.urls import patterns, include, url

urlpatterns = patterns('',
    url(r'', include('pages.urls'))
)

First content

Finally, let’s add some content. We’ll define the URLs and templates directly in pages/urls.py, and we’ll be making use of TemplateView so we don’t need to add any custom code.

Let’s create it with the following content to start with:

from django.conf.urls import patterns, url
from django.views.generic import TemplateView

urlpatterns = patterns('',
    url('^$', TemplateView.as_view(template_name='index.html')),
)

Now we just need to add the content. The templates will be looked up in pages/templates directory by default, so let’s create it and add index.html there:

<html><body><h1>Hello, Django!</h1></body></html>

After verifying it works with runserver, let’s commit those changes as well:

$ git add mysite/settings.py mysite/urls.py pages/*.py pages/templates/index.html
$ git commit -m 'add static pages app'

The static assets will be looked up in pages/static directory, this is the default Django staticfiles behaviour.

Serving production

The built-in runserver is fine and well, but it’s not designed for actual production use. A popular Python web application server is gunicorn, and that’s what we’ll be using here:

$ pip install gunicorn

Once installed, you can run it with:

$ gunicorn mysite.wsgi:application

Oh noez! Gunicorn doesn’t serve the static assets by default! This is because Django does a terrible job of serving them and doesn’t even pretend it’s usable for non-development (non-runserver) use. We must find another solution.

A nice solution is dj-static, a WSGI app that can wrap Django and take over serving the static contents:

$ pip install dj-static

To use it, we just need to modify mysite/wsgi.py slightly:

import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
from django.core.wsgi import get_wsgi_application

from dj_static import Cling
application = Cling(get_wsgi_application())

Start gunicorn again and all your static files should be accessible.

Let’s commit these changes as well:

$ git add mysite/wsgi.py
$ git commit -m 'use dj-static for serving static files'

And we’re done!

To wrap it up, we’ll also add requirements.txt tracking required Python packages (the versions pinned here are latest as of this writing, yours will probably change, see the output of pip freeze):

Django==1.6.2
dj-static==0.0.5
gunicorn==18.0
static==1.0.2

If you’re planning to use Heroku, make sure you add requirements.txt to the root of your repository, not in the mysite directory.

$ git add requirements.txt
$ git commit -m 'specifying dependencies in requirements.txt'

Bonus: deploying on Heroku

Since our app doesn’t rely on anything besides Python packages specified in requirements.txt, and doesn’t even need a database, it’s trivial to deploy it to Heroku.

First, let’s create a Procfile that will tell Heroku how to run our app:

web: cd staticsite && gunicorn -b 0.0.0.0:$PORT -w 3 staticsite.wsgi:application

The gunicorn option -w 3 here tells it to run 3 worker processes. The actual number depends on the memory usage of Django, and may be much higher than 3, but I’ve never seen three workers being problematic, so it’s a safe bet.

Now we can create a new Heroku app:

heroku apps:create mysite

The only thing remaining is to push to the new app:

git push heroku master:master

The conclusion

If you’ve managed to read this far, congratulations! In case you don’t want to manually go through all the above steps, I’ve prepared a git repository with the same setup (the only additions are README with install instructions and bundled CSS from purecss.io for the static asset example).

Just clone dobarkod/django-staticsite and start building your static site with Django in seconds.

Author
Senko Rašić
We’re small, experienced and passionate team of web developers, doing custom app development and web consulting.