Blog
Fog over mountain forest with "Django NGINX Gunicorn" written on top
18 Apr 2020

Django, NGINX & Gunicorn

A guide to hosting a Django website with NGINX and Gunicorn.

In the previous post, I explained the setup of my CentOS VPS, here I will explain how to host a Django website, using NGINX as a webserver and Gunicorn as a reverse proxy.

Project Folder & Virtualenv

We are about to create the Django project we want to host, so from now on keep in mind that:

  • Every reference to mysite or mysite.com will have to be replaced with your own project name/domain.
  • I like to keep all my websites in ~/webapps, in separate subfolders, your preference may vary.

Project Folder

Let’s create our project folder.

cd ~/webapps 
mkdir mysite 
cd mysite

Virtualenv

Now we will create a Python Virtualenv, this will be necessary if you plan to serve multiple Django websites, where every website should have its own dependencies.

python3 -m venv venv      # (This will only work with Python 3) 
source venv/bin/activate  # Activate the virtualenv

At this point, your terminal shell should look like this

(venv) user@Hostname:~/webapps/mysite$

That (venv) means we are now inside the Python Virtualenv, so every pip package installed will be installed inside this single environment, and not system-wide.

Installing Dependencies

For the sake of simplicity, I will be installing only 2 packages that are essential for this guide.

pip install --upgrade pip    # First let's update pip 
pip install django           # Install Django inside the virtualenv 
pip install gunicorn         # Install Gunicorn (needed later)

Note for Django 3.0

If you want to use the new Django 3.0 that came out, and you use SQLite as database, you will need SQLite 3.8, but CentOS 7 ships only with SQLite 3.7. To update it, you can install it from Fedora packages like this:

wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-3.8.11-1.fc21.x86_64.rpm 

sudo yum install sqlite-3.8.11-1.fc21.x86_64.rpm

And voilĂĄ, you now have SQLite 3.8!

Exit the Virtualenv

To exit the Virtualenv, simply type this in the shell.

deactivate

New Django Project

Alright, time to init our Django Project. If you already have a project you want to host you can skip this step, just make sure to install all the needed dependencies inside the virtualenv.

So, let’s go.

The following steps should run inside the virtualenv, so if you don’t see that (venv) on your terminal go ahead and run:

source venv/bin/activate

Now that we are inside the virtualenv, we can initialize our Django Project.

django-admin startproject mysite . 
# The dot at the end means we are initializing the project in the current directory

After that command, you should have all the project files created. We can now do some other commands to setup Django:

python manage.py migrate # Makes initial migrations to the Database 

# And create an Admin user: 
python manage.py createsuperuser 
# Then fill the required data with username, email and password

After that we are done with python, so you can exit the virtualenv (with deactivate).

Configuring Gunicorn

To host Python applications (Django) with NGINX we first need Gunicorn, a Python HTTP Server. This will serve our webapp, then NGINX will point to this web server.

First of all, we need to create the Gunicorn socket and service, and we do so by creating 2 files that will contain a Systemd process for the Python web server, and one for the socket.

Gunicorn Socket

Create and open the mysite.com.socket file under /etc/systemd/system/

# (remember to change "mysite.com" with your site domain) 
sudo nano /etc/systemd/system/mysite.com.socket

Now inside it paste this code:

/etc/systemd/system/mysite.com.socket

# Gunicorn socket file 
# - Adjust folder paths if different 

[Unit] 
Description=mysite gunicorn socket 

[Socket] 
ListenStream=/run/mysite.com.sock 

[Install] 
WantedBy=sockets.target

Gunicorn Service

This service will connect to the previously created socket. Create and open the mysite.com.service file under /etc/systemd/system/

# (remember to change "mysite.com" with your site domain) 
sudo nano /etc/systemd/system/mysite.com.service

Now inside it paste this code:

/etc/systemd/system/mysite.com.service
# Gunicorn service file 
# - Replace "mysite" with your project name 
# - Replace "simone" with your username 
# - Adjust folder paths if different 

[Unit] 
Description=mysite gunicorn daemon 
Requires=mysite.com.socket 
After=network.target 

[Service] 
User=simone 
Group=nginx 
WorkingDirectory=/home/simone/webapps/mysite ExecStart=/home/simone/webapps/mysite/venv/bin/gunicorn \
    -w 4 \	
    --error-logfile /home/simone/webapps/mysite/gunicorn.log \	
    --bind unix:/run/mysite.com.sock \	
    mysite.wsgi:application 
    
[Install] 
WantedBy=multi-user.target

This will be our Python web server, let me explain a couple of things about it:

