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 Rcs.
- Follow Rcs'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-contribSwitch to the
postgresuser, which PostgreSQL created during the installation.$ sudo su - postgresLog in to PostgreSQL.
$ psqlCreate a new role named
dbuserfor your Django project.postgres=# CREATE ROLE dbuser WITH LOGIN;Set a strong password for the
dbuserrole.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
dbnamedatabase.postgres=# CREATE DATABASE dbname;Grant all privileges on the
dbnamedatabase to thedbuserrole.postgres=# GRANT ALL PRIVILEGES ON DATABASE dbname TO dbuser;Exit the PostgreSQL command-line:
postgres=# \q
Exit the
postgresaccount 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-devCreate a dedicated user named
djangoto manage your project's source code.$ sudo adduser djangoSwitch to this user each time you change your source code.
$ sudo su djangoChange the working directory to the home directory.
$ cd ~Create a directory named
project_rootto store the project source code.$ mkdir project_rootCreate a virtual environment named
.venvinsideproject_rootto isolate Django and its dependencies.$ python3 -m venv project_root/.venvActive the virtual environment.
$ source project_root/.venv/bin/activateInstall Django with pip, the package installer for Python.
$ pip install DjangoInstall
psycopg2, a popular PostgreSQL adapter for Python, so that your Python code can connect to the database. You must install thewheelpackage beforepsycopg2.$ pip install wheel $ pip install psycopg2Install Gunicorn to the virtual environment.
$ pip install gunicorn
3. Create and Configure the Project
Upload your project's source code to the
project_rootdirectory. Make sure that themanage.pyfile is the direct child ofproject_root. For illustration purposes, this guide creates a sample project namedexampleinstead of uploading an existing one.$ django-admin startproject example project_rootThe content of
project_rootshould look like this.$ ls -a project_root . .. example manage.py .venvChange the working directory to the
project_rootdirectory.$ cd project_rootOpen the project settings file in your text editor.
$ nano example/settings.pyThe settings file is a Python module with module-level variables. Find the
DATABASESvariable 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_APPSlist and make sure'django.contrib.staticfiles'is one of its items. Then find theSTATIC_URL = '/static/'line and add/home/django/project_root/staticbelow 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_KEYvariable to provide cryptographic signing. Generate a unique value with theget_random_secret_keyfunction from Django'sutilsmodule.$ 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@l4hxjhCopy the string to your clipboard and reopen the settings file.
$ nano example/settings.pyFind the
SECRET_KEYvariable 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 defaultIf 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 makemigrationsRun the migrations and create tables in the database.
$ python manage.py migrateCreate the static directory you have configured above.
$ mkdir /home/django/project_root/staticCopy all the static files into the static directory. Type
yeswhen prompted.$ python manage.py collectstaticCreate an administrative user for the project.
$ python manage.py createsuperuserEnter your desired credentials for the administrative user.
Exit the virtual environment.
$ deactivateSwitch 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.servicein the/etc/systemd/systemdirectory.$ sudo nano /etc/systemd/system/gunicorn-example.servicePaste 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
3workers and listen on IP address127.0.0.1port8000. You can customize those values as needed.- Gunicorn will execute the project source code under the
djangouser. - The
examplestring that appears in theexample.wsgiandgunicorn_examplearguments 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-reloadEnable 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 nginxCreate a new configuration file for your project.
$ sudo nano /etc/nginx/sites-available/example-http.confPaste 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.confAdd the
www-datauser to thedjangogroup so that Nginx processes can access the project source code directory.$ sudo usermod -aG django www-dataCheck the new configuration.
$ sudo nginx -tReload 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.confCreate a new configuration file to serve HTTP requests.
$ sudo nano /etc/nginx/sites-available/example-http.confPaste 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 -tReload the Nginx service for the changes to take effect.
$ sudo systemctl reload nginx.serviceGet the Let's Encrypt certificate.
$ sudo certbot certonly --webroot -w /home/django/project_root -d django.example.com -m admin@example.com --agree-tosYou 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.comdirectory and creates corresponding symlinks in the/etc/letsencrypt/live/django.example.comdirectory for your convenience. Those symlinks are:$ sudo ls /etc/letsencrypt/live/django.example.com cert.pem chain.pem fullchain.pem privkey.pem READMEYou 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 20482048 is the recommended size of DH parameters.
Update the HTTPS configuration file.
$ sudo nano /etc/nginx/sites-available/example-https.confFind 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.confCheck the new configuration.
$ sudo nginx -tReload 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/deploydirectory to reload Nginx.Create the file in your text editor.
$ sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shPaste the following content into your file.
#!/bin/bash /usr/bin/systemctl reload nginx.serviceSave and exit the file.
Make the script executable.
$ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shTest the renewal process with a dry run.
$ sudo certbot renew --dry-run
This Rcs 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 rebootWait 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 = Falsesetting).