Serving multiple Django apps with Gunicorn and Nginx (CentOS 7)

Kate Dmitrieva
7 min readNov 11, 2019

It’s a real pleasure working with Django locally: thanks to SQLite and a simple built-in development webserver you are able to run all your applications with little or no effort at all. However deploy might become a headache. What to choose to serve your site, Apache or NginX, mod_wsgi or Gunicorn, or maybe something else or maybe all together? There are plenty of wonderful tutorials on how to deploy your Django app with our servers of choice — Gunicorn and NginX, but what if you have several apps and want to serve them on second-level domains, like for example if you have a professional portfolio consisting of many different websites on third-level domains with one common second-level domain? In this guide we will demonstrate how to achieve this goal on CentOS 7 (some steps and tools will differ on other Linux OS).

This guide uses a root user to simplify the demonstration, however for security reasons we recommend that you create another user with root privileges. You must have at least two ready-to-deploy Django apps and a fresh CentOS 7.

For each app we will use its own virtual environment, so you could use different Django versions and different settings in each venv. It doesn’t matter which database you use: in this guide we will use PostgreSQL for one app and SQLite for another.

Inside each app’s virtual env we will pip-install Gunicorn server and later create separate gunicorn.service and gunicorn.socket files for each project. We will then set up Nginx in front of Gunicorn instances and setup nginx.conf to listen to multiple sockets.

First lets make sure to have all our domains set up and ready to be connected to our django apps. For that we must login to our DNS control panel and create A-type DNS records for each site and direct them all to your CentOS 7 instance IP address.

With all our DNS settings ready, let’s setup your CentOS to serve django apps to these domains.

First of all, in order to use yum normally we need to install epel-release package:

$ yum install epel-release

After that we’ll be able to install, start and enable our general-purpose server of choice — nginx:

$ yum install nginx$ systemctl start nginx$ systemctl enable nginx

Now if one or all your apps are written in Django version ≥ 2.0 chances are you will need Python3 to work with, but all that Centos 7 has to offer at this point is Python2. To solve this inconvenience you need to run following commands, starting with Python version check, just to be sure:

$ python -V

>>> Python 2.7.5

This will definitely conflict with your Django 2 apps, so:

$ yum install centos-release-scl$ yum install rh-python36$ scl enable rh-python36 bash

Checking again:

$ python -V

>>> Python 3.6.3

Much better! Now we can serve each app with its appropriate python version.

Remember to check if your system’s selinux is active:

$ getenforce

In case if response is “Enforcing”, Selinux will most likely refuse connection to your django app unless its configured to permit it, so either turn it off (setenforce Permissive) or configure it in the correct way (this tutorial will not cover this topic, but you can easily find a number of decent step-by-step guides on how to config selinux correctly).

Let’s proceed. Create a new directory to store all your django projects in it: mkdir /var/www . Put all your django projects inside so that your www directory is structured like this:

/var/www/:
├── django_one
├── django_two
├── django_three

Since we have different projects presumably built with different versions of Django and Python, it is really important to create a virtual environment for each app. We will see how to setup venv and test django runserver for the first project or django_app_one, and after that there is no need to repeat this demonstration for other apps since the process is more or less similar in each case.

Create virtual env for selected project:

$ python3 -m venv django_one_venv

Later on you will have to specify this venv’s address in your gunicorn config file. Enable virtual environment:

$ source django_one_venv/bin/activate

Install the corresponding Django version:

$ (django_one_venv) pip install django==2.1

Install all additional packages or simply run the following command:

$ (django_one_venv) python3 manage.py runserver django_one.your_domain.com:8000

At this point your app must be available on django_one.your_domain.com:8000, but now it is being served with django built-in server which is not acceptable for production, so let’s continue and set up gunicorn.

While nginx will serve all apps and use one configuration file for this job, gunicorn will be split into several instances, and each of these instances will serve its dedicated app with its separate config and socket file. In other words, for each django app we must create a django_one.service file and a django_one.socket file, both of them must be stored in /etc/systemd/system directory — where normally (i.e in case of one single django app) the gunicorn.service file would be stored.

So, for our django_one app let’s first install its personal gunicorn inside this app’s venv (later on we will do the same for each app that we want to serve on a separate second-level domain):

$ (django_one_venv) pip install gunicorn

Moving on to the next step, let’s create django_one.socket and put it in /etc/systemd/system/:

[Unit]
Description=gunicorn socket
[Socket]
ListenStream=/run/django_one.sock
[Install]
WantedBy=sockets.target

Then let’s create django_one.service which will start gunicorn for this particular app with this particular socket. We’ll put this file right next to our .socket file in /etc/systemd/system/:

[Unit]
Description=gunicorn daemon
Requires=django_one.socket
After=network.target
[Service]
User=root
Group=root
WorkingDirectory=/var/www/django_one
ExecStart=/django_one_venv/bin/gunicorn --workers 3 --bind unix:/run/django_one.sock \
django_one.wsgi:application
[Install]
WantedBy=multi-user.target

So when we want to start gunicorn for this particular app, we will need to run the following commands:

$ systemctl start django_one.socket
$ systemctl start django_one.service #equals to 'start gunicorn'

And then, in case if everything seems to work fine:

$ systemctl enable django_one.socket
$ systemctl enable django_one.service

Basically, this process must be repeated for each app, so in the end you will have several properly configured .socket files and same amount of .service files in /etc/systemd/system/ directory.

Okay, now we must configure nginx to include all our apps in its nginx.conf, which can be found here: /etc/nginx/nginx.conf. What you need to do is to add a new server record for each django app:


server {
listen 80;
server_name django_one.your_domain.com;

location = /favicon.ico {
access_log off; log_not_found off;
}
location /static {
alias /var/www/django_one/static_root;
#or plain /var/www/django_one/static if you don’t use static_root in your django settings.py

}

location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarder-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://unix:/run/django_one.sock;
}

}

So in case if you have two django apps - django_one and django_two - your nginx.conf must look something like this:

# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/
user root;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;

server {
listen 80;
server_name django_one.your_domain.com;

location = /favicon.ico { access_log off; log_not_found off; }
location /static {
alias /var/www/django_one/static_root;

}

location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarder-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://unix:/run/django_one.sock;
}

}
server {
listen 80;
server_name django_two.your_domain.com;
location = /favicon.ico { access_log off; log_not_found off; }
location /static {
alias /var/www/django_two/static_root;

}
location / {
#include proxy_params;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarder-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://unix:/run/django_two.sock;
}
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
# Settings for a TLS enabled server.
#
# server {
# listen 443 ssl http2 default_server;
# listen [::]:443 ssl http2 default_server;
# server_name _;
# root /usr/share/nginx/html;
#
# ssl_certificate "/etc/pki/nginx/server.crt";
# ssl_certificate_key "/etc/pki/nginx/private/server.key";
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 10m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
#
# # Load configuration files for the default server block.
# include /etc/nginx/default.d/*.conf;
#
# location / {
# }
#
# error_page 404 /404.html;
# location = /40x.html {
# }
#
# error_page 500 502 503 504 /50x.html;
# location = /50x.html {
# }
# }
}

If you have more than two django apps, just add a new server setting for each one. You will need yo restart nginx after modifying nginx.conf:

$ systemctl restart nginx

All the sockets and all the services specified for your django apps at this point must be up and running.

So this is it, now you should be able to open your first django project here: django_one.your_domain.com, and your second django app here: django_two.your_domain.com. I hope this guide was helpful and appreciate comments, questions and suggestions. Thank you all))

--

--