Using Django with Nginx, PostgreSQL, and Gunicorn on Ubuntu 20.04
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
- Deploy a new Ubuntu 20.04 server at Vultr.
- Follow Vultr's best practices guides to create a sudo user and update the Ubuntu server.
- (Optional) Configure the Ubuntu firewall with ports 80, 443, and 22 open.
Make sure to replace django.example.com in these examples with your server's fully-qualified domain name or IP address.
1. Install PostgreSQL
Log in to the server as a non-root sudo user via SSH.
Install PostgreSQL 12 from the official Ubuntu 20.04 repositories.
$ sudo apt -y install postgresql postgresql-contrib
Switch to the
postgres
user, which PostgreSQL created during the installation.$ sudo su - postgres
Log in to PostgreSQL.
$ psql
Create a new role named
dbuser
for your Django project.postgres=# CREATE ROLE dbuser WITH LOGIN;
Set a strong password for the
dbuser
role.postgres=# \password dbuser
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';
Create a new
dbname
database.postgres=# CREATE DATABASE dbname;
Grant all privileges on the
dbname
database to thedbuser
role.postgres=# GRANT ALL PRIVILEGES ON DATABASE dbname TO dbuser;
Exit the PostgreSQL command-line:
postgres=# \q
Exit the
postgres
account and return to your sudo user for the remaining steps.$ exit
2. Install Django and Gunicorn
Install the Django dependencies.
$ sudo apt -y install build-essential python3-venv python3-dev libpq-dev
Create a dedicated user named
django
to manage your project's source code.$ sudo adduser django
Switch to this user each time you change your source code.
$ sudo su django
Change the working directory to the home directory.
$ cd ~
Create a directory named
project_root
to store the project source code.$ mkdir project_root
Create a virtual environment named
.venv
insideproject_root
to isolate Django and its dependencies.$ python3 -m venv project_root/.venv
Active the virtual environment.
$ source project_root/.venv/bin/activate
Install Django with pip, the package installer for Python.
$ pip install Django
Install
psycopg2
, a popular PostgreSQL adapter for Python, so that your Python code can connect to the database. You must install thewheel
package beforepsycopg2
.$ pip install wheel $ pip install psycopg2
Install Gunicorn to the virtual environment.
$ pip install gunicorn
3. Create and Configure the Project
Upload your project's source code to the
project_root
directory. Make sure that themanage.py
file is the direct child ofproject_root
. For illustration purposes, this guide creates a sample project namedexample
instead of uploading an existing one.$ django-admin startproject example project_root
The content of
project_root
should look like this.$ ls -a project_root . .. example manage.py .venv
Change the working directory to the
project_root
directory.$ cd project_root
Open the project settings file in your text editor.
$ nano example/settings.py
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', } }
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 theSTATIC_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/'
Find the following variables and change their values as shown for security.
DEBUG = False ALLOWED_HOSTS = ['django.example.com']
Save and close the settings file.
Django uses the value of the
SECRET_KEY
variable to provide cryptographic signing. Generate a unique value with theget_random_secret_key
function from Django'sutils
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
Copy the string to your clipboard and reopen the settings file.
$ nano example/settings.py
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'
Save and close the file.
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).
Create migrations based on your project models.
$ python manage.py makemigrations
Run the migrations and create tables in the database.
$ python manage.py migrate
Create the static directory you have configured above.
$ mkdir /home/django/project_root/static
Copy all the static files into the static directory. Type
yes
when prompted.$ python manage.py collectstatic
Create an administrative user for the project.
$ python manage.py createsuperuser
Enter your desired credentials for the administrative user.
Exit the virtual environment.
$ deactivate
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:
Create a new file named
gunicorn-example.service
in the/etc/systemd/system
directory.$ sudo nano /etc/systemd/system/gunicorn-example.service
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 address127.0.0.1
port8000
. You can customize those values as needed.- Gunicorn will execute the project source code under the
django
user. - The
example
string that appears in theexample.wsgi
andgunicorn_example
arguments is the name of your project.
- Gunicorn will execute the project source code under the
Save the service file and exit.
Reload the systemd daemon.
$ sudo systemctl daemon-reload
Enable the service so that it runs at boot.
$ sudo systemctl enable --now gunicorn-example.service
5. Install and Configure Nginx
Install Nginx.
$ sudo apt -y install nginx
Create a new configuration file for your project.
$ sudo nano /etc/nginx/sites-available/example-http.conf
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; } }
Save the configuration file and exit.
Enable the new configuration.
$ sudo ln -s /etc/nginx/sites-available/example-http.conf /etc/nginx/sites-enabled/example-http.conf
Add the
www-data
user to thedjango
group so that Nginx processes can access the project source code directory.$ sudo usermod -aG django www-data
Check the new configuration.
$ sudo nginx -t
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.
Follow our guide to install Certbot with Snap.
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
Create a new configuration file to serve HTTP requests.
$ sudo nano /etc/nginx/sites-available/example-http.conf
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.
Save the configuration file and exit.
Check the new configuration.
$ sudo nginx -t
Reload the Nginx service for the changes to take effect.
$ sudo systemctl reload nginx.service
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
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.
Update the HTTPS configuration file.
$ sudo nano /etc/nginx/sites-available/example-https.conf
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;
Save the configuration file and exit.
Enable the new configuration.
$ sudo ln -s /etc/nginx/sites-available/example-https.conf /etc/nginx/sites-enabled/example-https.conf
Check the new configuration.
$ sudo nginx -t
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.
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.Create the file in your text editor.
$ sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Paste the following content into your file.
#!/bin/bash /usr/bin/systemctl reload nginx.service
Save and exit the file.
Make the script executable.
$ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
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
Restart the server.
$ sudo reboot
Wait a moment for the system to boot, then open the http://django.example.com/admin link in your browser.
The Django administration screen appears with a login form.
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).