Django server setup

Instructions for Fedora Server 34



As simple as that

# dnf install nginx


Considering 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 {
    root         /usr/share/nginx/html;
    listen      80;
# dnf install python-certbot-nginx

# certbot --nginx -d

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


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 {
    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/; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/; # 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 = {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen       80;
    listen       [::]:80;
    return 404; # managed by Certbot


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


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


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 makemigrations
$ python migrate

Deploy checklist

For a production environment, make sure to apply these changes to your 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 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 import get_random_secret_key

Warning You could theoretically just place this in your 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.

import os
from 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 = ['', '']

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 collectstatic


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 and files are placed. Launch it to test it.

gunicorn -w 4 --bind django_project_folder.wsgi


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:

Description=Django using Gunicorn

ExecStart=/bin/bash -c 'source /home/django/venv/bin/activate; gunicorn -w 4 --bind django_project_folder.wsgi'


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


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


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