In this article you will see how to setup Nginx webserver for your Django application with detailed and actionable instructions.

Table of contents

Objectives

Given a fresh Ubuntu 20.04 LTS we will:

  • setup nginx server
  • Python environment
  • create a Django project
  • and run it with gunicorn
  • create a certificate with Let's encrypt
  • setup nginx server as a reverse proxy

Requirements

  • A fresh Ubuntu 20.04 LTS server
  • Terminal app & SSH client
  • SSH key(optional, we will create one)
  • A domain name(or a subdomain)

Setup Ubuntu with Nginx and Python 3.10

Log into your fresh Ubuntu box and let's update the packages and then install Python and Nginx.

ssh root@server-ip

Or if your cloud provider gave you a different username, e.g. AWS EC2 - it will be ubuntu user:

ssh ubuntu@server-ip

Update APT package sources:

sudo apt-get update

Upgrade all existing installed Ubuntu APT packages:

sudo apt-get upgrade -y

Install new packages:

sudo apt-get install -y nginx curl apt-transport-https ca-certificates software-properties-common haveged letsencrypt snapd

Install Python 3.10 on Ubuntu 20.04

Add the deadsnakes PPA to the APT package manager sources list as below:

sudo add-apt-repository ppa:deadsnakes/ppa -y

Install python3.10 and python3-pip:

sudo apt install -y python3.10 python3-pip python3.10-venv

Install Certbot on Ubuntu 20.04

sudo add-apt-repository -y universe
sudo snap install --classic certbot

Configure Let's Encrypt and Nginx

mkdir -p /var/www/letsencrypt/
chown www-data.www-data /var/www/letsencrypt/
if [ ! -f /etc/nginx/dhparams.pem ]
    then openssl dhparam -dsaparam -out /etc/nginx/dhparams.pem 4096
fi

echo "
location /.well-known/acme-challenge {
  root /var/www/letsencrypt/;
  }

" > /etc/nginx/acme.conf

echo "
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    include /etc/nginx/acme.conf;
    location / {
        return 301 https://\$host\$request_uri;
    }
}
" > /etc/nginx/sites-enabled/default

echo "log_format upstreamlog '[\$time_local] \$remote_addr - \$remote_user - \$server_name to: \$upstream_addr: \$request status - \$status - upstream_response_time \$upstream_response_time msec \$msec request_time \$request_time \$http_user_agent';" > /etc/nginx/conf.d/log.conf

service nginx restart

These commands are used for configuring an nginx server to use Let's Encrypt SSL/TLS certificates with the help of Certbot.

Here is a breakdown of what we have just done:

  1. mkdir -p /var/www/letsencrypt/: This command creates a directory /var/www/letsencrypt/ which will be used for hosting Let's Encrypt's well-known challenge.
  2. chown www-data.www-data /var/www/letsencrypt/: This command changes the ownership of the directory /var/www/letsencrypt/ to the www-data user and group, which is used by the nginx web server.
  3. if [ ! -f /etc/nginx/dhparams.pem ]: This is a conditional statement that checks if the file /etc/nginx/dhparams.pem exists. If it does not exist, the following command will create it.
  4. openssl dhparam -dsaparam -out /etc/nginx/dhparams.pem 4096: This command generates a Diffie-Hellman parameter file for secure key exchange between the server and clients. The resulting file is saved in /etc/nginx/dhparams.pem.
  5. echo "location /.well-known/acme-challenge { ... }" > /etc/nginx/acme.conf: This command creates a new file /etc/nginx/acme.conf and adds a location block for serving Let's Encrypt's well-known challenge.
  6. echo "server { ... }" > /etc/nginx/sites-enabled/default: This command creates a new file /etc/nginx/sites-enabled/default and adds a server block for serving HTTP traffic and redirecting it to HTTPS.
  7. echo "log_format upstreamlog '[\$time_local] \$remote_addr - \$remote_user - \$server_name to: \$upstream_addr: \$request status - \$status - upstream_response_time \$upstream_response_time msec \$msec request_time \$request_time \$http_user_agent';" > /etc/nginx/conf.d/log.conf: This command creates a new file /etc/nginx/conf.d/log.conf and defines a custom log format for the nginx web server.
  8. service nginx restart: This command restarts the nginx web server to apply the changes made to the configuration files.

Create an Ubuntu user app for running Django Project

Now since we don't want our app to work from the administrative user, let's create a separate user.

useradd -m -s /bin/bash app
mkdir /home/app/.ssh
cp ~/.ssh/authorized_keys /home/app/.ssh/authorized_keys
chown -R app:app /home/app/.ssh/
chmod 700 /home/app/.ssh
chmod 600 /home/app/.ssh/authorized_keys

These commands are used to create a new user app , set up SSH access for this user, and restrict access to the user's home directory.

Here is what these commands mean: 1. sudo useradd -m -s /bin/bash app: This command creates a new user app with a home directory /home/app and a login shell /bin/bash. 2. sudo mkdir /home/app/.ssh: This command creates a directory /home/app/.ssh for storing SSH keys. 3. sudo cp ~/.ssh/authorized_keys /home/app/.ssh/authorized_keys: This command copies the contents of the current user's authorized_keys file to the authorized_keys file in the app user's home directory. 4. sudo chown -R app:app /home/app/.ssh/: This command changes the ownership of the /home/app/.ssh directory and its contents to the app user and group. 5. sudo chmod 700 /home/app/.ssh: This command sets the permission of the /home/app/.ssh directory to 700, which means only the owner of the directory (i.e., the app user) can access it. 6. sudo chmod 600 /home/app/.ssh/authorized_keys: This command sets the permission of the authorized_keys file to 600, which means only the owner of the file (i.e., the app user) can read and write to it.

