Recently, a friend of mine asked me to tell him more about the rationale behind using NGINX with a Django app and how these two work together. It actually makes perfect sense for someone to raise this question: Python has a builtin web server which can render pages, serve static files on its own. So what’s the point of adding one more component to the stack?

PHP Land

When I started to learn programming, my first language was PHP. The only program I needed to run PHP scripts was Apache. My understanding back then was that Apache can serve static files, plus it can run my PHP scripts on its own. It actually made sense: a server should handle every aspect of running my website. I didn’t know that Apache has a mod_php. So, when I started to program in Ruby and wanted to serve a website I had to use 2 components: NGINX and Unicorn. My reasonable concern was: why would I need 2 components, when I needed only 1 for PHP websites? So, I started to dig around about this topic.

Here’s the truth: Apache or NGINX servers can only serve static files. They can’t run your PHP or Python scripts. Your Python script can only be executed by… a Python interpreter, because that’s the only program which understands Python language. If a Python script can be executed by a bigger server software (e.g. NGINX), it means that somewhere inside the server code a Python interpreter program is embedded. That’s what’s happening with Apache being able to run PHP scripts on its own: it has a PHP interpreter embedded inside it (by the way, this component is called mod_php). Each time you request a PHP file from Apache, the following happens:

  1. Apache inspects the HTTP request headers, extracts a file requested in the URL and invokes its builtin PHP interpreter with these parameters
  2. PHP interpreter executes the script file and sends the generated HTML (plus any possible headers) back to Apache
  3. Apache sends the HTTP response, which contains generated HTML from previous stage, back to the client

So, this is what happens with Apache and PHP combination. In order to have a better understanding of a Python server configuration, I’d like to explain you what a reverse proxy is first.

What is a reverse proxy?

Usually, when a person makes an HTTP request to an NGINX server, the server examines the URL, looks up the corresponding file on the filesystem and sends the HTTP response with that file back to the client. NGINX knows where to lookup the file on the filesystem because it was configured to look for files in a certain directory. In this scenario NGINX acts as a regular server: it receives an HTTP request from the client (usually, it is a browser), looks up the file on the computer where the NGINX is running, and sends an HTTP response back to the client. NGINX can also be configured to behave in a different way: instead of looking up files on its own, it can delegate this process to a different program.

Let’s first talk more about this hypothetical program, which we’re going to call X. This X program can be on the same computer (where NGINX is running) or on a different computer. It is expected that this X program can handle the HTTP request and send the HTTP response to the client on its own, the same way NGINX does. So, in case a client makes a request to the X program directly, this program should understand the request and be able to send the response to the client. These characteristics effectively make this program a server.

In the scenario where NGINX delegates its work to the X program, its workflow looks like this:

  1. A client (e.g. a browser) makes an HTTP request to the NGINX server
  2. NGINX receives an HTTP request, then it takes this request and forwards it to the X program. It does this by making the same HTTP request, that NGINX received earlier from the client, to the X program.
  3. X program receives the HTTP request from NGINX, builds an HTTP response and sends this response back to NGINX
  4. NGINX receives the response from X program
  5. NGINX sends the response, the one it got from X program, back to the client

So, basically, what we see here is a communication between two servers: NGINX and X program. NGINX is no longer a simple server here, it is a reverse proxy — it proxies a clients’ request to the destination server X, collects the response and sends it back to the client. This X program is usually a builtin Python server, the one that’s usually shipped with a Django app.

What is Gunicorn and what is it used for?

Gunicorn is a web server, written completely in Python. It can be launched with a Python interpreter only, because, obviously, it is a Python program. It can run as a standalone server and handle clients requests directly (including serving static files). Though, using it this way is highly inefficient, mainly because Python itself is slow. That’s where NGINX comes into picture. NGINX is written in C which means that it is very fast, but NGINX can’t run your Python programs, it can only serve static files, remember? So, the usual scenario is letting NGINX serve static files (the task where NGINX shines) and forward all dynamic requests (the ones that are expected to be handled by your Django app) to Gunicorn. In this situation, NGINX acts as a reverse proxy for a Gunicorn server, passing all dynamic requests to Gunicorn, and as a regular server for static files, handling this task on its own. So, the requests for static files never reach Gunicorn server.

Besides, serving static assets, NGINX can also cache the responses from Gunicorn in the filesystem, which means that any future requests to the same URL are treated as static requests — this means that those requests will never reach Gunicorn and be handled completely by NGINX. This in turn means that a website will have a much higher performance, because we are able to skip the weakest point in the infrastructure — Gunicorn and use the fastest program to serve the data. NGINX can also do a lot of other useful tasks, like compressing the responses and handling SSL certificates, taking a lot of burden off the Gunicorn, and doing that in the fastest way possible. This leaves Gunicorn with only one task to do — handling dynamic requests, because NGINX can’t run Python programs, remember? In other stacks the situation is pretty much the same: if you want to execute scripts there must be an interpreter for this language running which will handle dynamic requests either directly (which is inefficient) or behind NGINX, which acts as a reverse proxy.