Hosting Django Apps on Ubuntu with Nginx and uWSGI

I've tried a few different ways of hosting Django apps over the last couple of years. Here's a HOWTO guide for what has become my favourite approach: Ubuntu + Nginx + uWSGI + django + PostgreSQL. Setting it all up is fairly easy, and the flexibility and performance that you gain makes it a compelling alternative to using a PaaS like heroku.

These instructions assume you want to run Python 2.7 apps; for Python 3 or a mixed environment, some changes will be required.

Anchoring the stack is Ubuntu Server on a VPS such as Linode or DigitalOcean. Ubuntu Server is my choice because it's very easy to use and it's very popular — so when things go wrong, it's easy to find fixes or help.

I use Nginx to front all incoming web requests and act as a reverse proxy, passing web traffic on to uWSGI instances or any other type of app running on the server. The Ubuntu packaged version of Nginx is easy to work with and has a nice config file setup that makes it really easy to add and remove web apps.

(I've tried cutting out nginx and using Varnish as both the accelerator/cache and the web proxy. Once configured properly, this created a wickedly fast setup; unfortunately configuration is much more complex compared to Nginx, and many features I love to use with uWSGI apps such as UNIX sockets, uWSGI protocol support, and HTTPS aren't supported.)

uWSGI is an amazing piece of software for hosting WSGI apps, such as Django apps. It's written in C, has blazing-fast performance, works with any version of python (even several different versions at once), and has more features than you could ever want, without feeling bloated. Nginx has built-in support for the uwsgi protocol, so the two work together perfectly. I like to use the uWSGI emperor mode (specifically the emperor tyrant mode), which provides us with a monitored, self-healing, flexible, and secure multi-app setup.

Finally, all Django apps need a database, and there's none better than PostgreSQL. The more I use PostgreSQL, the more upset I get when I'm forced to use MySQL or some other DB on a project, because of PostgreSQL's awesome features like window functions, DDL transactions, and native JSON and UUID types. Also, for development on a Mac, there's Postgres.app, which is nicely self-contained and which even your grandma could install.

I like to configure each web app on a server to have its own nonpriviledged user account and for its code and static files to be placed in /home/appname. I find this keeps things separated nicely and is more secure than running all apps under a "www" user.

Setting up the server

First, provision a new VPS from your favourite provider, and choose Ubuntu Server. The instructions below were used with Ubuntu Server 13.04, but any recent version should be fairly similar.

  1. Set the Hostname

    # echo "servernamehere" > /etc/hostname
    # hostname -F /etc/hostname
    
  2. Configure the FQDN

    Make sure your server has both a hostname and a fully-qualified domain name. Once you've set up DNS to point "servernamehere.orgname.com" to your server's IP, you need to edit /etc/hosts on your new server like so, putting in your server's WAN IP in place of 55.55.55.55:

    127.0.0.1       localhost
    55.55.55.55     servernamehere.orgname.com  servernamehere
    
  3. Pick a timezone

    Choose a timezone for your server. For example, to use Vancouver time:

    # echo "America/Vancouver" > /etc/timezone
    # dpkg-reconfigure --frontend noninteractive tzdata
    
  4. Update packages

    # apt-get update && apt-get upgrade --show-upgraded
    
  5. Security

    At a minimum, install the fail2ban intrusion prevention framework, and use Ubuntu's excellent firewall tool, ufw, to close all the ports you aren't using.

    # apt-get install fail2ban
    # ufw allow 22/tcp
    # ufw allow 80/tcp
    # ufw allow 443/tcp
    # ufw enable
    

    Later on, once you've created at least one non-privileged user on the system and can login using an SSH key, I highly recommend changing your sshd config to deny root login via SSH and password authentication. If you need root access, SSH in to a non-privileged user, then use su or sudo. You can achieve this by editing /etc/ssh/sshd_config as follows:

    PermitRootLogin no
    PasswordAuthentication no
    

    And don't forget to restart sshd afterward (service ssh restart).

