Django development with mod_wsgi
There are many reasons as to why developing django apps in the same environments you intend in deploying with, is a good idea. Myself I like the idea of portability. This layout however was done on mac OS 10.6 but any *nix based OS would do. There might nonetheless still be some minor issues to sort out when in other distros. But as I said these will likely be minor.

We will be using python's virtualenv to isolate our development milieu and python setup-tools to install the needed software. So lets jump straight into it and install these if you don't have them already.

curl -O http://pypi.python.org/packages/source/s/setuptools/setuptools-0.6c11.tar.gz
tar zxvf setuptools-0.6c11.tar.gz
cd setuptools-0.6c11
python setup.py build
sudo python setup.py install

With setup tools in place we can use easy_install to install virtualenv

sudo easy_install virtualenv

There done, now with our python environment geared up, let's also get apache ready. We'll be using mod wsgi which is the favored module on which to run django apps, but don't take my word for it, Graham Dumpleton laid out a convincing informal comeback as to why

curl -O http://modwsgi.googlecode.com/files/mod_wsgi-3.1.tar.gz  
tar zxvf mod_wsgi-3.1.tar.gz
cd mod_wsgi-3.1
./configure && make && sudo make install
chmod 755 /usr/libexec/apache2/mod_wsgi.so

With that in place, we have installed almost all the software we need. We can proceed and initiate our environment. I like to put all my projects in the Sites folder (cause that's what we're making) but you are welcome to put them where ever you want. In this example we'll be calling our project sitename.tld.

cd Sites/
virtualenv --no-site-packages sitename.tld
cd sitename.tld

You should now have the following folders in your projects directory

  • bin
  • build
  • Include
  • lib

This will be one directory above your actual django project, thats on account of this being where all the additional software needed for your project will go and all dependencies will be in the correct PATH.
We use the --no-site-packages option to ensure that we have a clean python environment and we don't use the existing modules in the systems site-packages directory.
Let's go ahead and activate our new python env and while we are at it, install django.

source bin/activate
pip install django
pip install mysql-python

I use mysql both in development and in production so I went ahead and threw that in there. Preliminary to starting our django project let's add the apache configuration. Create a directory called apache. In it we'll have two files, I like to name them the same name as the site, so we'll call these sitename.tld.conf and sitename.tld.wsgi.
These are the contents of sitename.tld.conf

WSGIPythonHome /Users/feisal/Sites/sitename.tld
WSGIRestrictStdout Off
WSGIDaemonProcess sitename.tld
WSGIProcessGroup sitename.tld

<VirtualHost *:80>
ServerName sitename.local
Alias /site_media/ "/Users/feisal/Sites/sitename.tld/site-media/"
<Directory "/Users/feisal/Sites/sitename.tld/site-media/">
Order allow,deny
Options Indexes
Allow from all
IndexOptions FancyIndexing
</Directory>

Alias /media/ "/Users/feisal/Sites/sitename.tld/lib/python2.6/site-packages/django/contrib/admin/media/"
<Directory "/Users/feisal/Sites/sitename.tld/lib/python2.6/site-packages/django/contrib/admin/media/">
Order allow,deny
Options Indexes
Allow from all
IndexOptions FancyIndexing
</Directory>

WSGIScriptAlias / "/Users/feisal/Sites/sitename.tld/apache/sitename.tld.wsgi"

<Directory "/Users/feisal/Sites/sitename.tld/apache">
Allow from all
</Directory>
</VirtualHost>

This is your typical apache conf file. Feel free to add any additional configuration you need. We've wrapped these under a virtualhost, given it a server name sitename.local, so that we can distinguish it from other site's you might be running on localhost. Now open up /etc/hosts with your favorite editor and add this line.

127.0.0.1   sitename.local

Next we create our wsgi file sitename.tld.wsgi

import os, sys

sys.path = ['/Users/feisal/Sites/sitename.tld'] + sys.path

os.environ['DJANGO_SETTINGS_MODULE'] = 'sitename.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()

Thereupon we simply have to activate the wsgi module and include our configurations. Open up your httpd.conf file and add the following line at the bottom of all the lines that begin with LoadModule.