What the hell is happening here?

  • WorkingDirectory: It’s the root folder of your project.
  • ExecStart: The first path here tells Systemd to use the Gunicorn we installed inside our project’s virtualenv (at the “Installing Dependencies” step). Then -w specifies how many workers to spawn (usually (2 x $num_cores) + 1 ). Next, the –error-logfile tells Gunicorn where to save the logfile, in this example will be inside our project folder, put it where you want. Finally, –bind will point to the .sock file created by mysite.com.socket.

Putting aside the boring stuff, we now have the Python web server, we just need to start it, and run these 6 commands:

sudo systemctl start mysite.com.socket 
sudo systemctl enable mysite.com.socket 
sudo systemctl start mysite.com.service 
sudo systemctl enable mysite.com.service 
sudo systemctl daemon-reload 
sudo systemctl restart mysite.com.service

This will start the web server and enable it to autostart if your VPS/Server is rebooted.

Setup NGINX - Round 1

NGINX will be our proxy, it takes all the requests for mysite.com and tells them to go to the Gunicorn web server we set up earlier.

I called this step “Round 1” because we actually need to edit this config twice to have our nice HTTPS there:

  • First we setup a basic NGINX config that serves the website as HTTP
  • Then we request a Let’s Encrypt Certificate, and it will check if our website is actually running online, that’s why we need it online as HTTP.
  • Then, after we obtained our certificate, we can edit the NGINX config to add the “SSL” part.

I used a configuration generated with Digitalocean’s NGINX configurator, that consists in some files that will contain some general settings, a main nginx config file, and a file containing your website configuration.
To make things more clear, this is the final folder structure we will have

/etc/nginx/	
  ┣╸conf.d/	
  ┃	    ┗╸mysite.com.nginx.conf	
  ┃	
  ┣╸nginxconfig.io/	
  ┃	    ┣╸general.conf	
  ┃	    ┣╸letsencrypt.conf	
  ┃	    ┣╸proxy.conf	
  ┃	    ┗╸security.conf	
  ┃	
  ┣╸dhparam.pem	
  ┗╸nginx.conf

Let’s go from top to bottom, for every file you will find a codeblock with the path to that file at the bottom:

/etc/nginx/conf.d/mysite.com.nginx.conf

server {
	listen 80;
	listen [::]:80;

	server_name mysite.com;

	include nginxconfig.io/letsencrypt.conf;

	# reverse proxy
	location / {
		proxy_pass http://unix:/run/mysite.com.sock;
		include nginxconfig.io/proxy.conf;
	}
}

/etc/nginx/nginxconfig.io/general.conf

# favicon.ico
location = /favicon.ico {
	log_not_found off;
	access_log off;
}

# robots.txt
location = /robots.txt {
	log_not_found off;
	access_log off;
}

# assets, media
location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
	expires 7d;
	access_log off;
}

# svg, fonts
location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ {
	add_header Access-Control-Allow-Origin "*";
	expires 7d;
	access_log off;
}

# gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

# brotli (commented because was giving errors, but somehow brotli still works)
# brotli on;
# brotli_comp_level 6;
# brotli_types text/plain text/css text/xml application/json application/javascript # application/rss+xml application/atom+xml image/svg+xml;

/etc/nginx/nginxconfig.io/letsencrypt.conf

# ACME-challenge
location ^~ /.well-known/acme-challenge/ {
	root /etc/letsencrypt;
}

/etc/nginx/nginxconfig.io/proxy.conf

proxy_http_version	1.1;
proxy_cache_bypass	$http_upgrade;

proxy_set_header Upgrade			$http_upgrade;
proxy_set_header Connection 		"upgrade";
proxy_set_header Host				$host;
proxy_set_header X-Real-IP			$remote_addr;
proxy_set_header X-Forwarded-For	$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto	$scheme;
proxy_set_header X-Forwarded-Host	$host;
proxy_set_header X-Forwarded-Port	$server_port;

/etc/nginx/nginxconfig.io/security.conf

# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# . files
location ~ /\.(?!well-known) {
	deny all;
}
user nginx;
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;

events {
	multi_accept on;
	worker_connections 65535;
}

