Using Django with Nginx, PostgreSQL, and Gunicorn on Ubuntu 20.04

Updated on September 10, 2021
Using Django with Nginx, PostgreSQL, and Gunicorn on Ubuntu 20.04 header image

Introduction

Django is a popular open-source Python web framework. This guide explains how to deploy a secure Django project with Nginx, PostgreSQL, and Gunicorn on Ubuntu 20.04 LTS with a free Let's Encrypt TLS certificate.

Prerequisites

Make sure to replace django.example.com in these examples with your server's fully-qualified domain name or IP address.

1. Install PostgreSQL

  1. Log in to the server as a non-root sudo user via SSH.

  2. Install PostgreSQL 12 from the official Ubuntu 20.04 repositories.

     $ sudo apt -y install postgresql postgresql-contrib
  3. Switch to the postgres user, which PostgreSQL created during the installation.

     $ sudo su - postgres
  4. Log in to PostgreSQL.

     $ psql
  5. Create a new role named dbuser for your Django project.

     postgres=# CREATE ROLE dbuser WITH LOGIN;
  6. Set a strong password for the dbuser role.

    postgres=# \password dbuser

  7. Optimize the database connection parameters for Django.

     postgres=# ALTER ROLE dbuser SET client_encoding TO 'utf8';
     postgres=# ALTER ROLE dbuser SET default_transaction_isolation TO 'read committed';
     postgres=# ALTER ROLE dbuser SET timezone TO 'UTC';
  8. Create a new dbname database.

     postgres=# CREATE DATABASE dbname;
  9. Grant all privileges on the dbname database to the dbuser role.

     postgres=# GRANT ALL PRIVILEGES ON DATABASE dbname TO dbuser;
  10. Exit the PostgreSQL command-line:

    postgres=# \q

  11. Exit the postgres account and return to your sudo user for the remaining steps.

     $ exit

2. Install Django and Gunicorn

  1. Install the Django dependencies.

     $ sudo apt -y install build-essential python3-venv python3-dev libpq-dev
  2. Create a dedicated user named django to manage your project's source code.

     $ sudo adduser django
  3. Switch to this user each time you change your source code.

     $ sudo su django
  4. Change the working directory to the home directory.

     $ cd ~
  5. Create a directory named project_root to store the project source code.

     $ mkdir project_root
  6. Create a virtual environment named .venv inside project_root to isolate Django and its dependencies.

     $ python3 -m venv project_root/.venv
  7. Active the virtual environment.

     $ source project_root/.venv/bin/activate
  8. Install Django with pip, the package installer for Python.

     $ pip install Django
  9. Install psycopg2, a popular PostgreSQL adapter for Python, so that your Python code can connect to the database. You must install the wheel package before psycopg2.

     $ pip install wheel 
     $ pip install psycopg2
  10. Install Gunicorn to the virtual environment.

     $ pip install gunicorn

