Python + Gunicorn + Apache - A Simple and Performant Method to Deploy your Django Projects

Published on Feb. 25, 2018, 9:43 a.m.

Note: This is not a discussion about Apache being better than its competitors. It's only about how to set up Django, Gunicorn and Apache.

I used to host my Django projects with mod_wsgi and Apache. It worked well for most things. Some of its advantages are that it's fast and you only have to start up the Apache service (much like you would do for a PHP project).

One of its disadvantages is having to use one Python version for all sites being served by an Apache instance. So, even in Ubuntu 16.04 (which is the most recent stable LTS) and CentOS 7, Python 2.7 is installed. And with Python 3 being available for a LONG time now, I would still compromise and say I can still use Python 2.7 for anything I need to write now.

Well, that was fine a couple of years ago until the announcement was made that Python 2.7 will not be maintained past 2020. In fact, here's a countdown clock if you'd like to see. It's a good thing to stay ahead of the curve especially when the future has been spelled out.

Also, while it would be nice again to put everything in 3.6.4 (at the moment), I still will have some projects I host will need to stay at the 2.7 level. So, how can we deploy our Django project to an environment that will allow us to keep everything at their version until their owners give the OK to upgrade the code base?

Well, we can use Gunicorn and Apache to serve multiple projects that may use many different versions of Python. What is Gunicorn? First off, its more formal name I suppose is Green Unicorn which is usually shortened to "Gunicorn". Gunicorn is a Web Server Gateway Interface (WSGI) server implementation that is commonly used to run Python web applications. Unlike mod_wsgi, it runs its own server to process any frame work that can use wsgi like Django, Flask, Pylons, etc.

Now, if we have gunicorn to run as an actual wsgi server with a Django project, why do we need Apache? Well, gunicorn handles the wsgi part but does not handle serving static assets like images, css or javascript that must be sourced from disk. Gunicorn is also never meant to be able to handle the rigors and security needs that should be met for any site on the Internet today. Apache has been around for some time and excels in serving static content and in securing your site from the wolves that prowl the Internet.

So, I want to demonstrate in this article how we can take a simple Django Hello World application and serve it using Gunicorn and Apache.

First off, let's set up a Django Hello World app as mentioned in another article that tells just how to do that. Once you're done there, come back here and I'll guide you the rest of the way.

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>
    <title>{{ message }}</title>
    <link rel="stylesheet" href="/static/css/base.css">
    <p class="hello_world">{{ message }}</p>

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/ and add this setting:


    os.path.join(BASE_DIR, 'static'),

Now, let's run the Django development server and take a look at our work.

# ./ 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: (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've 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 Apache2

It is possible you may already have Apache2 (or httpd in RedHat packaging vernacular) installed. You can check that fairly quickly.

If you're on Ubuntu, issue:

# sudo service apache2 start


# sudo systemctl start apache2

If you're on CentOS 7 or Fedora or RHEL, you can issue:

# sudo service httpd start


# sudo systemctl start httpd

And you should see some kind of output that tells you that it's at least installed. If you get the idea from the output that it's not installed, then we need to install it.

If you're on Ubuntu, then follow this path for install and configuration:

Install Apache as root on Ubuntu:

# sudo apt install apache2

or on Redhat/CentOS/Fedora:

# sudo apt install httpd

Configure the Apache service:

# cd /etc/apache2/conf

or on Redhat/CentOS/Fedora:

# cd /etc/httpd/conf

Open apache2.conf or httpd.conf and add the following line:

ServerName (or whatever your external ip address is for this server)

In /etc/[apache2|httpd]/sites-available, create a vhost file called helloworld.conf and add the following VirtualHost configuration:

<VirtualHost *:80>
        # The ServerName directive sets the request scheme, hostname and port that
        # the server uses to identify itself. This is used when creating
        # redirection URLs. In the context of virtual hosts, the ServerName
        # specifies what hostname must appear in the request's Host: header to
        # match this virtual host. For the default virtual host (this file) this
        # value is not decisive as it is used as a last resort host regardless.
        # However, you must set it for any further virtual host explicitly.

        ServerAdmin webmaster@localhost
        DocumentRoot "/path/to/django/project"
        ServerName alfmonitor # or whatever hostname you would like to use

        # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
        # error, crit, alert, emerg.
        # It is also possible to configure the loglevel for particular
        # modules, e.g.
        #LogLevel info ssl:warn

        ErrorLog ${APACHE_LOG_DIR}/helloworld-error.log
        CustomLog ${APACHE_LOG_DIR}/helloworld-access.log combined

        # For Redhat systems, we'll need instead:

        # ErrorLog logs/helloworld-error.log
        # CustomLog logs/helloworld-access.log combined

        # For most configuration files from conf-available/, which are
        # enabled or disabled at a global level, it is possible to
        # include a line for only one particular virtual host. For example the
        # following line enables the CGI configuration for this host only
        # after it has been globally disabled with "a2disconf".
        #Include conf-available/serve-cgi-bin.conf

        ProxyPass /static/ !
        ProxyPass / http://localhost:8000/

        <Directory "/path/to/django/project/static/">
            Order allow,deny
            Allow from all
            Options Indexes FollowSymLinks MultiViews
            Satisfy Any
            #AllowOverride None

Save the file. In /etc/[apache2|httpd]/conf/sites-enabled run:

# ln -s /etc/apache2/sites-available/helloworld.conf

or on Redhat/CentOS/Fedora:

# ln -s /etc/apache2/sites-available/helloworld.conf

This will create a symlink to the config, letting Apache know to include this virtual host in its configuration.

On a Redhat based system (it loads all out of the box modules by default), you can skip this step but on Ubuntu you will need to load the correct modules:

# cd /etc/apache2/mods-enabled

and run:

# ln -s /etc/apache2/mods-available/proxy.* 
# ln -s /etc/apache2/mods-available/proxy.conf .
# ln -s /etc/apache2/mods-available/proxy.load .
# ln -s /etc/apache2/mods-available/proxy_balancer.conf .
# ln -s /etc/apache2/mods-available/proxy_balancer.load .
# ln -s /etc/apache2/mods-available/ssl.conf .
# ln -s /etc/apache2/mods-available/ssl.load .
# ln -s /etc/apache2/mods-available/socache_shmcb.load .
# ln -s /etc/apache2/mods-available/proxy_http.load .
# ln -s /etc/apache2/mods-available/slotmem_shm.load .

Start Apache:

# service apache2 start

Now, with gunicorn running and Apache running:

# gunicorn helloworld.wsgi
# sudo service apache2 (or httpd) start

You can now just go to http://localhost (no port number needed unless you configured Apache 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. I do like Heroku and other cloud platforms because they allow one to focus on your app as they handle serving it up to your users. But, I believe it's a good idea to know how to deploy it in your own server. Personally, I prefer to use a simple Ubuntu server I am renting from Linode. 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!

If this blog is helpful, please consider helping me pay it backward with a coffee.

Buy Me a Coffee at