How to set up and deploy a Django application on a Linux server
Tags: linux, apache, django, nginx
Published: 2021-07-28 14:16:47


*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 
```bash
sudo apt update && sudo apt upgrade
```
- server hostname can be set with the **hostnamectl** utility.
```bash
hostnamectl set-hostname <Host name>
# to verify
hostname
```
- Map  server hostname with the server IP address by adding the following line in */etc/hosts* file.
```bash
<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.
```bash
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*

```bash
 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.
```bash
PermitRootLogin no
PasswordAuthentication no
```
Restart the `sshd` service.
	
## Step2. Setting up FireWall

```bash
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
```bash
# 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.
```bash
# -r for recursive file transfer.
scp -r <local project path> <username>@<ip address>:~/
```
-  **git**
```bash
git clone <remote repository url>
```
### step 4. Set up the python virtual environment
```bash
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
```bash
pip install -r requirements.txt
```
### step 6. make changes to the settings.py.

```python
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')
```

```bash
python manage.py collectstatic
```

## Configuring Postgres Database
```bash
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';
GRANT ALL PRIVILEGES ON DATABASE databasename TO username;
```

### step 7. Test the application with the dev server
```bash
# we have opened the port 8000 with ufw
python manage.py runserver 0.0.0.0:8000
# 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**
```bash
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
- 
```xml
<!-- 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.

#ServerName www.example.com

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

</Directory>

Alias /media /home/YOURUSER/YOURPROJECT/media

<Directory /home/YOURUSER/YOURPROJECT/media>

Require all granted

</Directory>

<Directory /home/YOURUSER/YOURPROJECT/YOURPROJECT>

<Files wsgi.py>

Require all granted

</Files>

</Directory>

WSGIScriptAlias / /home/YOURUSER/YOURPROJECT/YOURPROJECT/wsgi.py

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

WSGIProcessGroup django_app

</VirtualHost>

```
- 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
```bash
sudo chown :www-data <Path to your sqlite file>
sudo chmod 664 <Path to your sqlite file>
```
```bash
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`

```python
server {

  server_name 127.0.0.1 yourhost@example.com;
  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.
    proxy_pass http://127.0.0.1:8000/;
  }

}

```

- Install gunicorn

` pip install gunicorn`

- Start your django project using gunicorn and the wsgi.py file

```python
$ cd </path/to/djangoproject_subdirectory_with_wsgi.py>

$ gunicorn wsgi -b 127.0.0.1:8000 --pid /tmp/gunicorn.pid --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 "wsgi.py"; you just have to use wsgi without the ".py" extension when calling gunicorn. This will start your wsgi application in the background.

--Visit "yourhost@example.com" in your browser

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

[http://yourhost@example.com/](http://yourhost@example.com/)

and see if your application is running. Do not forget to replce  _yourhost@example.com_  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 a`pip 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/gunicorn.pid; 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.
```bash
#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.
```python
import os, site, sys

site.addsitedir('/usr/local/virtualenvs/MYAPP-VIRTUALENV/lib/python2.7/site-packages')

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.
```bash
sudo touch /etc/config.json
```
config.json
```json
{
	"SECRET_KEY":"sncalsjkn@#545jnsjkcn",
	#other keys goes here
}
```
- update the settings.py file
```python
import json
with open('/etc/config.json') as configFile:
	config = json.loads(configFile)
SECRET_KEY = config.get("SECRET_KEY")

# OTHER SETTINGS....
```

### Restart and run the server
```bash
sudo systemctl restart apache2
```