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

Note published: 06/01/2020

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 third-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:


user root;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

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;
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 / {
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.


Developing a mobile app with React-Native and Django REST framework, part 1: token authentication with Django REST auth, preparing a registration endpoint

Note published: 10/01/2020

Django REST framework is a modern, light and relatively quick solution to develop a full-capacity backend for any app, web or mobile. In terms of frontend, React-Native is one of the best ways to create mobile apps for both iOS and Android. Coupling these two amazing technologies seems like a perfect idea, and guess what - it is! In this tutorial we will be building a social network app which you can download from AppStore or Google Play.

First thing to do is create your directory which will contain both frontend and backend:

$ mkdir vetpetapp
$ cd vetpetapp

Let's start with user registration and authenticatoion. First of all, we must install Django and Django REST framework in order to create our first customised User model and a first API endpoint.

$ pip install django
$ django-admin startproject backend
$ cd vetpetapp/backend
$ python manage.py startapp users

It's a very convinient custom -- to create different apps inside your django project, each app being responsible for its own part of the overall logic. In this step we have created a django project called 'backend' and then inside that project we've started an app called 'users'. This app will handle all the custom user model code.

Now we must update our backend/settings.py file INSTALLED_APPS setting: add a newly created app and specify that we’ll be using a custom user model called CustomUser instead of the default Django User model, which is too inconvinient and simpleS for a social network app.

INSTALLED_APPS = [
   ...
   'users',
   ]

AUTH_USER_MODEL = 'users.CustomUser'

Now go to your 'users' app models.py and create a new custom model, extending from a default AbstractUser model, supplying it with a simple 'name' field:

from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
   name = models.CharField(blank=True, max_length=255)

def __str__(self):
   return self.email

We must have a way to create new users and for that we'll need a form. Create a new forms.py file in your 'users' app:

from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from . models import CustomUser

class CustomUserCreationForm(UserCreationForm):
   class Meta:
      model = CustomUser
      fields = ('username', 'email')

class CustomUserChangeForm(UserChangeForm):
   class Meta:
      model = CustomUser
      fields = UserChangeForm.Meta.fields

After that we must update our 'users' app admin.py file and register our Custom User model.

from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from . forms import CustomUserCreationForm, CustomUserChangeForm
from . models import CustomUser

class CustomUserAdmin(UserAdmin):
   add_form = CustomUserCreationForm
   form = CustomUserChangeForm
   model = CustomUser
   list_display = ['email', 'username', 'name']

admin.site.register(CustomUser, CustomUserAdmin)

After all this work we must initiate our database with 'makemigrations' which will generate the SQL commands and 'migrate' which will create the database table in accordance with these commands.

$ python manage.py makemigrations
$ python manage.py migrate

We should also create a super user in order to login to the http://127.0.0.1:8000/admin and create a couple of test users to work later:

$ python manage.py createsuperuser
$ python manage.py runserver



Now it's time to add Django REST Framework with a very usefull package django-rest-auth to make our life much easier with its out of the box solution for all the basic user endpoints, like login, logout, restore password and so on. And for the user registration it's essential to add one more package namely django-allauth.

$ pip install djangorestframework
$ pip install django-rest-auth
$ pip install django-allauth

After pip-installing new packages we must add them to our ISTALLED_APPS and also make some other changes in backend/settings.py file:

INSTALLED_APPS = [
   ...
   'rest_framework',
   'rest_framework.authtoken', #since we are covering only token authentication here
   'rest_auth',
   'django.contrib.sites', #required by django-rest-auth package
   'allauth',
   'allauth.account',
   'rest_auth.registration',
   ...
   ]

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# we need that since according to our configuration the system must send an email confirmation after a new user registers

SITE_ID = 1 # required by django.contrib.sites

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
# we've left only token authentication and in this tutorial we don't really need other types
],
}

Now we must create endpoins which will be later connected to our react-native app via superagent request.
Django rest-auth provides us with all the neccassary basic addresses for fully functional user authentication: /rest-auth/login/, /rest-auth/logout/, /rest-auth/password/reset/ ecc. You can check the complete list available in rest-auth documentation.. All we have to do is connect the rest-auth endpoints to our django app by editing our apps and files, starting with the main URL conf backend/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
   path('admin/', admin.site.urls),
   path('api/v1/', include('vpa_api.urls')), # we instruct main conf to include urls from an app we must create - 'vpa_api'
   path('users/', include('users.urls')), # and inculde urls from the app we've already created - 'users'
]

Now create this app, which will be responsible for our authentication logic:

$ python manage.py startapp vpa_api

And don't forget to include it in your INSTALLED_APPS setting. After that we create a ne urls.py file inside the vpa_api dir and put this code inside:

from django.urls import include, path

urlpatterns = [
   path('rest-auth/', include('rest_auth.urls')),
   path('rest-auth/registration/', include('rest_auth.registration.urls')),
]

So as you can see we don't really need to create endpoints, we only must include the urls that were already provided by rest-auth and rest-auth.registration. Now you an open all these endpoints in your browser or try to run CURL on them, both POST and GET to check the correctness of responses. Essentially a POST CURL command is similar to what our superagent request will pass from our react-native app, which is a JSON data for user registration form:

curl --header "Content-Type: application/json" \
--request POST \
# this is what our registration endpoint is expecting: username, email, password min. 8 char long with special symbols and password repeat
--data '{"username": "some_test_user", "email": "some_test_user@email.xz", "password1": "Some_long_and_valid_Password_0", "password2": "Some_long_and_valid_Password_0"}' \
http://127.0.0.1:8000/api/v1/rest-auth/registration/

See you terminal messages to monitor the correctness of your request or simply open the user list via django admin and check that some_test_user has been successfully created by your CURL POST command. At this point we are ready to start working on our react-native app, create a SignUp.js component, pass this JSON data from SignUp.js to the same /registration/ endpoint and celebrate our first django/react-native cooperation success.