Multi Trac / Django Hosting with mod_wsgi
As you might now we switched the pocoo trac to mercurial, mod_wsgi and splitted it in the same go. The new structure is can be found at dev.pocoo.org. What you cannot see there is how all that is implemented. And I tell you. It’s dead simple.
Basically we use mod_wsgi for hosting the tracs. There are many reasons for that but the most important one is that you can host multiple trac instances without much configuration. Basically the configuration binds a wsgi application to a URL match rule. One important thing is the maximum-requests setting. To understand this parameter you have to know that mod_wsgi does not only keep a pool of running python interpreters, but also your application with all data in the memory. Now that’s a big difference to mod_php where your application is sourced on request and removed from the memory after the reqest. So basically you cannot create memory holes, which you can do in python. If your application leaks memory (and trac tends to do so) you can tell mod_wsgi to restart one python interpreter after 500 requests for example. That setting of course depends on your trac version, the number of plugins etc. And especially how many memory you have in your application. Here the Apache config:
<VirtualHost *:80>
ServerName dev.example.org
RewriteEngine On
WSGIDaemonProcess tracs threads=10 maximum-requests=500
RewriteCond %{REQUEST_URI} ^/([a-z_]+)
RewriteRule . - [E=TRAC_ID:%1]
WSGIScriptAliasMatch ^/([a-z_]+) /var/trac/trac.wsgi
<Directory /var/trac>
WSGIProcessGroup tracs
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all
</Directory>
<LocationMatch /([a-z_]+)/login>
AuthType "Basic"
AuthName "Trac Instances Login"
AuthUserFile /var/trac/users
Require valid-user
</LocationMatch>
</VirtualHost>
Then we need a trac.wsgi file which is basically just a minimal WSGI application that dispatches our key. Say we have our trac instances in /var/trac/instances, every trac in it’s own folder. Then we can use this code:
#!/usr/bin/python
from os import environ, path
from trac.web.main import dispatch_request
def application(environ, start_response):
trac_path = path.join('/var/trac/instances', environ['TRAC_ID'])
if path.exists(trac_path):
environ['trac.env_path'] = trac_path
return dispatch_request(environ, start_response)
start_response('404 NOT FOUND', [('Content-Type', 'text/plain')])
return ['Not Found']
You can of course modify that not found message, maybe render a fancy HTML page or just redirect to the index of that domain or whatever. The important thing is that you set the path to the trac instance before calling the dispatch_request function. In theory you can do that from the apache config too, but in that situation you cannot check if the trac really exists.
And now about the django hosting part. Basically you can do the same for django. Django basically has a environment key called the DJANGO_SETTINGS_MODULE key. This key basically controls what settings module django will import. Unfortunately the whole django core is not process safe, so you cannot run two different django powered applications in the same python interpreter. This however is not that much of an issue with mod_wsgi, because you can tell mod_wsgi to not share the interpreter. (In the trac config above we shared the interpreter to save some memory)
Your config could look like this:
<VirtualHost *:80>
ServerName www.example.org
WSGIDaemonProcess django_app1 threads=10 maximum-requests=5000
WSGIScriptAlias /app1 /var/www/django_app1.wsgi
WSGIDaemonProcess django_app2 threads=10 maximum-requests=5000
WSGIScriptAlias /app2 /var/www/django_app2.wsgi
<Location /app1>
WSGIProcessGroup django_app1
WSGIApplicationGroup %{GLOBAL}
</Location>
<Location /app2>
WSGIProcessGroup django_app2
WSGIApplicationGroup %{GLOBAL}
</Location>
</VirtualHost>
The actual “django_appX.wsgi” file is very, very simple. It just adds the folder and instanciates the django wsgi app:
#!/usr/bin/python
import sys, os
sys.path.insert(0, '/path/to/django_appX')
os.environ['DJANGO_SETTINGS_MODULE'] = 'django_appX.settings'
from django.core.handlers.wsgi import WSGIHandler
application = WSGIHandler()
Hope I could help a little bit, if you have some questions to our server setup just send me a mail or write a comment. Finally, we first encountered some problems with mod_wsgi two months ago, but at the moment everything is working well, a lot better than any other server setup we used. You can even put python applications into the context of another user which basically replaces fastcgi + suexec.
And btw, the support we got from Graham is really, really good :)
Update: removed WSGIPassAuthorization like Graham suggested in the comments below.