Setting up the server-wide web hosting environment

  1. Install nginx, memcached, and other basic software

    May I suggest:

    # apt-get install htop python-dev build-essential git python-virtualenv python-pip libpq-dev gettext memcached libmemcached-dev libjpeg-dev libfreetype6 libfreetype6-dev libpng12-0 libpng12-dev nginx
    

    Notes:

    • htop is great for monitoring processes on a server like this.
    • I included memcached since you'll most likely want to use it for caching. Installing is the only step required - it should now be ready to use.
  2. Install uWSGI

    The Ubuntu packaged versions of uWSGI are buggy, generally out of date, and aren't set up out of the box to take advantage of some of uWSGI's best features. You can get a much nicer install by simply using pip.

    # pip install uwsgi
    

    Now, we will be using the uWSGI Emperor Mode, and will be configuring each uWSGI app by placing a config file in /etc/uwsgi, so let's make sure that folder exists:

    # mkdir /etc/uwsgi
    

    Next, we want upstart to manage the uWSGI emperor process for us, so create /etc/init/uwsgi.conf with the following contents:

    description "uWSGI Emperor"
    start on runlevel [2345]
    stop on runlevel [06]
    exec uwsgi --die-on-term --emperor /etc/uwsgi --emperor-tyrant --logto /var/log/uwsgi.log
    

    Then, start uWSGI: service uwsgi start

  3. Optional: Set up a default website

    If someone accesses your web server directly via its hostname or IP, or if there's a typo in your virtual host configuration, nginx on Ubuntu will by default send traffic to the first virtual host configured. That's probably not what you want, so I recommend creating a simple splash page for your webserver. Here's an example. First, create a folder like /var/www/ to hold this default site, then put your splash page in /var/www/index.html. Then, edit /etc/nginx/sites-available/default to look like this:

    server {
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;
        root /var/www;
        index index.html;
    }
    

    Tell nginx to update its config using this command: nginx -s reload

  4. Optional: Install PostgreSQL

    You may want to use a hosted PostgreSQL service like Heroku's, or for larger projects you will want a dedicated database server that your web servers can share. But if you want to have PostgreSQL running on the same server, it's nice and easy to set up. As a bonus, you can use PostgreSQL's peer authentication which uses Linux user security instead of passwords, sparing you from having to generate and manage a database user password (using that approach, the root password could be the only password you ever set on the whole server, with SSH keys and peer authentication being used for everything else).

    # apt-get install postgresql postgresql-contrib
    # service postgresql start
    

    Don't forget to set up some kind of PostgreSQL backup as well.

Adding a new web app

Now, it's time to add the first web app to the server. As an example, let's say we have a django app that we want to be available at gizmo.io. First, we need to pick a short name for the app, and store it in a shell variable:

# NEW_APP_NAME=gizmo

Now, we will create a new user with the same name:

# useradd -d /home/$NEW_APP_NAME -G www-data -m -U -s /bin/bash $NEW_APP_NAME

Next, let's set up SSH config files for this user and set up a bare git repository at /home/gizmo/gizmo.git, so that we can push to the git repo from our development computer, and have this server automatically deploy the new version of the app into /home/gizmo/app/ :

# su - $NEW_APP_NAME
$ mkdir ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys
$ mkdir ~/$USER.git
$ cd ~/$USER.git
$ git init --bare
$ echo -e '#!/bin/bash\nGIT_WORK_TREE=~/app git checkout -f' > hooks/post-receive
$ chmod +x hooks/post-receive
$ mkdir ~/app

Next, put your personal SSH public key into the gizmo user's ~/.ssh/authorized_keys, and make sure you can ssh into your new server using just ssh gizmo@servernamehere.orgname.com. Now, on your local development computer, add a git remote for the new server to the gizmo project. In this example, we'll call the remote "gizmo_io" but call it whatever you want:

$ git remote add gizmo_io gizmo@servernamehere.orgname.com:gizmo.git

It's time to push the code!

$ git push gizmo_io master

On the server, you should now see your app's files in /home/gizmo/app, thanks to the post-receive hook that we previously installed. Now you need to create a virtual environment for your app. Run the following as the gizmo user:

$ cd ~/app
$ virtualenv venv
$ source venv/bin/activate
$ pip install -r requirements.txt  # Substitute whatever your app uses to manage dependencies

