Deploy Django website in Ubuntu server using Nginx, Gunicorn with Postgres DB and apply SSL

Introduction

Django is undoubtedly one of the most popular and powerful framework / MVC today. This is a high-level Python web framework. Many popular and heavy websites like Instagram, Pinterest use django framework.

As a developer, I struggled initially on how to deploy a website in a Ubuntu server. After going through many tutorials, videos and how-to guides over the internet I compiled following steps to deploy a django website / application on server.

Video demo on how to deploy django application

Prerequisites

You need to have following

  • Server with minimum 1vCPU, 1GB RAM and 20GB SSD
  • Ubuntu 20.04 OS
  • SSH access
  • PUTTY ready to access the console / terminal of your server
  • You should have sudo permissions

Legend

  1. example.com – You need to replace this with the domain name you use in your setup
  2. sleekproject – This is project directory, you can name this according to your project requirements
  3. sleekenv – This is virtual environment, you can name it as whatever you want to.
  4. sleeksoft – This is our django project, you can name this based on your project title etc..

Section 1 : Installing required packages

Before we dive into installing required packages, we need to update and upgrade the server and existing packages to the latest version.

sudo apt update -y
sudo apt upgrade -y

Install all the required packages in one command.

sudo apt install python3-pip python3-dev libpq-dev postgresql postgresql-contrib nginx curl file

List of packages installed are

  • Pip (Python package installer)
  • Python development files
  • PostgreSQL and Libraries
  • Nginx web server
  • CURL

Section 2 : Creating Postgres user and database

Run following command to enter into Postgres environment. By default when Postgres is installed, we will get an user called ‘postgres’ with administrative rights in PostgreSQL. We use this user to create Database and User account in postgres for our project.

sudo -u postgres psql

Create a database for our project

CREATE DATABASE sleeksoft;

Note: Every Postgres statement MUST end with a semi-colon

Create a database user for our project. Make sure to select a secure password.

CREATE USER sleekuser WITH PASSWORD 's133ks0ft';

Note: Here database user is ‘sleekuser’ and password is ‘s133ks0ft’

Set the default encoding to UTF-8, which is recommended by django

ALTER ROLE sleekuser SET client_encoding TO 'utf8';

Set the default transaction isolation scheme to ‘read committed’. This blocks the reads from uncommitted transactions.

ALTER ROLE sleekuser SET default_transaction_isolation TO 'read committed';

Set the default timezone to UTC. This is the default timezone our django project too will have.

ALTER ROLE sleekuser SET timezone TO 'UTC';

Now, give our database user ‘sleekuser’ to have all the privileges in the database ‘sleeksoft’.

GRANT ALL PRIVILEGES ON DATABASE sleeksoft TO sleekuser;

Now we have successfully created a database and a database user who has all the privileges over the database we created in the PostgreSQL. Quit from Postgres environment.

\q

Section 3 : Creating Python Virtual Environment

Upgrade pip and install Python virtual environment.

sudo -H pip3 install --upgrade pip
sudo -H pip3 install virtualenv

Create a project folder where you can create virtual environment and install django, gunicorn etc.. for our project. In this tutorial, I am going to create the project folder inside /home/ directory.

cd /home
mkdir sleekproject

Navigate to the project folder / directory.

cd sleekproject/

Create virtual environment inside the project directory. I am naming the virutal environment as ‘sleekenv’.

virtualenv sleekenv

This will create a directory called sleekenv within ‘sleekproject‘ directory. Inside, it will install a local version of Python and a local version of pip. We can use this to install and configure an isolated Python environment for our project. Before we install our project’s Python requirements, we need to activate the virtual environment.

source sleekenv/bin/activate

The terminal prompt should change to indicate that you are now operating within a Python virtual environment. It will look something like this

(sleekenv) root@host:/home/sleekproject#

With virtual environment activated, now we can install Django, Gunicorn and the PostgreSQL adapter (psycopg2) using pip. In virtual environment we can just use pip instead of pip3 no matter if you are using Python version 3.

pip install django gunicorn psycopg2-binary pillow

We now have all the softwares we needed to start our Django project.

Section 4 : Creating and configuring django project

Create django project called ‘sleeksoft’

django-admin startproject sleeksoft

Now you will see a new directory ‘sleeksoft’ inside our project folder. So the whole path will be /home/sleekproject/sleeksoft. Now we need to configure settings.py file in ‘sleeksoft’ directory.

Open settings.py and add ALLOWED_HOSTS to have the localhost and domain name. Change ‘example.com’ to your domain name.

ALLOWED_HOSTS = ['example.com', 'localhost']

Note: Be sure to include localhost as one of the options since we will be using connections through a local Nginx server instance.

Update database details in settings.py. These settings will define the link between our django project and the PostgreSQL database we created. By default settings.py will have a Database details linking to SQLite, replace that with the following code.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'sleeksoft',
        'USER': 'sleekuser',
        'PASSWORD': 's133ks0ft',
        'HOST': 'localhost',
        'PORT': '',
    }
}

