Deploy your django website via Digital Ocean with Nginx and Gunicorn

To be able to centralize my knowledge and to learn more about web development I thought about building my own personal website.

Thus this project was born. I quickly decided to use django for that because it's a mature framework and comes with a lot of handy builtin features. Unfortunatly django replaced their old logo which I liked better :)

Designing the website

For the design of the website I decided to use the most simplest way of sharing simple posts. When I started researching blog designs I stumbled upon the django tutorial by Corey Schafer. I instantly liked the design of his demo blog and I adapted it.

Set up the production server

To deploy my application I needed to register and pay a server provider for that. I decided on Digital Ocean because I already own an account there and the server fee of 5$/month is also cheap. If you don't have a digital ocean account create one here with my referral link if you want (You will get 100$ for free and I get 25$ to continue to run this website for 5 more months :D). After buying the server or creating a droplet I got an IP-address to which I can ssh to. If you're on linux like me you can just run in your terminal:

$ ssh root@xxx.xx.xx.xx

Where xxx.xx.xx.xx is the IP address of your server. Windows users should use putty to ssh into their server.

Next I installed the necessary ubuntu 20.04 packages with:

$ sudo apt update
$ sudo apt install python3-pip python3-dev libpq-dev curl virtualenv git nginx

After that I git clone my django project from gitlab to the home directory and create a virtual environment and activate it.

$ git clone https://gitlab.com/user/myproject.git
$ cd myprojectenv
$ virtualenv venv -p python3
$ source venv/bin/activate

Now I installed all the packages which are required for django to run.

(venv) $ pip install black
(venv) $ pip install django
(venv) $ pip install django-crispy-forms
(venv) $ pip install Pillow
(venv) $ pip install uwsgi
(venv) $ pip install gunicorn

Of course you should have all the packages in a requirements.txt file and install it with:

(venv) $ pip install -r requirements.txt

Now I made sure that the settings.py is adjusted accordingly. Setting DEBUG = False and defining the STATIC_ROOT location:

import os
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

I ran next:

(venv) $ python manage.py collectstatic

and confirmed that a folder called static got created.

Now I needed to make migrations for the database and create a super user with which I can post posts:

(venv) $ python manage.py makemigrations
(venv) $ python manage.py migrate
(venv) $ python manage.py createsuperuser

To test my server I created an exception in the firewall and started the server:

(venv) $ sudo ufw allow 8000
(venv) $ python manage.py runserver 0.0.0.0:8000

Then I checked if the server is running correctly on xxx.xx.xx.xx:8000.

Set up Gunicorn

Gunicorn is a python wsgi http server for unix which is used to make a bridge between your django application and the the nginx server. Since I already installed gunicorn in my virtual environment I only need it to setup.

To test if Gunicorn can serve my application I run:

(venv) $ gunicorn --bind 0.0.0.0:8000 myproject.wsgi

in my project folder. This will start Gunicorn on the same interface that the Django development server was running on. Now I can test the application again.

Note: A WSGI is a python spec that defines a standard interface for communication between your application and a web server. This was defined to make it easy to communicate between these two common components.

To make the communication more robust I make use of the systemd service and socket files.

The Gunicorn socket will be created at boot and will listen for connections. When a connection occurs, systemd will automatically start the Gunicorn process to handle the connection.

To create a socket open a file:

$ sudo vim /etc/systemd/system/gunicorn.socket

and define the Unit (socket description), Socket (socket location) and Install (when to create the socket) sections like this:

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

Save and quit (:wq).

Next we define the gunicorn service in:

$ sudo vim /etc/systemd/system/gunicorn.service

and add description and requirements for this service:

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

Then we define the service section about the location of the django project:

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

[Service]
User=viktor
Group=www-data
WorkingDirectory=/home/viktor/myproject
ExecStart=/home/viktor/myproject/venv/bin/gunicorn \
          --access-logfile - \
          --workers 3 \
          --bind unix:/run/gunicorn.sock \
          myproject.wsgi:application

Then create the Install section which will tell systemd what to link this service to if it's enable to start at boot:

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

[Service]
User=viktor
Group=www-data
WorkingDirectory=/home/viktor/myproject
ExecStart=/home/viktor/myproject/venv/bin/gunicorn \
          --access-logfile - \
          --workers 3 \
          --bind unix:/run/gunicorn.sock \
          myproject.wsgi:application

[Install]
WantedBy=multi-user.target

Save and close the gunicorn.service with :wq. Now we need just to start the Gunicorn socket:

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

To make sure that the socket is running we can check the journal logs with:

$ sudo journalctl -u gunicorn.socket

or check the status with:

$ sudo systemctl status gunicorn.socket

In /run there should also be a file called gunicorn.sock. We can check it with:

$ file /run/gunicorn.sock

Which should us give this output:

/run/gunicorn.sock: socket

If for some reason something went wrong try restarting the Gunicorn process with:

$ sudo systemctl daemon-reload
$ sudo systemctl restart gunicorn

Set up Nginx to proxy pass to Gunicorn

Nginx pronounced "engine x" is an HTTP and reverse proxy server which runs on many heavy loaded russian sites like yandex.ru and mail.ru. I decided to make use of this high performance server and learning on the way on how to set it up myself. To my surprise setting up nginx to pass traffic to the Gunicorn process was quite easy.

First I needed to open a new server block in Nginx's sites-available directory:

$ sudo vim /etc/nginx/sites-available/myproject

Inside I define that the server should listen to port 80 and that it should respond to my domain name or IP address (I will explain in the next section how to setup the custom domain name):

server {
    listen 80;
    server_name www.kreschenski.com;
}

Next I defined where it can find the static files:

server {
    listen 80;
    server_name www.kreschenski.com;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /home/viktor/myproject;
    }
}

Furthermore I added to ignore any problems with finding the favicon.ico. Finally I tell nginx to pass all the traffic which it receives to Gunicorn:

server {
    listen 80;
    server_name www.kreschenski.com;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /home/viktor/myproject;
    }
    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

Save and close the file and enable the file by linking it to the sites-enables directory:

$ sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled

Now let's test if the nginx configuration has no syntax errors and restart the service if everything was fine:

$ sudo nginx -t && sudo systemctl restart nginx

At last I opened the normal traffic port for nginx with:

$ sudo ufw allow 'Nginx Full'

and delete the testing port:

$ sudo ufw delete allow 8000

Not the application can be opened and viewed on kreschenski.com.

Setting up my custom domain name

To use my custom domain name I bought the kreschenski.com domain from namecheap.com for around 9$/year. It already includes the whois guard.

Just as a side note the whois guard protects your data from the public. Meaning if someone is running the command $ whois kreschenski.com on linux or mac my personal information is hidden.

Ok let's continue!

After buying the domain name I clicked on Manage beside my domain name and went to the Nameservers section. There I needed to setup a Custom DNS which links to Digital Ocean:

ns1.digitalocean.com
ns2.digitalocean.com
ns3.digitalocean.com

Note that namecheap needs a couple of hours to link the DNS settings to Digital Ocean. Then I saved it and went over to Digital Ocean. In the domain section of your droplet enter a domain and add it to your droplet. To link the domain name to your IP create a new record at your specific domain https://cloud.digitalocean.com/networking/domains/kreschenski.com which links kreschenski.com and www.kreschenski.com to your individual IP address.

And that's it! After some time you can access the website not only via the IP address but also through the domain name.

SSL Encryption

To enrypt the connection to my website and get rid of the not-secure warning in the browser I need an encryption certificate. To get this certificate i went to letsencrypt. Since I have shell access I used the Certbot ACME client for Ubuntu 20.04 and nginx server. I tested if the latest snapd is installed via:

$ sudo snap install core
$ sudo snap refresh core

I made sure that certbot was not installed via apt with:

$ sudo apt remove certbot

and then install the classic certbot:

$ sudo snap install --classic certbot

Then I ran:

$ sudo certbot --nginx

and tested the renewal of the certificate with:

$ sudo certbot renew --dry-run

After that I confirmed that certbot was working by going to my site and checking if the browser shows if the connection was secure which it was :).

After 90 days the certificate needs to be renewed. Because I am lazy and I don't want to do that I set up a so called cron job to do the renewal every month. For that run in your terminal:

$ sudo crontab -e

Choose an editor which you prefer and add this to the last l>ine of the file:

# m h dom mon dow command
30 2 1 * * sudo certbot renew --quite

The renewal will run every first day of a month at 2:30 a.m..

Django deployment checklist

To make sure that my website is production ready it's always a good practice to check the deployment list on the offical django page. For a more detailed way on setting up your django website even with PostgreSQL I strongly recommend this article by Erin Glass from which I took strong inspiration.

Welcome!

Welcome to my website. Here I share my knowledge, projects and interests.