mate /private/etc/apache2/httpd.conf
LoadModule wsgi_module libexec/apache2/mod_wsgi.so

In addition, at the bottom of the same file include our new sitename.tld.conf

Include /Users/feisal/sitename.tld/apache/sitename.tld.conf

Finally we can start our django project and restart apache

django-admin.py startproject sitename
sudo apachectl restart

Go ahead and fire up your browser and point it to http://sitename.local, you should be presented with django's magnificent welcome page. Go ahead and create your database and run syncdb, just to be sure everything is running. Don't worry, I'll be right here waiting for you with some more awesomeness.

Ok, everything is fine except we have to restart apache every time while developing to see our changes, as opposed to django's built in server which detects all the changes in your source code automatically. Guess what? wsgi ships with some of those exact same features, coincidently just as awsome, you can go ahead and copy the example code and call it autoreload.py and place it inside your django project sitename/autoreload.py

import os
import sys
import time
import signal
import threading
import atexit
import Queue

_interval = 1.0
_times = {}
_files = []

_running = False
_queue = Queue.Queue()
_lock = threading.Lock()

def _restart(path):
    _queue.put(True)
    prefix = 'monitor (pid=%d):' % os.getpid()
    print >> sys.stderr, '%s Change detected to \'%s\'.' % (prefix, path)
    print >> sys.stderr, '%s Triggering process restart.' % prefix
    os.kill(os.getpid(), signal.SIGINT)

def _modified(path):
    try:
        # If path doesn't denote a file and were previously
        # tracking it, then it has been removed or the file type
        # has changed so force a restart. If not previously
        # tracking the file then we can ignore it as probably
        # pseudo reference such as when file extracted from a
        # collection of modules contained in a zip file.

        if not os.path.isfile(path):
            return path in _times

        # Check for when file last modified.

        mtime = os.stat(path).st_mtime
        if path not in _times:
            _times[path] = mtime

        # Force restart when modification time has changed, even
        # if time now older, as that could indicate older file
        # has been restored.

        if mtime != _times[path]:
            return True
    except:
        # If any exception occured, likely that file has been
        # been removed just before stat(), so force a restart.

        return True

    return False

def _monitor():
    while 1:
        # Check modification times on all files in sys.modules.

        for module in sys.modules.values():
            if not hasattr(module, '__file__'):
                continue
            path = getattr(module, '__file__')
            if not path:
                continue
            if os.path.splitext(path)[1] in ['.pyc', '.pyo', '.pyd']:
                path = path[:-1]
            if _modified(path):
                return _restart(path)

        # Check modification times on files which have
        # specifically been registered for monitoring.

        for path in _files:
            if _modified(path):
                return _restart(path)

        # Go to sleep for specified interval.

        try:
            return _queue.get(timeout=_interval)
        except:
            pass

_thread = threading.Thread(target=_monitor)
_thread.setDaemon(True)

def _exiting():
    try:
        _queue.put(True)
    except:
        pass
    _thread.join()

atexit.register(_exiting)

def track(path):
    if not path in _files:
        _files.append(path)

def start(interval=1.0):
    global _interval
    if interval < _interval:
        _interval = interval

    global _running
    _lock.acquire()
    if not _running:
        prefix = 'monitor (pid=%d):' % os.getpid()
        print >> sys.stderr, '%s Starting change monitor.' % prefix
        _running = True
        _thread.start()
    _lock.release()

Next add this in your wsgi file

import sitename.autoreload
sitename.autoreload.start(interval=1.0)

Go ahead, add and make some changes to your source code or enable the admin. You should now see the changes applied are immediately accessible to the server. You can now concentrate on your code, knowing that your django project is as portable as can be (well almost).

Next we'll look into using fabric to automate this entire process and have the site setup and deployed in a single or two commands.
But thats an entirely different blog post.

blog comments powered by Disqus

Latest tweet

The most expensive burgers I ever had, this better be worth it. — at Burger & Bun http://gowal.la/c/2r2qB?137

Copyright {Feisal Adur} 2009. Made with Django.