Add / Change setting indicating where the static files should be placed. This is necessary so that Nginx can handle requests for these files. The following setting tells Django to place static files in a directory called static in the base project directory.

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'static'

Let’s make the initial migration, which will setup the database tables etc.. You need to be inside the django project we created to run following commands /home/sleekproject/sleeksoft.

cd sleeksoft/
python3 manage.py makemigrations
python3 manage.py migrate

Now, create a superuser for our website.

python3 manage.py createsuperuser

You should enter username, email and password to create a superuser account. After this, lets do the collectstatic command which will collect all the static files in our website to /static/ folder. Remember we defined ‘/static/’ folder in settings.py.

python3 manage.py collectstatic

Create an exception for port 8000. So, we can test that django setup is installed properly.

sudo ufw allow 8000

Now, we can test our your project by starting up the Django development server with the following command

python3 manage.py runserver 0.0.0.0:8000

In the web browser, visit the server’s domain name or IP address followed by :8000. Here 8000 is a port number.

http://server_domain_or_IP:8000

If all setup is done properly, you should see following in the browser.

Django Home

We can login into the django admin by going to /admin/ from home page. There we can use the super user account we created in previous steps. After exploring the Django admin, use CTRL-C in the terminal window to shut down the development server.

Bind Gunicorn with our project

Run following command to bind Gunicorn with our project.

gunicorn --bind 0.0.0.0:8000 sleeksoft.wsgi

This will start Gunicorn on the same interface that the Django development server was running on. We can go back and test the website again. The only difference is we are not doing startserver command from Django, instead Gunicorn will take care of that.

Note: In some cases the admin interface will not have any styling applied since Gunicorn does not know how to find the static CSS content responsible for this. We will configure that in Nginx.

After finished testing, use CTRL-C in the terminal window to stop Gunicorn. We now finished configuring our Django application / Website. We can quit from the virtual environment using the following command.

deactivate

The virtual environment indicator in the command prompt will be removed.

Section 5 : Creating systemd Socket and Service Files for Gunicorn

We have tested that Gunicorn can interact with our Django application, but we should implement an easy and efficient way of starting and stopping the application server. To achieve this, we’ll make systemd service and socket files for Gunicorn.

Create and open a systemd socket file for Gunicorn in the following path.

sudo touch /etc/systemd/system/gunicorn.socket

gunicorn.socket file will be created in the above path, download via FTP and start adding the following code to it.

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock
# Our service won't need permissions for the socket, since it
# inherits the file descriptor by socket activation
# only the nginx daemon will need access to the socket
SocketUser=www-data
# Optionally restrict the socket permissions even more.
# SocketMode=600

[Install]
WantedBy=sockets.target

Save, close and upload the file via FTP when you are finished.

Next, create and open a systemd service file for Gunicorn with sudo privileges. The service filename should match the socket filename with the exception of the extension:

sudo touch /etc/systemd/system/gunicorn.service

gunicorn.service file will be created in the above path, download via FTP and start adding the following code to it.

[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
Type=notify
# the specific user that our service will run as
User=root
# Group=someuser
# another option for an even more restricted service is
# DynamicUser=yes
# see http://0pointer.net/blog/dynamic-users-with-systemd.html
RuntimeDirectory=gunicorn
WorkingDirectory=/home/sleekproject/sleeksoft
ExecStart=/home/sleekproject/sleekenv/bin/gunicorn sleeksoft.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Save, close and upload the file via FTP when you are finished.

Now, our systemd service file is complete. We can now start and enable the Gunicorn socket. This will create the socket file at /run/gunicorn.sock now and at boot. When a connection is made to that socket, systemd will automatically start the gunicorn.service to handle it.

sudo systemctl start gunicorn.socket
sudo systemctl enable gunicorn.socket

We can confirm that the operation was successful by checking for the socket file. Check the status of the process to find out whether it was able to start:

sudo systemctl status gunicorn.socket

Next, check for the existence of the gunicorn.sock file within the /run directory

file /run/gunicorn.sock

Output : /run/gunicorn.sock: socket

If the systemctl status command show errors or if you do not find the gunicorn.sock file in the directory, it’s an indication that the Gunicorn socket was not able to be created correctly. Check the Gunicorn socket’s logs using the following command

sudo journalctl -u gunicorn.socket

Testing Socket Activation

Currently, we have started the gunicorn.socket unit, the gunicorn.service will not be active yet because the socket has not received any connections. we can check this by using the following command

sudo systemctl status gunicorn

Output :

● gunicorn.service - gunicorn daemon
   Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled)
   Active: inactive (dead)

To test the socket activation, we can send a connection to the socket through curl by using the following command

curl --unix-socket /run/gunicorn.sock localhost

