In this article, we’re going to talk about WSGI, Gunicorn, and Nginx. We will discuss why you need these things, and how they work together with Django.

Here’s the video if you prefer watching over reading:

The article will help you understand the process of deploying Django application better and I hope that at the end of it, you will understand what the heck is WSGI because for me, personally, wsgi.py file was some kind of magic. I didn’t understand why does this even exist.

Project

I was thinking about how to explain these things to you, and I think that the best way will be to show an example. So, let’s create a simple Django server and host it on DigitalOcean.

When you successfully created and connected to your DigitalOcean droplet through SSH, the first thing that we need to do is to install python dependencies. We only need Django and Gunicorn:

$ pip install django gunicorn

Now we can create a Django project:

$ django-admin startproject lifecycle .

What we usually do after we create a Django project is we type python manage.py runserver in the terminal. Runserver management command is a command that built into Django and the purpose of this command is to run a development server. This server is only suitable for development, not production. And if you open Django documentation, you will see that it warns you to never use this on production (https://docs.djangoproject.com/en/dev/ref/django-admin/#runserver).

This management command is not scalable and it’s not reliable. It should be only used locally. So, instead of using this management command, we’re going to use Gunicorn.

Gunicorn & WSGI

Gunicorn is a library that is battle-tested, it’s reliable and it’s also scalable. You can scale it by creating multiple workers of different types.

Gunicorn is a WSGI server and you can run any web application using Gunicorn if it supports WSGI. So, you can run not only your Django application, but you can also run, for example, Flask application using Gunicorn because it also supports WSGI.

WSGI is a protocol, it’s a standard of communication between a web server and a web application. Basically, between Gunicorn and Flask or Django. The web server should know how to speak to the web application, and the web application should know how to respond to the web server. They both should talk using the same language. And the language is WSGI.

As for the WSGI itself, the way it works is if we want to create a WSGI compatible python application ourselves, we need to define a callable object that takes two arguments and this callable object should return an iterable of strings.

Here’s the most basic example of WSGI compatible script in python:

# hello_world.py

def process_http_request(environ, start_response):
    status = '200 OK'
    response_headers = [
        ('Content-type', 'text/plain; charset=utf-8'),
    ]
    start_response(status, response_headers)
    text = 'Hello World'.encode('utf-8')
    return [text]

And if we wanna test this function, all we need to do is we need to specify a path to this function when we run a Gunicorn server:

$ gunicorn hello_world:process_http_request --bind 0.0.0.0:8000

And if we open the page now, we should see Hello World text:

As for the arguments of the function. The first argument is environ. It’s a dictionary that has different information about the request that the browser sent to the server.

Gunicorn gets the request, it populates the dictionary, and when it calls the function process_http_request, it passes this dictionary as a parameter.

As for the second argument, start_response. It’s a function that we need to call when we want to send a status code and headers of the response.

Gunicorn & WSGI with Django

Now, let’s get back to Django. Django has wsgi.py file that is generated automatically when we create a Django project. And if we open this file, we will see that Django calls get_wsgi_application function:

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')

application = get_wsgi_application()

This function returns WSGI compatible callable object. This callable object also takes two arguments: environ and start_response. And it also returns an iterable of strings.

Basically, it’s the same callable object as we had before. The only difference is that the callable object that built into Django is much more complicated than the one that we had.

Let’s run Gunicorn WSGI server for our Django application:

$ gunicorn lifecycle.wsgi:application --bind 0.0.0.0:8000

Right now we’re running a WSGI server on port 8000. But, that’s not everything that we need to run. We’re not finished yet. Just Gunicorn and Django are not enough. We also need a web server like Nginx.

Nginx with Django

Nginx server is going to interface with the outside world. Basically, all the requests that the browser sends will be handled and processed by Nginx.

Here’s how Nginx, Gunicorn, and Django work together. If the browser wants dynamically generated content like an HTML page, then, in that case, Nginx will pass the request to Gunicorn and Django because Django is responsible for generating HTML pages. And, when the job is done, when Django generated a page, Gunicorn will send this HTML page to Nginx, and Nginx will send this page back to the browser.

However, you don’t need to pass the request to Gunicorn all the time. For example, if the browser wants static or media files, then there is no need to pass this request to Gunicorn. Nginx can directly serve these files to the browser.

And that’s basically the flow. Now let’s try to quickly install Nginx and run a web server. Here’s the command to install Nginx:

$ sudo apt install nginx

After installing Nginx, we need to configure our firewall. Basically, we need to allow incoming traffic on port 80 and port 443 if we wanna set up HTTPS. Since we don’t need HTTPS, let’s just open port 80:

$ sudo ufw allow 'Nginx HTTP'

Let’s create a configuration file:

$ nano /etc/nginx/sites-available/lifecycle
upstream server_django {
    server 0.0.0.0:8000;
}

server {

    listen 80;

    location / {
        proxy_pass http://server_django;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /static/ {
        alias /app/static/;
    }

}

As you can see from the configuration file, if the location is root, then we just proxy requests to Gunicorn server. If the location is static, then we serve static files directly. There is no need to proxy requests to Gunicorn.

Now when we have the configuration file, we need to remove a default Nginx configuration file, and we need to add the configuration file that we just created to sites-enabled folder:

$ rm -rf /etc/nginx/sites-available/default
$ rm -rf /etc/nginx/sites-enabled/default
$ sudo ln -s /etc/nginx/sites-available/lifecycle /etc/nginx/sites-enabled/

The only thing left is to restart Nginx:

$ sudo systemctl restart nginx

Conclusion

That’s everything I wanted to discuss today. When I started writing this article, I was thinking about explaining the whole life cycle of Django, but, as you can see, just explaining WSGI, Gunicorn, Nginx took quite a lot of time.

If you’re interested in learning the life cycle of Django, leave a comment below or subscribe to my YouTube channel. I will probably make a video about that.

5 Comments

  1. Jorge May 26, 2021 at 8:24 am

    great article!

    I just had a question. So the only reason to use Nginx on top of WSGI is when we need to serve static files? is there another reason to use Nginx alongside WSGI?

    Reply
  2. Tom September 16, 2021 at 7:06 pm

    in the paths above, what is etc?

    Reply
  3. Vitor de Lima Tamberlini November 11, 2021 at 9:48 am

    Great article!
    Very well explained and easy to understand.

    Reply
  4. Ashfaque Chowdhury February 11, 2023 at 2:45 pm

    Isn’t Nginx being used as a reverse proxy as well for the application server?

    Reply

Leave A Comment

Your email address will not be published. Required fields are marked *