Howsthe.com has made an architectural change in our deployment stack. We were using mod_wsgi and Apache to serve the static content, which was not working as well as planned. We moved to using nginx as a front end proxy and a static content server. It also acts as a gateway for the SSL connection so we don't have to run different mod_wsgi instances for HTTP connections and for HTTPS connections. Below we explain how we configured this setup.
Prerequisites:
We'll be using Ubuntu 9.04 server, the following packages need to be installed:
- apache2
- libapache2-mod-wsgi
- nginx
- subversion ( Only if you are going to install Django from SVN )
This doesn't include database installation, for this example we will not be using a database. If your application uses a database, install the correct python drivers and make sure your settings.py file is configured correctly.
Once these packages are installed, then install Django. We will not be going over this here due to expediency. You can find the directions at http://docs.djangoproject.com/en/dev/intro/install/.
Creating A Simple Django Project:
We need to create a simple Django project to demonstrate that the nginx and mod_wsgi are working together, and to test the SSL gateway through ngnix.
django-admin.py startproject simple_ssl_project
cd simple_ssl_prject
python manage.py startapp ssl_test
Now that we have our project and a Django application in place, we can create the view and url to test for the SSL support.
# ssl_test/views.py:
from django.http import HttpResponse
def using_ssl(request):
""" Simple View that will return true if it is using HTTPS """
if request.is_secure():
msg = "Yes we are secure"
else:
msg = "We are not running in secure mode"
return HttpResponse(msg)
And now the url.py.
# url.py (Root Url Conf):
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^', 'ssl_test.views.using_ssl')
)
We have a very simple application setup, we first have to get it to work with mod_wsgi. We will need to create a wsgi handler, I usually place this file in the root of the project and name the file wsgi_handler.py.
# wsgi_handler.py:
import sys
import os
sys.path.append('/home/vbabiy/')
sys.path.append('/home/vbabiy/simple_ssl_project/')
os.environ['DJANGO_SETTINGS_MODULE'] = 'simple_ssl_project.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()
We need to configure apache and create a virtual host.
# /etc/apache2/sites-available/example.com
<VirtualHost *>
ServerName www.example.com
ServerAlias *example.com
# In production you will probly want to setup your logging config
WSGIDaemonProcess simple_test user=vbabiy group=vbabiy display-name=%{GROUP} maximum-requests=10000
WSGIProcessGroup simple_test
WSGIScriptAlias / /home/vbabiy/simple_ssl_project/wsgi_handler.py
</VirtualHost>
For the user and group on the WSGIDaemonProcess, you will need to change them to be valid for you setup. This should be the user and group you would like the process to run as. You now need to enable the site:
sudo a2ensite example.com # Vhost file name
You will need to change which port Apache is going to be listening on, by default this is set to port 80 which needs to be set to a different port. For this example we will be using 8080. This can be done in the /etc/apahce2/ports.conf file. We need to modify this because when we are setting up ngnix as the front end proxy it will be listening on port 80.
# /etc/apache2/ports.conf
Listen 8080
<IfModule mod_ssl.c>
# SSL name based virtual hosts are not yet supported, therefore no
# NameVirtualHost statement here
Listen 443
</IfModule>
And now you can restart apache:
sudo /etc/init.d/apache2 restart
Now that apache is running on port 8080, we need to setup nginx to handle requests on port 80. First create a proxy configuration file in the /etc/ngnix directory, name it proxy.conf content of the file is below:
# /etc/ngnix/proxy.conf
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
Create a server configuration for ngnix, the file needs to be in /etc/nginx/site-available directory with the similar content to:
# /etc/nginx/site-available/example.com
upstream django {
server 127.0.0.1:8080;
}
# This can be used to redirect example.com to www.example.com
#server {
#server_name example.com;
#rewrite ^(.*) http://www.example.com$1 permanent;
#}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://django;
# This will proxy this request to upstream of django which is defined above.
include /etc/nginx/proxy.conf;
}
# Link your media to server static content
location /media {
root /home/vbabiy/simple_ssl_project/media/;
# This would be set to whatever is in your setting.MEDIA_ROOT.
}
}
Static Media:
In the ngnix configuration we set the location of the static content in the following code snippet (this snippet is from the above code).
location /media {
root /home/vbabiy/simple_ssl_project/media/;
}
This code is self explanatory, if you have more than one static media source for example, your admin media, you can add more than one of these definitions per server configuration.
Now that we have added the site to the site-available directory, we need to create a simlink to the sites-enabled directory.
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
In order for these configurations to take place, you need to restart ngnix. If you get no errors you should be able to go to http://localhost and see our Django application running you should see We are not running in secure mode.
Debugging (I see errors): If you are seeing errors on the command line or thourgh the web browser, you should check the error log for more details. By default it is located at /var/log/ngnix/error.log.
Our project is now running using nignx as a front end proxy and mod_wsgi in the back end. If this is all your application requires you're done. Otherwise if you need to setup SSL support in nignx follow on below.
Adding SSL Support: In order to get SSL support we will need to make a few changes to our wsgi handler and add the SSL support to nginx. We will start off by adding it nignx, but before we do that you will need to have a SSL certification or you can create your own signed certificate. It is not recommended to use in production, but if you are using this in a staging or testing enviorment it is fine. Ubuntu's wiki has a great step by step of how to create one: http://help.ubuntu.com/8.04/serverguide/C/certificates-and-security.html.
We will need to append this server configuration to the nginx configuration:
server {
listen 443;
server_name example.com;
ssl on;
ssl_certificate /etc/nginx/ssl/certs/example.com.crt; # Specify the location of the cert
ssl_certificate_key /etc/nginx/ssl/private/example.com.key; # Specify the location of the private key
ssl_prefer_server_ciphers on;
include /etc/nginx/proxy.conf;
proxy_set_header X-Forwarded-Protocol https; # Forward in the request to django
location / {
proxy_pass http://django;
}
location /media {
root /home/vbabiy/simple_ssl_project/media;
# This would be set to whatever is in your setting.MEDIA_ROOT.
}
}
After adding the certificate location to the configuration you need to restart nginx, to reload the new configuration
If you try to access django application with https://localhost you will get a warning from the browser that you are using an invalid security certificate, because it is a self signed certificate. You may just ignore this error, and proceed. Once you see the output of the Django application you will notice that the output is still We are not running in secure mode, this is caused by using nginx as a SSL gateway for the application, which is easy to fix. We will need to modify the wsgi_handler to add some logic on how to pass the HTTPS header.
Modified wsgi_handler:
import sys
import os
sys.path.append('/home/vbabiy/')
sys.path.append('/home/vbabiy/simple_ssl_project/')
os.environ['DJANGO_SETTINGS_MODULE'] = 'simple_ssl_project.settings'
import django.core.handlers.wsgi
_application = django.core.handlers.wsgi.WSGIHandler()
def application(environ, start_response):
# trick django into thinking proxied traffic is coming in via HTTPS
# HTTP_X_FORWARDED_SSL is used on WebFaction
if environ.get("HTTP_X_FORWARDED_PROTOCOL") == "https" or \
environ.get("HTTP_X_FORWARDED_SSL") == "on":
environ["wsgi.url_scheme"] = "https"
return _application(environ, start_response)
You should see the output of the ssl_test application in your browser as Yes we are secure. Now nginx is serving your static content and acting as a SSL gateway for all apache mod_wsgi instances in the background.
If you have any suggestions, question, or comments with this deployment stack please leave it in the comments.
