Django server setup

Instructions for Fedora Server 34

Webserver

nginx

As simple as that

# dnf install nginx

certbot

Considering www.example.com as our domain

First I added a simple server block with your correct domain and port 80 only. I don't know if it's really needed, I have to check

server {
    server_name  example.com;
    root         /usr/share/nginx/html;
    listen      80;
}
# dnf install python-certbot-nginx

# certbot --nginx -d www.example.com

Certbot takes that server block and redirects it as https only. It redirects 80 to 443 automatically

proxy_pass

After running certbot, you'll have a http block 80 redirected to 443 to secure your traffic.

We need to add this part to the secure server block. This tries to retrieve a static file. If it fails, it retrieves an answer from the Django server. We're also passing useful request headers to the django server.

location / {
    try_files $uri @django_proxy;
}

location @django_proxy {
    proxy_pass http://localhost:9000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Rate limiting

We don't want people to flood our server. This is especially true for dynamic routes. While static files are easy to serve, are usually cached or can be even outsourced to a CDN, this is not true for dynamic routes.

Each call to a dynamic route could make a SQL query, template generation wasting our energies. Maybe someone is trying to DDOS us or bruteforce some password. If the login page is not rate limited and the used password is very simple, this becomes a concrete risk.

Let's this line at the top of your nginx django.conf file. This creates a rule to limit each user to 5 requests per second. We'll use it later: limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;

Then, inside the proxy pass block, use the just defined my_limit rule.

location @django_proxy {
    limit_req zone=mylimit burst=20;# <- added this line
    proxy_pass http://localhost:9000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

You can spot I've added a burst queue of size 20. This allows the user to make burst requests (happens a lot when loading a page and having to retrieve many data by javascript), while avoiding flooding in the long run.

More information on rate limiting here.

This is my final /etc/nginx/conf.d/django.conf file. (Check if the conf.d folder is correct or there's a better one)

server {
    server_name example.com;
    root         /usr/share/nginx/html;    

    location / {
        try_files $uri @django_proxy;
    }

    location @django_proxy {
    limit_req zone=mylimit burst=20;# <- added this line
    proxy_pass http://localhost:9000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    error_page 404 /404.html;
    
    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}
server {
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen       80;
    listen       [::]:80;
    server_name example.com;
    return 404; # managed by Certbot
}

Django

The django software is run by an internal server and using a lower privileged user. This exchange data with nginx.

Low privilege user creation

Create a django user by typing

# useradd -m django

Set its password if you want to login there, useful for acting in its home directory files

# passwd django

If you're root, you can also login to this user with

# su -- django

Python stuff

You need pip to proceed: # dnf install python3-pip

Virtualenv

To not put shit into the global python installation, it's recommended to place a self-contained python interpeter in a folder. Local dependencies will be placed there too.

Make sure virtualenv creator is installed in your system. You can do it in two ways: # dnf install python3-virtualenv # pip install virtualenv

In Debian, it is $ sudo apt install python3-venv

Login as django user, go in its home folder.

Create the virtualenv in a folder called venv or how you like:

$ python3 -m venv venv

Activate it with:

$ source /venv/bin/activate

From this point, all python commands will execute the virtualenv's interpreter and modules instead of the systemwide one. If you run pip install while in the virtualenv, dependencies will be placed inside it.

Deactivate with:

$ deactivate

Django project config

Copy-paste how you want your django project folder next to the virtualenv one, for example placing it in the home folder too. I'll use the simple sqlite driver for the database.

Make sure to have a requirements.txt file, listing all needed dependencies. If you've a local working copy of the your django project in your pc, you can generate this file with:

$ pip freeze > requirements.txt

Here's my requirements.txt file as an example. Don't use this, you may have different packages and versions

asgiref==3.4.1
Django==3.2.8
django-ranged-response==0.2.0
django-simple-captcha==0.5.14
gunicorn==20.1.0
Pillow==8.4.0
pytz==2021.3
six==1.16.0
sqlparse==0.4.2

After having copied the project, activate the virtualenv, cd into the project folder and then run $ pip install -r requirements.txt. All dependencies will be installed automatically and resolved.

Now create all the database file and tables automatically with:

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

Deploy checklist

For a production environment, make sure to apply these changes to your settings.py file:

Secret signing key

Set a random SECRET_KEY used for signing sessions and cookies. Do NOT use the debug one. You can get it from an environment variable. Do NOT versions control it. Edit your project's settings.py file and add

import os
SECRET_KEY = os.environ['SECRET_KEY']

You can generate a secret key with this command. You'll need it in the next steps while configuring the Systemd service. Save it somewhere else securely:

from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())

Warning You could theoretically just place this in your settings.py file. The problem of this approach is that it generates a new secret key each time the server starts Django. This means it invalidates every existing cookie and session at each server restart.

## WARNING; MIGHT NOT ME WHAT YOU WANT
import os
from django.core.management.utils import get_random_secret_key
SECRET_KEY = get_random_secret_key()

Debug mode

Disable debug mode to reduce the amount of information returned in responses in case of errors. If Django is in debug mode, it lists the code that crashed and some values helping us troubleshooting our code. If it's running in production mode, it just returns a generic "server error" with an HTTP status code. DEBUG = False

Allowed hosts

Add your domain name here. This protects you from some CSRF attacks:

ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

In Debug mode, this list is ignored. In production mode, the list is enforced.

Database password

If you're using a non-sqlite database, make sure to also get the db password from an environment variable like you did with the SECRET_KEY.

Static files

Set STATIC_ROOT and STATIC_URL to tell django where to store static files that will be served by nginx/apache.

Static files can be collected by running

$ python manage.py collectstatic

Gunicorn

Gunicorn is a WSGI server written in Python. Make sure to install it into the virtualenv.

After entering the virtualenv, we can install it using: pip install gunicorn Remember to save it into the requirements file using pip freeze.

djangoprojectfolder is the internal folder where the settings.py and wsgi.py files are placed. Launch it to test it.

gunicorn -w 4 --bind 0.0.0.0:9000 django_project_folder.wsgi

systemd

We need to automated all this stuff so that:

We can do all of that with a systemd service. This is my configuration as an example. I placed it at /etc/systemd/system/djangogunicorn.service

Here's the sample file, where the "source /home/django/venv/bin/activate;" part activates the virtualenv:

[Unit]
Description=Django using Gunicorn
After=network.target

[Service]
User=django
Group=django
WorkingDirectory=/home/django/django_project_folder
Environment="SECRET_KEY=put_here_your_secret_key_as_environment_variable"
ExecStart=/bin/bash -c 'source /home/django/venv/bin/activate; gunicorn -w 4 --bind 0.0.0.0:9000 django_project_folder.wsgi'
Restart=always

[Install]
WantedBy=multi-user.target

After saving the file, run and see if it runs correctly. # systemctl start djangogunicorn See its status with # systemctl status djangogunicorn

If everything is fine, enable the service at boot by typing: systemctl enable djangogunicorn

Whenever you modify the .service file, run:

# systemctl daemon-reload
# systemctl restart djangogunicorn

SELinux

Just setting gunicorn to listen on port 9000 seems to overcome any SeLinux problems. SeLinux already allows communication on this port for network purposes.

Firewall

In my case, I easily modified the firewall through the cockpit interface (find it at http://server_address:9090/). You can also modify it using a command line.

I have:

UFW, Uncomplicated FireWall

On Debians, you can use ufw

$ sudo apt install ufw
$ sudo ufw allow 22/tcp
$ sudo ufw allow 80/tcp
$ sudo ufw allow 443/tcp

$ sudo ufw enable