Local Django development with Nginx

11 October 2010   12 comments   Django

Powered by Fusion×

When doing local Django development with runserver you end up doing some changes, then refreshing in Firefox/Chrome/Safari again and again. Doing this means that all your static resources are probably served via Django. Presumably via django.views.static.serve, right? What's wrong with that? Not much, but we can do better.

So, you serve it via Nginx and let Nginx take care of all static resources. You'll still use Django's own runserver so no need for mod_wsgi, gunicorn or uWSGI. This requires that you have Nginx installed and running on your local development environment. First you need to decide on a fake domain name. For example mylittlepony. Edit your /etc/hosts file by adding this line:

127.0.1.1       mylittlepony

Then create the file /etc/nginx/sites-available/mylittlepony and type something like this in it:

server {
   root /home/peterbe/projects/mylittlepony/static;
   server_name mylittlepony;
   gzip            off;

   location = /favicon.ico  {
       rewrite "/favicon.ico" /img/favicon.ico;
   }
   proxy_set_header Host $host;
   location / {
     if (-f $request_filename) {
         add_header X-Static hit;
         access_log   off;
     }

     if (!-f $request_filename) {
         proxy_pass http://127.0.0.1:8000;
         add_header X-Static miss;
     }
   }
}

Then when you've done that enable it:

# cd /etc/nginx/sites-enabled
# ln -s ../sites-available/mylittlepony
# /etc/init.d/nginx reload

Now test the site with curl or something:

$ curl -I http://mylittlepony/ 
HTTP/1.1 200 OK
Server: nginx/0.7.65
Date: Fri, 08 Oct 2010 14:35:04 GMT
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Expires: Fri, 08 Oct 2010 14:35:04 GMT
Vary: Cookie
Last-Modified: Fri, 08 Oct 2010 14:35:04 GMT
ETag: "fecf14808e52fe8652373f6e49e1ac06"
Cache-Control: max-age=0
Set-Cookie: csrftoken=f6eb9e767ca058ecde24bb51c8db9448; Max-Age=31449600; Path=/
Set-Cookie: sessionid=c48af92360ad29410d199081e6067f54; expires=Fri,  
  22-Oct-2010 14:35:04 GMT; Max-Age=1209600; Path=/
X-Static: miss

Now test to get a static resource:

$ curl -I http://mylittlepony/css/jquery-ui-1.8.4.css
HTTP/1.1 200 OK
Server: nginx/0.7.65
Date: Fri, 08 Oct 2010 14:36:31 GMT
Content-Type: text/css
Content-Length: 24806
Last-Modified: Tue, 31 Aug 2010 18:11:38 GMT
Connection: keep-alive
X-Static: hit
Accept-Ranges: bytes

Awesome! If you're curious how much faster Nginx is than Django's serve view, here's a hasty benchmark from my laptop:

# ab -n 10000 -c 10 http://localhost:8000/css/jquery-ui-1.8.4.css
...
Requests per second:    446.34 [#/sec] (mean)
Time per request:       22.405 [ms] (mean)
Time per request:       2.240 [ms] (mean, across all concurrent requests)
Transfer rate:          10893.44 [Kbytes/sec] received
...

And the same directly from Nginx:

# ab -n 10000 -c 10 http://mylittlepony/css/jquery-ui-1.8.4.css
...
Requests per second:    15709.54 [#/sec] (mean)
Time per request:       0.637 [ms] (mean)
Time per request:       0.064 [ms] (mean, across all concurrent requests)
Transfer rate:          384039.88 [Kbytes/sec] received

Obviously you're not going to be able to hit your website that hard but trust me, when you work like this a lot and do a lot of refreshing over and over (working on some Javascript code for example) you can really feel the difference. The static resources load faster because and Django just have to do one single thing which is to create the HTML page. Your stdout on the runserver is only going to log actual views used. Like this:

[08/Oct/2010 15:43:14] "GET / HTTP/1.0" 200 8639
[08/Oct/2010 15:43:27] "GET /crm/clients/ HTTP/1.0" 200 28830
[08/Oct/2010 15:43:30] "GET /crm/clients/A1215/ HTTP/1.0" 200 12804

I hope this helps other people shave milliseconds off their development time.

Comments

Ray
I'd been using Apache in a similar way for quite a while now but switched to Nginx a few month ago, much snappier. Perfect for the job. Just a suggestion for the conf: The encouraged convention instead of ``if`` blocks is using try_files and named locations, something like:

location / {
try_files $uri $uri/ @django;
}

location @django {
proxy_pass http://127.0.0.1:8000;
# etc.
}
Peter
Excellent tip! Thanks. I guess I haven't updated my nginx-fu.
Olivier Favre-Simon
Nice. Yes nginx is great as django front-end.

Your example would be completely functional with a reminder to add line

include /etc/nginx/sites-enabled/mylittlepony;

to /etc/nginx/nginx.conf because this is not in the default config in some setups (e.g. mine is www-servers/nginx-0.8.52 on a Gentoo Linux box).
Peter
Default on Ubuntu and Debian :) It also creates the directories.
Olivier Favre-Simon
Also it would be quite fair to show results of ab2 with setup like http://docs.djangoproject.com/en/dev/howto/static-files/

that is with a line like
#(r'^mylittlepony/media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': '/path/to/mylittlepony/media'}),

at the top of urlpatterns in urls.py.
Peter
Nginx is much faster than 'django.views.static.serve'.
Dennis Fisher
This worked well for me except for one thing; I kept getting 502 errors on just http://hostname/, but http://hostname/blah.txt would work fine. I looked around a bit online and eventually tried just one tweak, which was removing the proxy_pass line in the server conf. Files still seem to get served just fine, and the 502 error is gone. What is the purpose of the proxy_pass line?
Snaky
Dennis, proxy_pass is needed to actually, well, proxy the non-static request to django, so you will need it! Maybe the 502 comes because there is no index.html - but with the try_files directive this should not happen.
mikeshantz
I had the same issue, until I realized I hadn't started my devserver. Doh!
Victor Hooi
heya,

How would this handle Django admin resources?

Not sure what's the best way to setup settings.py and Nginx to handle this?

Cheers,
Victor
Kevin
Thanks :)
Anshuman Aggarwal
Minor improvement suggestion:

proxy_set_header Host $http_host; #instead of $host, allows for proxy port retention etc.
Thank you for posting a comment

Your email will never ever be published


Related posts

Previous:
In jQuery, using the :visible selector can be dangerous 14 September 2010
Next:
My tricks for using AsyncHTTPClient in Tornado 13 October 2010
Related:
Benchmarking Autocompeter 12 April 2015
uwsgi and uid 03 November 2014
django-fancy-cache with or without stats 11 March 2013
How I stopped worrying about IO blocking Tornado 18 September 2012
Is Nginx obsolete now that we have Amazon CloudFront? 28 July 2012
Secs sell! How frickin' fast this site is! (server side) 05 April 2012
How much faster is Nginx+gunicorn than Apache+mod_wsgi? 22 March 2012
Goodies from tornado-utils - part 2: tornado_static 22 September 2011
Optimization of getting random rows out of a PostgreSQL in Django 23 February 2011
How I profile my Nginx + proxy pass server 16 February 2011