How to set up and deploy a Django application on a Linux server

29th July 2021

Step 1. Server configuration.

  • Install any linux based distribution(Ubuntu, manjaro ...etc). Update and upgrade your software repositories.

// on any debian based distribution

sudo apt update && sudo apt upgrade
  • server hostname can be set with the hostnamectl utility.
hostnamectl set-hostname <Host name>
# to verify
  • Map server hostname with the server IP address by adding the following line in /etc/hosts file.
<ip address>    <host name>

( Running commands as the root user is not safe, it's better to login as a regular user with sudo permission)

  • Add a new user and add him to the sudoers list.
adduser <username>
adduser <username> sudo
# Login to the system as the new user

(Password authentication is not secure, generate and use ssh keys instead)

  • ssh-keygen for key generation (use ssh-copy-id if it is available in your system ).

  • generate ssh keys in local machine with ssh-keygen -b 4096 command.

  • create an empty .ssh directory inside home directory of your server . Provide the read,write and execution permissions.

  • Transfer the locally generated ssh keys to the server with scp

scp <location of the generated key> <username>@ipaddress:~/.ssh/<name given to the keys>
  • provide read and write permissions for the key.
  • For disallowing the root Login and password logins, add the following lines to the /etc/ssh/sshd_config file.
PermitRootLogin no
PasswordAuthentication no

Restart the sshd service.

Step2. Setting up FireWall

sudo apt install ufw
# Block all incoming and outgoing traffic except ssh 
sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw allow ssh

#  allow the port 8000 (Django uses this port run the development server)
sudo ufw allow 8000
# make sure to enable ssh before enabling ufw
sudo ufw enable

# after testing
sudo ufw delete allow 8000 && sudo ufw allow http/tcp
#for status
sudo ufw status

Step 3. Set up the project for production.

3.1 Generate the requirements.txt file

# in the root directory
pip freeze > requirements.txt

# Django==3.2.5
# psycopg2==2.9.1
# pytz==2021.1
# sqlparse==0.4.1

3.2 Transfer the project from the local machine to the server.It can be done by using any of the following tools.

  • An ftp client like FileZilla
  • scp to transfer files via ssh.
# -r for recursive file transfer.
scp -r <local project path> <username>@<ip address>:~/
  • git
git clone <remote repository url>

step 4. Set up the python virtual environment

sudo apt install python3-pip
sudo apt install python3-venv
# Creating a virtual environment inside the project directory
python3 -m venv <projectDirectory>/<virtualEnvName>
# activation
source <projectDirectory>/<virtualEnvName>/bin/activate

step 5. install project dependencies

pip install -r requirements.txt

step 6. make changes to the

DEBUG = False
ALLOWED_HOSTS = ['<ipAddress/domainName>']
# Static handled differently in production.
# third party storages or whitenoise requires additional settings
STATIC_ROOT = os.path.join(BASE_DIR,'static')
python collectstatic

Configuring Postgres Database

sudo apt install postgresql postgresql-contrib
sudo -u postgres psql
CREATE DATABASE databasename;
CREATE USER username WITH PASSWORD 'password';
ALTER ROLE username SET client_encoding TO 'utf8';
ALTER ROLE username SET default_transaction_isolation TO 'read committed';
ALTER ROLE username SET timezone TO 'Asia/Kolkata';

step 7. Test the application with the dev server

# we have opened the port 8000 with ufw
python runserver
# and test the application on <ipAddress>:8000

step 8. Configuring production server

Django server is only for development purposes, you should not use it for production. We can use reliable servers like apache with mode_wsgi or Nginx with gunicorn

8.1 apache with mod_wsgi

  • install apache sudo apt install apache-2
  • install mod_wsgi
    sudo apt install libapache2-mod-wsgi-py3

  • configure apache

cd /etc/apache2/sites-available
# duplicate any of the default configuration file to get  started.
cp 000-default.conf <name_given_to_your_config>.conf

- edit your config file and add the followings

<!-- NB: all paths needs to be absolute -->
<VirtualHost *:80>

# The ServerName directive sets the request scheme, hostname and port that

# the server uses to identify itself. This is used when creating

# redirection URLs. In the context of virtual hosts, the ServerName

# specifies what hostname must appear in the request's Host: header to

# match this virtual host. For the default virtual host (this file) this

# value is not decisive as it is used as a last resort host regardless.

# However, you must set it for any further virtual host explicitly.


ServerAdmin webmaster@localhost

DocumentRoot /var/www/html

# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,

# error, crit, alert, emerg.

# It is also possible to configure the loglevel for particular

# modules, e.g.

#LogLevel info ssl:warn

ErrorLog ${APACHE_LOG_DIR}/error.log

CustomLog ${APACHE_LOG_DIR}/access.log combined

# For most configuration files from conf-available/, which are

# enabled or disabled at a global level, it is possible to

# include a line for only one particular virtual host. For example the

# following line enables the CGI configuration for this host only

# after it has been globally disabled with "a2disconf".

#Include conf-available/serve-cgi-bin.conf

<!-- right before the closing of </VirtualHost> we need to map Aliases for static and media -->

Alias /static /home/YOURUSER/YOURPROJECT/static

<Directory /home/YOURUSER/YOURPROJECT/static>

Require all granted


Alias /media /home/YOURUSER/YOURPROJECT/media

<Directory /home/YOURUSER/YOURPROJECT/media>

Require all granted




Require all granted




WSGIDaemonProcess django_app python-path=/home/YOURUSER/YOURPROJECT python-home=/home/YOURUSER/YOURPROJECT/<VIRTUALENV>

WSGIProcessGroup django_app

  • enable the configuration sudo a2ensite <YourConfiguration>
  • disable the default configuration sudo a2dissite <Default configuration>
  • reload apache service systemctl reload apache2

// Give database access to apache
$ For sqlite database

sudo chown :www-data <Path to your sqlite file>
sudo chmod 664 <Path to your sqlite file>
sudo chown :www-data <Path to your Django project>
sudo chown -R :www-data <Path to your media directory>
sudo chmod -R 774 <Path to your media directory>

8.2 Nginx with gunicorn

  • Install nginx and add this to /etc/nginx/sites-enabled/default
server {

  access_log /var/log/nginx/domain-access.log;

  location / {
    proxy_pass_header Server;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_set_header X-Forwarded-For  $remote_addr;
    proxy_set_header X-Scheme $scheme;
    proxy_connect_timeout 10;
    proxy_read_timeout 10;

    # This line is important as it tells nginx to channel all requests to port 8000.
    # We will later run our wsgi application on this port using gunicorn.

  • Install gunicorn

pip install gunicorn

  • Start your django project using gunicorn and the file
$ cd </path/to/>

$ gunicorn wsgi -b --pid /tmp/ --daemon

# --daemon parameter tells gunicorn to run in the background
# So that gunicorn continues to run even if you close your ssh session
# (You cannot remain ssh-ed into your server all the time right!)

Please do not use ""; you just have to use wsgi without the ".py" extension when calling gunicorn. This will start your wsgi application in the background.

--Visit "[email protected]" in your browser

Now your application must be up and running on your instance. Visit:

and see if your application is running. Do not forget to replce [email protected] in the above and in the nginx configuration file before.

--(Optional) Additional Notes

  • In Step 1, if confused; remove all existing lines from the /etc/nginx/sites-enabled/default file and put the above code inside it. (Or delete and create a new blank file and add the code)

  • If you are using virtualenv, and you did apip install gunicorn inside the virtualenv in Step 2, then run the Step 3 command with respective virtualenv activated.

  • The pid of the gunicorn process is stored in /tmp/; incase you want to kill the existing gunicorn process and restart it.

  • supervisord might be used in conjunction, which helps in restarting the gunicorn daemon automatically in case it dies due to some reason. This is useful in production environments.

step 9. Environment variables

9.1 Passing Apache Environment Variables to Django via mod_wsgi.

#add the env variables to the apache configuration
     SetEnv DB_NAME mydatabase
     SetEnv DB_USER mydbuser
     SetEnv DB_PASSWD sekrit
     SetEnv DB_HOST localhost
# reload and restart apache secrvice

edit the wsgi file so that mode_wsgi can pass vars to the application.

import os, site, sys


BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(BASE_DIR, '..'))

os.environ["DJANGO_SETTINGS_MODULE"] = "myapp.settings"  # see footnote [2]

from django.core.wsgi import get_wsgi_application
_application = get_wsgi_application()

env_variables_to_pass = ['DB_NAME', 'DB_USER', 'DB_PASSWD', 'DB_HOST', ]
def application(environ, start_response):
    # pass the WSGI environment variables on through to os.environ
    for var in env_variables_to_pass:
        os.environ[var] = environ.get(var, '')
    return _application(environ, start_response)

9.2 we can use a config file.

sudo touch /etc/config.json


    #other keys goes here
  • update the file
import json
with open('/etc/config.json') as configFile:
    config = json.loads(configFile)
SECRET_KEY = config.get("SECRET_KEY")


Restart and run the server

sudo systemctl restart apache2