
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.serviceorsudo 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.
- Create a Python Virtualenv that contains all our python dependencies.
- Initialize a new Django project.
- Configure and enable a Gunicorn Systemd service and socket, this is the Python Web Server.
- Create a basic NGINX config (with other needed configs) to serve our website as HTTP. This will forward requests to our Gunicorn web server.
- Generate a Letâs Encrypt Certificate for our website.
- Complete our NGINX config with the newly obtained SSL Certificate.
đ
Simone