3. Create and Configure the Project

  1. Upload your project's source code to the project_root directory. Make sure that the manage.py file is the direct child of project_root. For illustration purposes, this guide creates a sample project named example instead of uploading an existing one.

     $ django-admin startproject example project_root
  2. The content of project_root should look like this.

     $ ls -a project_root
     .  ..  example  manage.py  .venv
  3. Change the working directory to the project_root directory.

     $ cd project_root
  4. Open the project settings file in your text editor.

     $ nano example/settings.py
  5. The settings file is a Python module with module-level variables. Find the DATABASES variable and change its value with the credentials created in Section 1. Make sure it looks like this:

     DATABASES = {
         'default': {
             'ENGINE': 'django.db.backends.postgresql',
             'NAME': 'dbname',
             'USER': 'dbuser',
             'PASSWORD': 'dbpassword',
             'HOST': '127.0.0.1',
             'PORT': '5432',
         }
     }
  6. To efficiently serve static files in a production environment, find the INSTALLED_APPS list and make sure 'django.contrib.staticfiles' is one of its items. Then find the STATIC_URL = '/static/' line and add /home/django/project_root/static below it. This is the directory that stores you project's static files.

     STATIC_ROOT = '/home/django/project_root/static/'
  7. Find the following variables and change their values as shown for security.

     DEBUG = False
    
     ALLOWED_HOSTS = ['django.example.com']
  8. Save and close the settings file.

  9. Django uses the value of the SECRET_KEY variable to provide cryptographic signing. Generate a unique value with the get_random_secret_key function from Django's utils module.

     $ python manage.py shell -c 'from django.core.management import utils; print(utils.get_random_secret_key())'

    The result should be random characters, similar to this:

     e-m^lc!--w3$-9qv^54*=qpe=4gko(x_-h@h@s!2k81@l4hxjh
  10. Copy the string to your clipboard and reopen the settings file.

     $ nano example/settings.py
  11. Find the SECRET_KEY variable and paste your random string. Be sure to surround the value with single quotes as shown.

     SECRET_KEY = 'e-m^lc!--w3$-9qv^54*=qpe=4gko(x_-h@h@s!2k81@l4hxjh'
  12. Save and close the file.

  13. Check the database settings.

     $ python manage.py check --database default

    If the settings are correct, the result should look like this:

     System check identified no issues (0 silenced).
  14. Create migrations based on your project models.

     $ python manage.py makemigrations
  15. Run the migrations and create tables in the database.

     $ python manage.py migrate
  16. Create the static directory you have configured above.

     $ mkdir /home/django/project_root/static
  17. Copy all the static files into the static directory. Type yes when prompted.

     $ python manage.py collectstatic
  18. Create an administrative user for the project.

     $ python manage.py createsuperuser

    Enter your desired credentials for the administrative user.

  19. Exit the virtual environment.

     $ deactivate
  20. Switch back to the sudo user to continue with the next step.

     $ exit

4. Configure Gunicorn

A systemd service starts Gunicorn as a background service when the operating system starts. To create this service:

  1. Create a new file named gunicorn-example.service in the /etc/systemd/system directory.

     $ sudo nano /etc/systemd/system/gunicorn-example.service
  2. Paste the following into the file.

     [Unit]
     Description=Gunicorn for the Django example project
     After=network.target
    
     [Service]
     Type=notify
    
     # the specific user that our service will run as
     User=django
     Group=django
    
     RuntimeDirectory=gunicorn_example
     WorkingDirectory=/home/django/project_root
     ExecStart=/home/django/project_root/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 example.wsgi
     ExecReload=/bin/kill -s HUP $MAINPID
     KillMode=mixed
     TimeoutStopSec=5
     PrivateTmp=true
    
     [Install]
     WantedBy=multi-user.target

    * This service will run Gunicorn with 3 workers and listen on IP address 127.0.0.1 port 8000. You can customize those values as needed.

    • Gunicorn will execute the project source code under the django user.
    • The example string that appears in the example.wsgi and gunicorn_example arguments is the name of your project.
  3. Save the service file and exit.

  4. Reload the systemd daemon.

     $ sudo systemctl daemon-reload
  5. Enable the service so that it runs at boot.

     $ sudo systemctl enable --now gunicorn-example.service

5. Install and Configure Nginx

  1. Install Nginx.

     $ sudo apt -y install nginx
  2. Create a new configuration file for your project.

     $ sudo nano /etc/nginx/sites-available/example-http.conf
  3. Paste the following into the file.

     server {
     listen 80;
     listen [::]:80;
    
     server_name django.example.com;
    
     # Process static file requests
     location /static/ {
         root /home/django/project_root;
    
         # Set expiration of assets to MAX for caching
         expires max;
     }
    
     # Deny accesses to the virtual environment directory
     location /.venv {
         return 444;
     }
    
     # Pass regular requests to Gunicorn
     location / {
         # set the correct HTTP headers for Gunicorn
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header Host $http_host;
    
         # we don't want nginx trying to do something clever with
         # redirects, we set the Host: header above already.
         proxy_redirect off;
    
         # turn off the proxy buffering to handle streaming request/responses
         # or other fancy features like Comet, Long polling, or Web sockets.
         proxy_buffering off;
    
         proxy_pass http://127.0.0.1:8000;
     }
     }
  4. Save the configuration file and exit.

  5. Enable the new configuration.

     $ sudo ln -s /etc/nginx/sites-available/example-http.conf /etc/nginx/sites-enabled/example-http.conf
  6. Add the www-data user to the django group so that Nginx processes can access the project source code directory.

     $ sudo usermod -aG django www-data
  7. Check the new configuration.

     $ sudo nginx -t
  8. Reload the Nginx service for the changes to take effect.

     $ sudo systemctl reload nginx.service

6. (Optional) Configure HTTPS with a Let's Encrypt Certificate

If you own a valid domain name, you can set up HTTPS for your Django project at no cost. You can get a free TLS certificate from Let's Encrypt with their Certbot program.

  1. Follow our guide to install Certbot with Snap.

  2. Rename the HTTP configuration file to make it the template for the HTTPS configuration file.

     $ sudo mv /etc/nginx/sites-available/example-http.conf /etc/nginx/sites-available/example-https.conf
  3. Create a new configuration file to serve HTTP requests.

     $ sudo nano /etc/nginx/sites-available/example-http.conf
  4. Paste the following into your file.

     server {
     listen 80;
     listen [::]:80;
    
     server_name django.example.com;
    
     root /home/django/project_root;
    
     location / {
         return 301 https://$server_name$request_uri;
     }
    
     location /.well-known/acme-challenge/ {}
     }

    This configuration makes Nginx redirect all HTTP requests, except those from Let's Encrypt, to corresponding HTTPS requests.

  5. Save the configuration file and exit.

  6. Check the new configuration.

     $ sudo nginx -t
  7. Reload the Nginx service for the changes to take effect.

     $ sudo systemctl reload nginx.service
  8. Get the Let's Encrypt certificate.

     $ sudo certbot certonly --webroot -w /home/django/project_root -d django.example.com -m admin@example.com --agree-tos

    You may need to answer a question about sharing your email with the Electronic Frontier Foundation. When finished, Certbot places all the files related to the certificate in the /etc/letsencrypt/archive/django.example.com directory and creates corresponding symlinks in the /etc/letsencrypt/live/django.example.com directory for your convenience. Those symlinks are:

     $ sudo ls /etc/letsencrypt/live/django.example.com
     cert.pem  chain.pem  fullchain.pem  privkey.pem  README

    You will use those symlinks in the next step to install the certificate.

Install the Certificate with Nginx

  1. Generate a file with DH parameters for DHE ciphers. This process may take a while.

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

    2048 is the recommended size of DH parameters.

  2. Update the HTTPS configuration file.

     $ sudo nano /etc/nginx/sites-available/example-https.conf
  3. Find the following lines.

     listen 80;
     listen [::]:80;

    Replace them with the following lines.

     listen 443 ssl http2;
     listen [::]:443 ssl http2;
    
     ssl_certificate /etc/letsencrypt/live/django.example.com/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/django.example.com/privkey.pem;
    
     ssl_session_timeout 1d;
     ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    
     # DH parameters file
     ssl_dhparam /etc/nginx/dhparam.pem;
    
     # intermediate configuration
     ssl_protocols TLSv1.2;
     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;
     ssl_prefer_server_ciphers off;
    
     # HSTS (ngx_http_headers_module is required) (63072000 seconds)
     #
     # Uncomment the following line only if your website fully supports HTTPS
     # and you have no intention of going back to HTTP, otherwise, it will
     # break your site.
     #
     # add_header Strict-Transport-Security "max-age=63072000" always;
    
     # OCSP stapling
     ssl_stapling on;
     ssl_stapling_verify on;
    
     # verify chain of trust of OCSP response using Root CA and Intermediate certs
     ssl_trusted_certificate /etc/letsencrypt/live/django.example.com/chain.pem;
    
     # Use Cloudflare DNS resolver
     resolver 1.1.1.1;
  4. Save the configuration file and exit.

  5. Enable the new configuration.

     $ sudo ln -s /etc/nginx/sites-available/example-https.conf /etc/nginx/sites-enabled/example-https.conf
  6. Check the new configuration.

     $ sudo nginx -t
  7. Reload the Nginx service for the changes to take effect.

     $ sudo systemctl reload nginx.service

Automate Renewal

Let's Encrypt certificates are valid for 90 days, so you must renew your TLS certificate at least once every three months. The Certbot installation automatically created a systemd timer unit to automate this task.

  1. Verify the timer is active.

     $ sudo systemctl list-timers | grep 'certbot\|ACTIVATES'

    After renewing the certificate, Certbot will not automatically reload Nginx, so Nginx still uses the old certificate. Instead, you must write a script inside the /etc/letsencrypt/renewal-hooks/deploy directory to reload Nginx.

  2. Create the file in your text editor.

     $ sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
  3. Paste the following content into your file.

     #!/bin/bash
    
     /usr/bin/systemctl reload nginx.service
  4. Save and exit the file.

  5. Make the script executable.

     $ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
  6. Test the renewal process with a dry run.

     $ sudo certbot renew --dry-run

This Vultr article explains all the above steps in more detail. This kind of TLS setup gives you an A on the SSL Labs test.

7. Verify the Setup

  1. Restart the server.

     $ sudo reboot
  2. Wait a moment for the system to boot, then open the http://django.example.com/admin link in your browser.

  3. The Django administration screen appears with a login form.

  4. Use the username and password of the administrative user created in step 3 to log in.

You now have a working Django site up on your Ubuntu 20.04 server.

If you follow this tutorial by creating a sample project instead of uploading an existing one, you will get a Not Found error message when you visit the homepage http://django.example.com/. That is completely normal. Because, by default, Django will not automatically generate the homepage content for the sample project in a production environment (with the DEBUG = False setting).

References