We will run our Django app from this app user.

Create a simple Django project with gunicorn to work behind Nginx

Let's make a simple Django project that will live behind reverse proxy.

Open another terminal and ssh into the server as the app user

ssh app@server-ip

Let's create a Django project and run it with gunicorn.

  1. Create a virtual environment:
python3 -m venv myenv

This will create a new virtual environment in the "myenv" directory.

  1. Activate the virtual environment:
source myenv/bin/activate

This will activate the virtual environment so that any subsequent Python commands will use the virtual environment's Python interpreter.

  1. Install Django:
pip install django

This will install the latest version of Django in the virtual environment.

  1. Create a new Django project:
django-admin startproject myproject

This will create a new Django project in a directory called "myproject".

  1. Install Gunicorn:
pip install gunicorn

This will install Gunicorn in the virtual environment.

  1. Change into the project directory:
cd myproject
  1. Run Gunicorn:
gunicorn myproject.wsgi  --log-file -

This will start Gunicorn and serve your Django application on the default port (8000).

Here are all commands at once so you can copy them:

python3.10 -m venv myenv
source myenv/bin/activate
pip install django
django-admin startproject myproject
pip install gunicorn
cd myproject
gunicorn myproject.wsgi   --log-file -

Create myenv and run Django with gunicorn

That's it! You now have a Django application running with Gunicorn in a Python3.10 virtual environment.

Issue SSL certificate with Certbot

Switch to the terminal with the root user.

Make sure that your domain or subdomain has an A-record that points to your server IP.

Let's say the domain is example.com

certbot certonly --cert-name example.com --non-interactive --expand --email your.email@gmail.com --webroot -w /var/www/letsencrypt -d example.com --agree-tos

It should say that certificate is created. If it wasn't created, there is a chance that you have just updated DNS records and it needs time to propogate. Check DNS settings and try again in a few minutes.

Create Nginx configuration for Django project

Now let's create an nginx configuration for our Django app that will use our new SSL certificate.

Create a file /etc/nginx/sites-enabled/ssl.example.com.conf with this content:

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
    ssl_dhparam /etc/nginx/dhparams.pem;
    ssl_prefer_server_ciphers on;
    ssl_stapling on;
    access_log /var/log/nginx/ssl.example.com.access.log upstreamlog;
    error_log /var/log/nginx/ssl.example.com.errors.log warn;
    client_max_body_size 100m;

    include /etc/nginx/acme.conf;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_read_timeout 600;
        proxy_send_timeout 600;
        proxy_set_header Host \$http_host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_set_header X-Forwarded-Host \$host;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection upgrade;
        proxy_set_header X-Request-Id \$pid-\$msec-\$remote_addr-\$request_length;
    }
}

This configuration block is for an HTTPS server using Nginx as the web server. It listens on port 443 and serves the example.com domain.

Here's a brief explanation of each directive:

  • listen 443 ssl;: Tells Nginx to listen on port 443 for HTTPS connections.

  • server_name example.com;: Specifies the domain name that the server will handle.

  • ssl_certificate and ssl_certificate_key: Specifies the paths to the SSL certificate and private key files.

  • ssl_protocols: Specifies the SSL/TLS protocols that are allowed. In this case, it allows TLSv1, TLSv1.1, and TLSv1.2.

  • ssl_ciphers: Specifies the cipher suites that are allowed for SSL/TLS connections. It is a list of ciphers that are prioritized by the server.

  • ssl_dhparam: Specifies the path to the Diffie-Hellman parameter file.

  • ssl_prefer_server_ciphers on;: Instructs Nginx to use the server's preferred ciphers rather than the client's.

  • ssl_stapling on;: Enables SSL stapling, which helps improve the security and performance of SSL connections.

  • access_log and error_log: Specifies the paths to the access and error log files.

  • client_max_body_size: Limits the maximum allowed size of the client request body.

  • include /etc/nginx/acme.conf;: Includes additional configuration directives from the acme.conf file.

  • location /: Specifies the URI location that will be handled by this configuration block.

  • proxy_pass: Specifies the address of the backend server to which requests will be proxied.

  • proxy_read_timeout and proxy_send_timeout: Specifies the maximum time allowed for reading and sending data to the backend server.

  • proxy_set_header: Sets the headers that will be sent to the backend server, such as the Host and X-Real-IP headers. It also sets additional headers to pass information about the connection, such as X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-For.

  • proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection upgrade;: Sets the headers necessary for WebSocket connections.

  • proxy_set_header X-Request-Id $pid-$msec-$remote_addr-$request_length;: Sets a custom header with a unique request ID for debugging purposes.

Run service nginx reload to apply changes to nginx configuration.

Conclusion

Now try opening your site at https://example.com.

Congratulations! You have successfully setup Ubuntu with Django and Nginx Reverse proxy and your site has SSL certificate!