You should see the HTML output from our django application in the terminal. This means that Gunicorn was started and was able to serve our Django application. We can verify that the Gunicorn service is running by using the following command

sudo systemctl status gunicorn

Output :

gunicorn.service - gunicorn daemon
    Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled)
    Active: active (running) since Mon 2021-05-10 21:00:35 UTC; 4s ago
  Main PID: 1157 (gunicorn)
     Tasks: 4 (limit: 1153)
    CGroup: /system.slice/gunicorn.service
            ├─1157 /home/sleekproject/sleekenv/bin/python3 /home/sleekproject/sleekenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock sleeksoft.wsgi:application
            ├─1178 /home/sleekproject/sleekenv/bin/python3 /home/sleekproject/sleekenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock sleeksoft.wsgi:application
            ├─1180 /home/sleekproject/sleekenv/bin/python3 /home/sleekproject/sleekenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock sleeksoft.wsgi:application
            └─1181 /home/sleekproject/sleekenv/bin/python3 /home/sleekproject/sleekenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock sleeksoft.wsgi:application
 May 10 21:00:35 django1 systemd[1]: Started gunicorn daemon.
 May 10 21:00:35 django1 gunicorn[1157]: [2018-07-09 20:00:40 +0000] [1157] [INFO] Starting gunicorn 19.9.0
 May 10 21:00:35 django1 gunicorn[1157]: [2018-07-09 20:00:40 +0000] [1157] [INFO] Listening at: unix:/run/gunicorn.sock (1157)
 May 10 21:00:35 django1 gunicorn[1157]: [2018-07-09 20:00:40 +0000] [1157] [INFO] Using worker: sync
 May 10 21:00:35 django1 gunicorn[1157]: [2018-07-09 20:00:40 +0000] [1178] [INFO] Booting worker with pid: 1178
 May 10 21:00:35 django1 gunicorn[1157]: [2018-07-09 20:00:40 +0000] [1180] [INFO] Booting worker with pid: 1180
 May 10 21:00:35 django1 gunicorn[1157]: [2018-07-09 20:00:40 +0000] [1181] [INFO] Booting worker with pid: 1181
 May 10 21:00:36 django1 gunicorn[1157]:  - - [10/May/2021:21:00:36 +0000] "GET / HTTP/1.1" 200 16348 "-" "curl/7.58.0"

If the output from curl or the output of systemctl status indicates that a problem occurred, check the logs for additional details:

sudo journalctl -u gunicorn

Check the /etc/systemd/system/gunicorn.service file for problems. If we make changes to the /etc/systemd/system/gunicorn.service file, reload the daemon to reread the service definition and restart the Gunicorn process using the following command

sudo systemctl daemon-reload
sudo systemctl restart gunicorn

Section 6 : Configure Nginx web server

We need to configure Nginx to pass traffic to the Gunicorn process. We need to create and open a new server block in Nginx’s sites-available directory using the following command.

sudo touch /etc/nginx/sites-available/sleeksoft

Download this file via FTP and start editing. We will add following in this server block.

  • Specify that this block should listen on the normal port 80 and that it should respond to our server’s domain name or IP address.
  • Instruct Nginx to ignore any problems with finding a favicon.
  • Instruct Nginx on where to find the static files that we collected in our /sleekproject/static directory. All of these files have a standard URI prefix of “/static”, so we can create a location block to match those requests
server {
    server_name example.com;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        alias /home/sleekproject/sleeksoft/static/;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

Save, close and upload the file via FTP when you are finished. Now, we can enable the file by linking it to the sites-enabled directory

sudo ln -s /etc/nginx/sites-available/sleeksoft /etc/nginx/sites-enabled

Test the Nginx configuration for syntax errors by using the following command

sudo nginx -t

If no errors are reported, then we can go ahead and restart Nginx by using the following command

sudo systemctl restart nginx

Now, we need to open up our firewall to normal traffic on port 80. Since we no longer need access to the development server, we can remove the rule to open port 8000 now.

sudo ufw delete allow 8000
sudo ufw allow 'Nginx Full'

We should now be able to go to our server’s domain or IP address to view our django application/website.

Section 7 : Certbot installtion for enabling SSL

Install snapd

sudo apt install snapd

Execute the following instructions on the command line on the machine to ensure that you have the latest version of snapd

sudo snap install core; sudo snap refresh core

Run this command on the command line on the machine to install Certbot.

sudo snap install --classic certbot

Execute the following instruction on the command line on the machine to ensure that the certbot command can be run.

sudo ln -s /snap/bin/certbot /usr/bin/certbot

Run this command to get a certificate and have Certbot edit your Nginx configuration automatically to serve it, turning on HTTPS access in a single step.

sudo certbot --nginx

When prompted enter the desired value. This will automatically create SSL certificates for the pointed domain.

Now reboot the server.

reboot

Credits:

  • Digital Ocean
  • Barath Prabu S

Share this post