The goal of this post is to summarize the steps needed to deploy a Django project running on nginx and gunicorn on Mac OSX. I am going to document a list of problems encountered and provide solutions below. (If you are running on Linux, I imagine the process will be very similar)

My setup (the installation order of the following packages matters, just keep on reading. But you don't have to have the exact same versions listed below):

Summary of Installation

Let me explain how each aforementioned package fits within the big picture. To make everything easier to understand for novice users like myself, I simplified the functionalities of the pieces below, so please go to their websites for a more accurate description.

1. Virtualenv and VirtualenvwrapperVirtualenv allows you to create a 'sandbox' that contains all of the Python packages you need for deployment without messing up the system's python environment. Virtualenvwrapper as its name suggests, wraps around virtualenv to provide a list of 'shortcut' commands to reduce the amount of efforts needed to create/switch between/delete virtual environments. The sole purpose of these two pieces is to isolate your deployment environment from other Python environments on the same machine.

To illustrate this issue with an example, I develop and deploy Django projects on the same machine for learning purpose. When I work on a Django project, I use the latest version of Django. But when I deploy it, I use an older version. Without virtualenv, I have to install the latest Django for development and delete it and install an older version for deployment. They can never co-exist at the same time. Virtualenv allows me to create two separate environments with different Django version in each and co-exist nicely.

2. Postgres.app and Psycopg2
The Postgres.app installs the PostgreSQL server on your machine with one click. It also provides a shortcut for you to open up a Terminal as a client that connects to the database server. Psycopg2 is used by Django or any Python program to access the PostgreSQL database programmatically.

3. nginx and gunicorn
Why do we need two servers instead of one？Well, by using gunicorn alone, it is enough to get your Django project up and running. The purpose of adding nginx in front of it is for:

Load Balancing - The more servers you have the more users you can serve concurrently. But who should be the one that directs traffic to different servers? Well, the nginx server in this case. (I am aware that I have everything running on a single Mac...)

Acceleration - Not all of the contents on your web page are dynamically generated. For instance, CSS style sheets, the logo of your website, javascript libraries like jQuery. We can have a server dedicated for serving these static contents so that our gunicorn server can focus on serving dynamic contents.

Security - Users never see the gunicorn server. The only server exposed to the outside world is the nginx server. The gunicorn server will never accept traffics other than the ones forwarded by nginx.

The choice of which server to use is arbitrary in this case. You can replace nginx with Apache and gunicorn with (uWSGI)[https://uwsgi-docs.readthedocs.org/en/latest/]. You should Google to find out more about them and see which one fits better with you.

This concludes the first part of this post. Here is the second part but I suggest you taking a break as the second portion is rather long.

In this part of the post, I would like to document the steps needed to run an existing Django project on gunicorn as the backend server and nginx as the reverse proxy server. (Please refer to Part 1 of this post)

Disclaimer: this post is based on this site but I added a lot of details that wasn't explained.

Setup nginx

Before starting the nginx server, we want modify its config file. I installed nginx via brew on my machine, and the conf file is located here:

/usr/local/etc/nginx/nginx.conf

We can modify this file directly to add configurations for our site, but this is not a good idea. Because you may have multiple web apps running behind this nginx server. Each web app may need its own configuration. To do this, let's create another folder for storing these site-wise configuration files first:

cd /usr/local/etc/nginx
mkdir sites-enabled

We can store our site specific config here but wouldn't it be better if we store the config file along with our project files together? Now, let's navigate to our project folder. (I named my project testproject and stored it under /Users/webapp/Apps/testproject)

Let me elaborate on the '/static' part. This part means that any traffic to 'your_server_ip/static' will be forwarded to '/Users/webapp/Apps/testproject/vis/static'. You might ask why doesn't it forward to '/Users/webapp/Apps/testproject/vis'?(without '/static' in the end) Because when using 'root' in the config, it will append the '/static' part after it. So be aware! You can fix this by using alias instead of root and append /static to the end of the path:

location /static {
alias /Users/webapp/Apps/testproject/vis/static;
}

Here is the folder structure of my project :

/Users/webapp/Apps/testproject/
manage.py <- the manage.py file generated by Django
nginx.conf <- the nginx config file for your project
gunicorn.conf.py <- the gunicorn config file that we will create later, just keep on reading
testproject/ <- automatically generated by Django
settings.py
urls.py
wsgi.py <- automatically generated by Django and used by gunicorn later
vis/ <- the webapp that I wrote
admin.py
models.py
test.py
urls.py
template/
vis/
index.html
static/ <- the place where I stored all of the static files for my project
vis/
css/
images/
js/

All of the static files are in the /testproject/vis/static folder, so that's where nginx should be looking. You might ask that the static files live in their own folders rather than right under the /static/ path. How does nginx know where to fetch them? Well, this is not nginx's problem to solve. It is your responsibility to code the right path in your template. This is what I wrote in my template/vis/index.html page:

href="{% static 'vis/css/general.css' %}

It is likely that you won't get the path right the first time. But that's ok. Just open up Chrome's developer tools and look at the error messages in the console to see which part of the path is messed up. Then, either fix your nginx config file or your template.

To let nginx read our newly create config file:

cd /usr/local/etc/nginx/
nano nginx.conf

Find the ' http { ' header, add this line under it:

http{
include /usr/local/etc/nginx/sites-enabled/*;

This line tells nginx to look for config files under the 'sites-enabled' folder. Instead of copy our project's nginx.conf into the 'sites-enabled' folder, we can simply create a soft link instead:

Setup gunicorn

Setting up gunicorn is more straight forward (without considering optimization). First, let's write a config file for gunicorn. Navigate to the directory which contains your manage.py file, for me, this is what I did:

save the process id of the gunicorn process to a specific file ('~/logs/gunicorn.pid' in this case)

run gunicorn in daemon mode so that it won't die even if we log off

To shutdown this daemon process, open '~/logs/gunicorn.pid' to find the pid and use (assuming 12345 is what stored in '~/logs/gunicorn.pid'):

kill -9 12345

to kill the server.

This is it! Enter 127.0.0.1 in your browser and see if your page loads up. It is likely that it won't load up due to errors that you are not aware of. That's ok. Just look at the error logs:

tail -f error.log

Determine if it is an nginx, gunicorn or your Django project issue. It is very likely that you don't have proper access permission or had a typo in the config files. Just going through the logs and you will find out which part is causing the issue. Depending on how you setup your Django's logging config, you can either read debug messages in the same console which you started gunicorn, or read it form a file. Here is what I have in my Django's settings.py file:

Bad Request 400

Just when you thought everything is ready, and you want the world to see what you have built...BAM! Bad Request 400. Why??? Because when you turn DEBUG = False in the settings.py file, you have to specify the ALLOWED_HOSTS attribute:

Conclusion

Setting up nginx, gunicorn and your Django project can be a very tedious process if you are not familiar with any of them like me. I document my approach here to hopefully help anybody who has encountered the same issue.