http {
	charset utf-8;
	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	server_tokens off;
	log_not_found off;
	types_hash_max_size 2048;
	client_max_body_size 16M;

	# MIME
	include mime.types;
	default_type application/octet-stream;

	# logging
	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log warn;

	# SSL
	ssl_session_timeout 1d;
	ssl_session_cache shared:SSL:10m;
	ssl_session_tickets off;

	# Diffie-Hellman parameter for DHE ciphersuites
	ssl_dhparam /etc/nginx/dhparam.pem;

	# Mozilla Intermediate configuration
	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

	# OCSP Stapling
	ssl_stapling on;
	ssl_stapling_verify on;
	resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
	resolver_timeout 2s;

	# load configs
	include /etc/nginx/conf.d/*.conf;
}

Ok we miss just one more file, the dhparam.pem, you will need to generate it with openssl (code below).
Keep in mind that this file will take some time to generate, normally 3-4 minutes, so just be patient till it’s done, this will be unique for your server.

sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048

Once done, we have everything we need, so just to start (and enable) NGINX:

sudo systemctl start nginx 
sudo systemctl enable nginx

Remember that after every edit you make to the NGINX config file, you need to reload NGINX for it to see the updated content, you can do it like this:

# Checks if the new NGINX config has errors 
sudo nginx -t 

# Reload NGINX 
sudo systemctl reload nginx

Let’s Encrypt for SSL (HTTPS)

Time to get that green lock! To get an SSL Certificate, we first install certbot, a utility made to request free Let’s Encrypt Certificates.

sudo dnf install certbot python3-certbot-nginx

Before requesting a certificate, make sure your website is accessible from the internet! Certbot needs to view it to verify it’s an existing website!

Then, to request a certificate:

# For more information about this command, go here: 
# https://certbot.eff.org/lets-encrypt/centosrhel7-nginx 

sudo certbot certonly --nginx

This will generate some files under /etc/letsencrypt/live/mysite.com, we will need them later for the final NGINX config.

SSL Certificates, obviously, have an expiry. But fear not! We can renew it automatically as much as we want, and that’s done by using crontab with python3-certbot-nginx (installed previously with certbot).

You use it like this:

sudo echo "0 0,12 * * * root python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null

With this, crontab will try to renew every certificate we have, every day, all on its own, no action required!

Setup NGINX - Round 2

Almost there!
Now that we have our SSL Certificate, this will be the final NGINX config file.

/etc/nginx/conf.d/mysite.com.nginx.conf

# In this file, make sure to replace every path with your own paths.
# For example, every /home/simone with /home/yourusername
# and mysite.com with your own website name

server {
	listen 443 ssl http2;
	listen [::]:443 ssl http2;

	server_name mysite.com www.mysite.com;
	root /home/simone/webapps/mysite/public;

	# SSL
	ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
	ssl_trusted_certificate /etc/letsencrypt/live/mysite.com/chain.pem;

	# security
	include nginxconfig.io/security.conf;

	# logging
	access_log /var/log/nginx/mysite.com.access.log;
	error_log /var/log/nginx/mysite.com.error.log warn;

	# reverse proxy
	location / {
		proxy_pass http://unix:/run/mysite.com.sock;
		include nginxconfig.io/proxy.conf;
	}

	# additional config
	include nginxconfig.io/general.conf;
}

# HTTP redirect
server {
	listen 80;
	listen [::]:80;

	server_name mysite.com www.mysite.com;

	include nginxconfig.io/letsencrypt.conf;

	location / {
		return 301 https://www.mysite.com$request_uri;
	}
}

That’s a lot of stuff, I know.

For now, reload NGINX and you should have your website accessible over HTTPS!

sudo nginx -t 
sudo systemctl reload nginx

Errors management

“Something’s wrong, I can feel it!”

I, for sure, know the pain when you do all this stuff and nothing works, so let me give you a quick list of things to check if “something’s wrong”:

  • If you kept all the logs paths I wrote in this guide, you have 2 error logs files you can look for:
    NGINX error log is under /var/log/nginx/mysite.com.error.log, then Gunicorn error log is inside your project folder, so /home/yourusername/webapps/mysite/gunicorn.log.
  • Another thing that can go wrong is system services, the ones we started with systemctl. To see if one of them is running (and check if there are errors) just type sudo systectl status mysite.com.service or sudo systectl status mysite.com.socket (obviously “mysite” will be whatever you called it).
  • Still something not working? Maybe you have problems with permissions, so check you didn’t miss one of the first steps where we did chmod 710 /home/yourusername, or maybe in the last guide, you didn’t add your user to the nginx` group.

Wrapping things up

I’ll try to write a super short list of “key points” of this guide, to wrap things up.

  1. Create a Python Virtualenv that contains all our python dependencies.
  2. Initialize a new Django project.
  3. Configure and enable a Gunicorn Systemd service and socket, this is the Python Web Server.
  4. Create a basic NGINX config (with other needed configs) to serve our website as HTTP. This will forward requests to our Gunicorn web server.
  5. Generate a Let’s Encrypt Certificate for our website.
  6. Complete our NGINX config with the newly obtained SSL Certificate.

🚀

SD

Simone

More posts