I'm going to skip instructions on how to configure your app's database settings, staticfiles app, django secret key, and so on, but it's important to note that such settings probably should not be included in the git repository itself. You can put them into the gizmo user's bashrc/bash_profile, put them into the virtualenv activation script, put them into a settings_local.py file that's ignored by git, put them into a hidden .env file, or any number of other things. Now, as the gizmo user on the server, you should run any commands required to set up your django app, such as installing fixtures, or the syncdb, migrate, collectstatic, or compress management commands. Once your site is working well, you should also add these commands to the post-receive hook, so that deploying a new version of your site is as simple as pushing to the new server's remote.

uWSGI and Nginx config:

Once your app is configured, it's time to add it to uWSGI and nginx. exit the gizmo user shell and return to our root shell, then run these two commands:

# echo -e "[uwsgi]\\nsocket = /home/%n/uwsgi/socket\\nchmod-socket = 664\\nmaster = true\\nprocesses = 2\\nvirtualenv = /home/%n/app/venv\\npythonpath = /home/%n/app/\\nmodule = %n.wsgi\\npidfile2 = /home/%n/uwsgi/pid\\ndaemonize = /home/%n/uwsgi/log" > /etc/uwsgi/$NEW_APP_NAME.tmp
# chown $NEW_APP_NAME:www-data /etc/uwsgi/$NEW_APP_NAME.tmp

The above commands have created a template uWSGI config file at /etc/uwsgi/gizmo.tmp. We have used chown so that the uWSGI emperor will run the gizmo app as the gizmo user (since we have previously enabled the uWSGI emperor "tyrant mode"). The config file must be named .tmp for now so that the uWSGI emperor won't try to read it until we are ready. Edit the config file now. Make sure that the module setting is correct, and that the number of processes is reasonable.

Start the uWSGI app

To start the uWSGI app server:

mv /etc/uwsgi/$NEW_APP_NAME.tmp /etc/uwsgi/$NEW_APP_NAME.ini
su - $NEW_APP_NAME -c "mkdir ~/uwsgi && ln -s /etc/uwsgi/$NEW_APP_NAME.ini ~/uwsgi/config"

What's cool about this is that the gizmo user can now edit the uWSGI config at ~/uwsgi/config which saves us from needing root privileges if we need to scale the app or adjust settings. This is secure because the the uwsgi emperor's tyrant mode will prevent privilege escalation no matter what the contents of the config file are set to (although it does not stop the gizmo user from hogging all the system resources by cranking the # of processes, etc.). Also, restarting the uWSGI app is as simple as the gizmo user issuing the command touch ~/uwsgi/config. The uWSGI app should now be running, serving your django app on the UNIX socket /home/gizmo/uwsgi/socket. Check the log file at /home/gizmo/uwsgi/log and/or /var/log/uwsgi.log to make sure everything is working.

Start the nginx virtual host reverse proxy

It's time for the last step, which is configuring nginx to forward traffic for gizmo.io to the uWSGI gizmo app. Edit /etc/nginx/sites-available/$NEW_APP_NAME and put in a config such as the following:

server {
    server_name gizmo.io;
    # Remove any period after domain name:
    if ($http_host ~ "\.$" ) { rewrite ^(.*) http://$host$1 permanent; }
    # Set up django static file serving:
    location /static {
        alias /home/gizmo/app/site_media/;
    }
    location /media {
        alias /home/gizmo/app/user_media/;
    }
    # pass all non-static requests to uWSGI:
    location / {
        uwsgi_pass unix:///home/gizmo/uwsgi/socket;
        include uwsgi_params;
    }
}
# Remove the www from our URLs:
server {
    server_name www.gizmo.io;
    return 301 http://gizmo.io$request_uri;
}

Last steps: enable the site and reload the whole nginx config:

ln -s /etc/nginx/sites-available/$NEW_APP_NAME /etc/nginx/sites-enabled/
nginx -s reload

Your cool new web app is now live! Of course, once you've tweaked this process to your way of working, you'll want to automate the above steps so that you can spin up new web apps quickly.