Python + Gunicorn + Nginx - A Simpler and More Performant Method to Deploy your Django Projects
Published on Sun, 04 Mar 2018
By
Harlin Seritt
Last week, I posted an article that had to do with hosting your Django projects with gunicorn and Apache. Today, I want to mention an alternative to Apache called Nginx. Most of you know that Apache is probably the most used web server on the planet and has been around for some time going back to 1995. It has always had more market share than Microsoft's IIS by a long shot. Like Apache, Nginx is a web server that can also be used as a reverse proxy, load balancer and HTTP cache. It was created by Igor Sysoev and first publicly released in 2004. At the moment, Apache has about a 48% marketshare of web servers on the Internet while Nginx has about 35% which is very impressive.
So, how do Nginx and Apache compare? I won't go into too many details on the subject of the comparison but from my own research, I'll give you some quick bullet points on what I think each's strength is:
Nginx advantages:
- Better at serving static content
- Faster throughput
- Simplest to configure
- Lightest on resource usage
Apache advantages:
- More granular configurability
- Better dynamic processing
- Better dynamic handling of modules
- Better overall support
I have heard recently from a commenter that the ultimate way to use Apache and Nginx is to perhaps use them together: Apache handling the dynamic and Nginx handling the static but even then that requires more research and a different article all together. I would encourage you to do your own research and decide what works best for your needs.
Without further to do, let's get on with installing and configuring Nginx to work with Django and gunicorn. So that you can follow along, I recommend building your own Hello World Django app. You can read about how to do that here. So, go take care of that and then come back here when done.
Welcome back. If you've done everything as you should have from the Hello World article, in the browser at http://localhost:8000, you should see "Hello World". Next, we'll add some code to make use of css (this will demonstrate how to use Apache later to serve static content).
Let's create the static folder in our project:
# mkdir -p static/css
Inside static/css open a file called base.css and add the following:
p.hello_world {
font-weight: bold;
}
Go ahead and save it.
Open web/templates/index.html and change the html in there to this:
<!DOCTYPE html>
<html>
<head>
<title>{{ message }}</title>
<link rel="stylesheet" href="/static/css/base.css">
</head>
<body>
<p class="hello_world">{{ message }}</p>
</body>
</html>
Next, we need to tell Django to use the static folder to serve up static content. Keep in mind, that this is fine for a development environment but in a production environment, you will want to create a folder outside your Django project to store static assets.
Open helloworld/settings.py and add this setting:
...
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
Now, let's run the Django development server and take a look at our work.
# ./manage.py runserver
At http://localhost:8000 we should still see "Hello World!" but it should now be using bold face type. If this is what you're seeing, then let's go on to the next part.
Do a control-c to exit the Django development server.
Install and Run Gunicorn
To install Gunicorn, we only need to use pip like so:
# pip install gunicorn
Collecting gunicorn
Using cached gunicorn-19.7.1-py2.py3-none-any.whl
Installing collected packages: gunicorn
Successfully installed gunicorn-19.7.1
To start Gunicorn, run:
# gunicorn helloworld.wsgi
[2018-02-25 09:41:01 -0700] [16365] [INFO] Starting gunicorn 19.7.1
[2018-02-25 09:41:01 -0700] [16365] [INFO] Listening at: http://127.0.0.1:8000 (16365)
[2018-02-25 09:41:01 -0700] [16365] [INFO] Using worker: sync
[2018-02-25 09:41:01 -0700] [16409] [INFO] Booting worker with pid: 16409
If you have output similar to this, open up http://localhost:8000. You should see "Hello World" in your browser but now it will not be using the static css reference. This is because gunicorn doesn't handle static assets. We'll need Apache for that. But, besides not handling the static assets, Gunicorn does handle Python and Django code very well and fairly quickly.
Install, Configure and run Nginx
# sudo apt install nginx
# sudo service nginx start
Test http://localhost to make sure it's working. You can also run:
# lsof -i :80
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 29232 root 8u IPv4 11967580 0t0 TCP *:http (LISTEN)
nginx 29232 root 9u IPv6 11967581 0t0 TCP *:http (LISTEN)
nginx 29234 www-data 8u IPv4 11967580 0t0 TCP *:http (LISTEN)
nginx 29234 www-data 9u IPv6 11967581 0t0 TCP *:http (LISTEN)
nginx 29236 www-data 8u IPv4 11967580 0t0 TCP *:http (LISTEN)
nginx 29236 www-data 9u IPv6 11967581 0t0 TCP *:http (LISTEN)
nginx 29239 www-data 8u IPv4 11967580 0t0 TCP *:http (LISTEN)
nginx 29239 www-data 9u IPv6 11967581 0t0 TCP *:http (LISTEN)
nginx 29241 www-data 8u IPv4 11967580 0t0 TCP *:http (LISTEN)
nginx 29241 www-data 9u IPv6 11967581 0t0 TCP *:http (LISTEN)
nginx 29242 www-data 8u IPv4 11967580 0t0 TCP *:http (LISTEN)
nginx 29242 www-data 9u IPv6 11967581 0t0 TCP *:http (LISTEN)
nginx 29243 www-data 8u IPv4 11967580 0t0 TCP *:http (LISTEN)
nginx 29243 www-data 9u IPv6 11967581 0t0 TCP *:http (LISTEN)
nginx 29244 www-data 8u IPv4 11967580 0t0 TCP *:http (LISTEN)
nginx 29244 www-data 9u IPv6 11967581 0t0 TCP *:http (LISTEN)
nginx 29245 www-data 8u IPv4 11967580 0t0 TCP *:http (LISTEN)
nginx 29245 www-data 9u IPv6 11967581 0t0 TCP *:http (LISTEN)
Apache is a little more flexible with regards to serving from areas outside of the document root. Nginx is less so but that's not a problem. For the purposes of demonstration, we'll create a subfolder under /var/www/html where Nginx serves out of the box:
# mkdir /var/www/html/nginx-test
One directory level above your project:
# cp -rfv helloworld /var/www/html/nginx-test/.
Now, let's start gunicorn from the document root:
# cd /var/www/html/nginx-test
# gunicorn helloworld.wsgi
[2018-02-26 12:39:57 -0700] [28155] [INFO] Starting gunicorn 19.7.1
[2018-02-26 12:39:57 -0700] [28155] [INFO] Listening at: http://127.0.0.1:8000 (28155)
[2018-02-26 12:39:57 -0700] [28155] [INFO] Using worker: sync
[2018-02-26 12:39:57 -0700] [28199] [INFO] Booting worker with pid: 28199
# sudo su -
# cd /etc/nginx/site-available
# vi nginx-test.conf
server {
listen 80;
server_name nginx-test;
access_log /var/log/nginx/nginx-test-access.log;
error_log /var/log/nginx/nginx-test-error.log;
location /static {
alias /var/www/html/nginx-test/helloworld/static;
}
location / {
proxy_pass http://127.0.0.1:8000;
}
}
# cd /etc/nginx/sites-enabled
# ln -s /etc/nginx/sites-available/nginx-test.conf
# sudo service nginx restart
# vi /etc/hosts
Add line:
127.0.0.1 nginx-test
You can now just go to http://localhost (no port number needed unless you configured Nginx to use something besides the default port 80) and you should see "Hello World!" in bold type.
This is a very simple example but the whole idea is to give you an idea of how to deploy a Django app in your own server using gunicorn+Nginx. I do like Heroku and other cloud platforms because they allow you to focus on your app as it handles all of the server stuff. But, I believe it's a good idea to know how to deploy it in your own server and understand how it works there.
Personally, I prefer to use a simple Ubuntu server I am renting from Linode at the moment. Of course, this means I will be responsible for deploying my Django projects and all the server management that comes with it. It's actually not hard and doesn't take up a lot of time especially when you can write bash scripts to handle most of the maintenance work.
You can see also, that each project can have its own version of Python and own version of Gunicorn. So, you will always have options when using this kind of setup. Enjoy!