Django, mod_wsgi, Apache and OS X - do it.

by jesse in , ,


whut_2.jpgSo, I'm one of those people where I don't like running things "too far" from what a production setup might look like (I code on OS/X, deploy to Linux). This is why I jump(ed) through various hoops on my OS X system to get Apache/Django/mod_wsgi/etc all up and running and happy (not for serving the site; just developing). Since I like simple/succinct guides, I thought I'd post what I did so others can follow in my stead.

Note: These instructions work with the python 2.5 version which ships with Leopard, or a self-compiled version of 2.6 (which is what I prefer) - see the InstallationOnMacOSX mod_wsgi page. Additionally, see the "Missing Code For Architecture" section for possible work-arounds if you find yourself needing 32 bit execution of Apache; I think the "Forcing 32 Bit Execution" are preferred over the "thinning" of the Apache binary.

First, download and install mod_wsgi on leopard, this is as easy as (on Leopard):

curl -o mod_wsgi.tgz http://modwsgi.googlecode.com/files/mod_wsgi-2.5.tar.gz
tar -xzf mod_wsgi.tgz
cd mod_wsgi-2.5
./configure
make
sudo make install

Now, edit (via sudo) /etc/apache2/httpd.conf and add the line:

LoadModule wsgi_module libexec/apache2/mod_wsgi.so

After the rest of the LoadModule lines. Cool.

Invariably all of my directions play with virtualenv/virtualenvwrapper and pip:

mkvirtualenv django
cdvirtualenv
easy_install pip
pip install http://media.djangoproject.com/releases/1.1/Django-1.1-rc-1.tar.gz
django-admin.py startproject mysite
django-admin.py startapp myapp
cd mysite
mkdir apache
mkdir media

Now, that just sets up the skeleton - the meat of the wsgi configuration goes in apache/ in the mysite/apache directory. The first file is named mysite.wsgi:

import os, sys

#Calculate the path based on the location of the WSGI script.
apache_configuration = os.path.dirname(__file__)
project = os.path.dirname(apache_configuration)
workspace = os.path.dirname(project)
sys.path.append(workspace)

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

This does the needed wsgi project magic for the Django application - don't worry about the interpreter path; we'll do that next.

Next up is a file named apache_django_wsgi.conf, this looks like this:

# mod_wsgi configuration directives - I like having stdout access, the other two 
# options run mod_wsgi in daemon mode - more on this in a minute.
WSGIPythonHome /<path to virtualenv>
WSGIRestrictStdout Off
WSGIDaemonProcess django
WSGIProcessGroup django

#
# This should be the path of the /mysite/media directory
# for example "/Users/jesse/mysite/media/"
#
Alias /site_media/ "<PATH TO>/mysite/media/"
<Directory "<PATH TO>/mysite/media">
Order allow,deny
Options Indexes
Allow from all
IndexOptions FancyIndexing
</Directory>

#
# Directory path to the admin media, for example:
#

Alias /media/ "<PATH TO>/virtualenv/site-packages/django/contrib/admin/media/"
<Directory "<PATH TO>/virtualenv/site-packages/django/contrib/admin/media">
Order allow,deny
Options Indexes
Allow from all
IndexOptions FancyIndexing
</Directory>

#
# Path to the mysite.wsgi file, for example:
# "/Users/jesse/mysite/apache/mysite.wsgi"
#

WSGIScriptAlias / "<PATH TO>/mysite/apache/mysite.wsgi"

<Directory "<PATH TO>/mysite/apache">
Allow from all
</Directory>

The apache_django_wsgi.conf file is the meat-and-potatoes here. This sets up all the paths/permissions, and is in Apache httpd.conf format. You can pretty much logjam any apache configuration directive here that you like.

Your final step is to once again edit (via sudo) /etc/apache2/httpd.conf and add a line like this at the verrrrry bottom:

Include "/path to/mysite/apache/apache_django_wsgi.conf"

And then run "sudo apachectl restart"

You should now be able to hit http://127.0.0.1/ and see the friendly and inviting django welcome page. Note, that if you are using sqlite as your database, you should chmod a+rw the file, so that processes which are not you can mess with it.

There's a final piece to this though. Normally, if you run mod_wsgi in embedded mode, you're going to need to restart apache every single time you make a change to your django app.

Ah! But we're running in daemon mode. This means all you need to do when you change a file is:

touch mysite/apache/mysite.wsgi

This will trigger a reload and magic happens. Me being as lazy as I am (ask my wife) ended up snagging Bruno Bord's tdaemon script, and hacking it up a bit. The tdaemon script will watch a directory and run tests. Well, I wanted it to watch a directory (and let me filter sub directories) and then run that touch command. So I reused my watcher.py (here) - I used this to monitor my sphinx tree and run builds as well (and other stuff). Here's how I'd use this:

workon django
cdvirtualenv
cd mysite
python ~/.slash/bin/watcher.py --command "touch apache/mysite.wsgi" -f media

This will auto-fire the touch command whenever it detects a file change (including svn updates).

You can also do this another way In my rush to reuse a tool I use a bit (watcher) I skipped past the mod_wsgi document section on code reloading that shows how to setup a monitor which will watch .py file changes and kill the wsgi daemon, here. If you scroll down a bit, you'll see the "Monitoring For Code Changes" section. All you need to do here is copy the code from the wiki into a module on your PYTHONPATH - in my case, I wrote it to mysite/apache/wsgi_monitor.py (just for this example! you should put it someplace else!) and then changed the mysite.wsgi file to import it, and set it up:

import os, sys

#Calculate the path based on the location of the WSGI script.
apache_configuration = os.path.dirname(__file__)
project = os.path.dirname(apache_configuration)
workspace = os.path.dirname(project)
sys.path.append(workspace)

sys.path.append(apache_configuration) # you probably shouldn't do this.
import wsgi_monitor
wsgi_monitor.start(interval=1.0)

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

This method - once you reload apache - will watch the project for changes and then kill the wsgi daemon (forces a reload). So there you go - two ways of doing it.

The nice thing about this setup is that I can make production version of the wsgi scripts and check them in, but keep local "my copies" (ala local_settings.py) additionally, I don't have to jump through hoops to get static media and content served up via the django development server.

Additional reading: