10 min read

Mastering Django: Step-by-Step Guide to Deploying Your Project on a Linux Server in 2024

Follow our step-by-step guide to deploy on Linux servers effortlessly!

What we will achieve?

By the end of this guide we should have a Django project deployed on a linux VPS server. We would also setup SSL/TLS certificates using Certbot.

The arcitecture we would achieve is as follows.

Arcitecture diagram

Table of Contents

  1. Deploy bare metal linux server
  • Update server software
  • Setup domain name
  • Set hostname
  • Set timezone
  • Create a new user
  • Allow only the new user to login via ssh.

2. Create and deploy a Django project

  • Install python and dependencies
  • Create a new ssh key pair for github
  • Run the Django development server and check if it's accessible from the internet

3. Install and setup Gunicorn

  • Install Gunicorn
  • Create a systemd service to run gunicorn on startup
  • Test if the project is accessible from Gunicorn instead of Django development server

4. Setup Nginx

  • Create an nginx config usign nginxconfig.io
  • Change some parts of the config to accommodate for Gunicorn (default is uWSGI)
  • Install certbot and manage certificates
  • Dry run cerbot cron certificate update

5. Firewall setup

  • Setup firewall to only allow SSH, HTTP and HTTPS traffic

1. Deploy bare metal linux server

Usually, when I deploy a linux server I reach for Ubuntu LTS release. At the time of this tutorial on Linode the latest version is Ubuntu 20.04 LTS. Also, add your public SSH key to linode and use it with this server. You can do this by navigating in your profile under the SSH keys and add your public key.

Add new SSH key to linode

Setup domain name

Let's first setup the domain name. We do this first since DNS propagation takes some time. By the time we need to test if our domain name working hopefully it should have been propagated.

I bought the domain name from Namecheap. The server runs on Linode. Therefore, I need to use Linode's domain servers in Namecheap as shown below.

Change Namecheap default DNS

Next, on Linode you will need to create a new domain. I used the default configuration and it works fine. If you don't want to use the default configuration you'll need to manually setup A records, MX records etc. Check the images below.

Create a domain on Linode using default configuration
Created domain on Linode

Note: If you have to setup the DNS records manually use the below image for reference. Otherwise, you will already have this if you followed the s

All the DNS records created on Linode

Update server software

To update the existing software on a fresh install lets do:

apt-get update && apt-get upgrade

Set hostname

Here you need to set the server's hostname. Replace <your-hostname> with your prefer hostname. For example, I used `costapiy-server`

hostnamectl set-hostname <your-hostname>

Then, you'll need to add the hostname to your hosts file. Edit the file using Vim as below.

vim /etc/hosts

# Add a line similar to:
<server-ip> <host-name>

For example, mine looks like this.

# /etc/hosts       localhost costapiy-server

# The following lines are desirable for IPv6 capable hosts
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters

Set timezone

Let's set the timezone to UTC. You can choose a different timezone based on your needs. I always try to use UTC in all my projects since it's easier for me to remember.

timedatectl set-timezone UTC

Create a new user

So far, we were running the above commands as the root user. In general, is not a good idea to run everything as root since if your server is compromised then the adversary will have root access and can do anything in your system.

To create a new user:

adduser webserver
usermod -aG sudo webserver

Change to this user:

su webserver

Create a new directory for SSH stuff

mkdir ~/.ssh

Set the permissions to only allow this user into it

chmod 700 ~/.ssh

Create a file for SSH keys

vim ~/.ssh/authorized_keys

Use the root user to copy .ssh files

This command will change the user from webserver to root again

sudo su

Copy the ~/.ssh/authorized_keys from root folder to the new user folder

cp .ssh/authorized_keys /home/webserver/.ssh/authorized_keys
su webserver
sudo chown webserver:webserver  ~/.ssh/authorized_keys
sudo chmod 600 ~/.ssh/authorized_keys

Logout from this ssh session

Use the exit command to logout of the ssh session as many times as required.

Log into your server as the new user

# Replace the IP address with your own
ssh webserver@

Allow only the new user to login via ssh.

sudo vim /etc/ssh/sshd_config

NOTE: You will be asked for a password when you use the sudo command. This is the password you used when you created the user earlier in this tutorial. Inside, you need to update two settings:

  1. Find PermitRootLogin and change it to PermitRootLogin no
  2. Find PasswordAuthentication and change it to PasswordAuthentication no

Reload the ssh deamon for the changes to be applied

sudo systemctl reload sshd

This will achieve two things:

  1. You'll be able to login as the webserver user without password
  2. You'll NOT be able to login as root user. (Which is more secure)

2. Create and deploy a Django project

Install python and dependencies

Here will install the python dependencies on our VPS.

sudo apt-get install python3-pip python3-dev nginx
sudo -H pip3 install --upgrade pip
sudo -H pip3 install virtualenv

Create a new ssh key pair for github

At this point we need a way to transfer our code to the linux server. There are multiple different approaches for this. I chose a simple one, Git and Github. The workflow is as follows.

  1. Make changes locally and push to Github.
  2. Login to the server and pull from Github the changes.
ssh-keygen -t rsa
cat ~/.ssh/id_rsa.pub

Copy the output of the above command to Github. This will enable you to pull the code from Github without the need to use a password.

On the webserver clone / upload your project

Clone your project from github. If this is successful you should have the project in your home directory on the server.

git clone git@github.com:username/repository.git

Create virtual env for project doworkout

virtualenv doworkout
source doworkout/bin/activate
pip install django

Apply db migrations

My project folder is doworkout. Therefore, I have to navigate to that folder and run the migrations. This will create the necessary database file and tables.

cd doworkout
python manage.py migrate
python manage.py createsuperuser

Run the development server

python manage.py runserver

Now our project should be accessible from the internet on port 8000. You can check by navigating to http://server-ip:8000. You might see a Django specific error message saying to add the server ip to the allowed hosts.

Modify project/settings.py to add the server ip


At this point the django development server should work normally without any errors.

Generate Django's static files

Since Django is served using a WSGI compatible server, it's necessary to serve static files using NGINX since it's much more efficient in serving static files. Django has a command to create all the static files. We need to setup where we want this static files to be generated.

Add this to settings.py

The static files will be generated under the directory project/static

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

Collect (generate) all the static files

python manage.py collectstatic
# 128 static files copied to '/home/webserver/doworkout/static'.

3. Install and setup Gunicorn

Test if gunicorn can server our project

This will create a virtual environment called doworkout. I like to create a python virtual environment per project. It will install Gunicorn in the virtual environment as recommended by Gunicorn documentation here. Last it will start Gunicorn and server our project on port 8000 again. (If you run the development server on port 8000 you'll need to stop it)

source doworkout/bin/activate
pip install gunicorn
cd ~/doworkout
gunicorn --bind doworkout.wsgi

Gunicorn works, but we are not serving static files anymore. You can see there is no css served for the admin pages. Again, you can navigate to http://server-ip:8000/admin. You'll notice that the admin page now is only the html without any css styling. This is because our static files are not served using Gunicorn. They will get served by NGINX in section 4 below.

Create systemd service to run gunicorn

We need to run Gunicorn as systemd service. Essentially, this will run Gunicorn as a deamon instead of a normal process. We want to use systemd to do this since it will take care of running Gunicorn if it crashes or if the system restarts.

Create a file called gunicorn-doworkout.service to save the Gunicorn systemd configuration. You can change the name to be related to your project name.

sudo vim /etc/systemd/system/gunicorn-doworkout.service

Contents of the file /etc/systemd/system/gunicorn-doworkout.service

Make sure to change the User, paths and filenames accordingly.

Description=gunicorn daemon

ExecStart=/home/webserver/doworkout/bin/gunicorn --access-logfile - --workers 3 --bind unix:/home/webserver/doworkout/doworkout.sock doworkout.wsgi:application


Create the service and enable it

sudo systemctl start gunicorn-doworkout
sudo systemctl enable gunicorn-doworkout

Check the gunicorn socket file

sudo systemctl status gunicorn-doworkout

Here if you should see something similar to this. Also, on the project folder you should see the socket file.

webserver@costapiy-server:~$ sudo systemctl status gunicorn-doworkout
[sudo] password for webserver:
● gunicorn-doworkout.service - gunicorn daemon
     Loaded: loaded (/etc/systemd/system/gunicorn-doworkout.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2021-12-30 20:44:02 UTC; 14h ago
   Main PID: 1781 (gunicorn)
      Tasks: 4 (limit: 1071)
     Memory: 88.9M
     CGroup: /system.slice/gunicorn-doworkout.service
             ├─1781 /home/webserver/doworkout/bin/python /home/webserver/doworkout/bin/gunicorn --access-logfile - --workers 3 --bind unix:/home/webserver/doworkout/do>
             ├─1793 /home/webserver/doworkout/bin/python /home/webserver/doworkout/bin/gunicorn --access-logfile - --workers 3 --bind unix:/home/webserver/doworkout/do>
             ├─1794 /home/webserver/doworkout/bin/python /home/webserver/doworkout/bin/gunicorn --access-logfile - --workers 3 --bind unix:/home/webserver/doworkout/do>
             └─1795 /home/webserver/doworkout/bin/python /home/webserver/doworkout/bin/gunicorn --access-logfile - --workers 3 --bind unix:/home/webserver/doworkout/do>

Helpful link for debugging

4. Setup NGINX

Follow the guide here. Only use this to generate the files. Do not use this for setting up certbot since will do it later. This will generate almost all the necessary files we need. We just need to make some minor adjustments since the default setup is for use with uWSGI and we are using Gunicorn.

Copy the files to your /etc/nginx directory.

Delete one unnecessary file

sudo rm -f /etc/nginx/nginxconfig.io/security.conf

We need to change one location block here to use the Gunicorn setup.

Again, you'll need to be careful here to use the correct domain names and paths.

Basically, from

location / {
        include nginxconfig.io/python_uwsgi.conf;


 location / {
        include proxy_params;
        proxy_pass http://unix:/home/webserver/doworkout/doworkout.sock;

The finished file with the modifications /etc/nginx/sites-available/doworkout.net.conf

server {
    listen                  443 ssl http2;
    listen                  [::]:443 ssl http2;
    server_name             doworkout.net;
    set                     $base /home/webserver/doworkout;

    # SSL
    ssl_certificate         /etc/letsencrypt/live/doworkout.net/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/live/doworkout.net/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/doworkout.net/chain.pem;

    # security
    include                 nginxconfig.io/security.conf;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/webserver/doworkout/doworkout.sock;

    # Django media
    location /media/ {
        alias $base/media/;

    # Django static
    location /static/ {
        alias $base/static/;

    # additional config
    include nginxconfig.io/general.conf;

# subdomains redirect
server {
    listen                  443 ssl http2;
    listen                  [::]:443 ssl http2;
    server_name             *.doworkout.net;

    # SSL
    ssl_certificate         /etc/letsencrypt/live/doworkout.net/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/live/doworkout.net/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/doworkout.net/chain.pem;
    return                  301 https://doworkout.net$request_uri;

# HTTP redirect
server {
    listen      80;
    listen      [::]:80;
    server_name .doworkout.net;
    include     nginxconfig.io/letsencrypt.conf;

    location / {
        return 301 https://doworkout.net$request_uri;

Restart NGINX and Gunicorn

sudo systemctl restart nginx
sudo systemctl daemon-reload
sudo systemctl restart gunicorn

Install Certbot

NOTE: Make sure you are not in a virtual env

sudo apt-get install python3-certbot-nginx

Generate the SSL/TLS certificate

Change the domain name to match yours. Add both variants of the domain name domain.com and www.domain.com

sudo certbot certonly --standalone -d doworkout.net -d www.doworkout.net

Fix cron renwal

Our Certbot installation create a cron job to run everyday and try to update the SSL/TLS certificate. It needs a bit of tweaking in order to work as expected.

sudo vim /etc/cron.d/certbot

# Add the following.
0 */12 * * * root test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot -q renew --nginx

Test if renewal works

Certbot has a dry run functionality. This allows us to test if the renewal will work but it doesn't update the certificates. It just simulates an update.

sudo certbot renew --nginx --dry-run

Might get the following error

Error output from post-hook command nginx-reload.sh: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful

However, it should be working as expected now.

Restart NGINX

By restarting NGINX you should be able to navigate to your domain name and the Django project will be served as expected using HTTPS.

sudo service nginx status

At this point, if you try to navigate to the HTTP it will redirect you to the HTTPS version. In addition, if you navigate to www.domain.com it should also redirect you to the non-www version.

5. Firewall setup

Last, we will harden the server to only allow SSH, HTTP and HTTPS connections.

# Reset all existing rules
sudo ufw reset

# Enable OpenSSH connections
sudo ufw allow OpenSSH

# Enable HTTP traffic
sudo ufw allow http

# Enable HTTPS traffic
sudo ufw allow https

# Turn the firewall on
sudo ufw enable


Congratulations! You made it to the end.

At this point you should have a working django deployment available to for the whole world to see :)

If you like the content here please consider subscribing.

I will create a video of this guide as well in the future. If you want to get notified when the video tutorial is ready subscribe.

If you have any comments or feedback feel free to reach me @costapiy