img

Javascript, React, VSCode and Snippets

2019-11-07

Python book

We will use the technologies below Nginx as our frontserver to handle HTTPS, static files and route traffic to the uWSGI instance uWSGI will be used to serve the WSGI application Django to build lightweight models for authentication Django REST Framework to build a simple and lightweight API Redis to store cached API data Postgres to store User data dotenv will be used to store secrets on the server virtualenv will be used to create an isolated environment for our app.

The big picture

We will use the technologies below

  • Nginx as our frontserver to handle HTTPS, static files and route traffic to the uWSGI instance

  • uWSGI will be used to serve the WSGI application

  • Django to build lightweight models for authentication

  • Django REST Framework to build a simple and lightweight API

  • Redis to store cached API data

  • Postgres to store User data

  • dotenv will be used to store secrets on the server

  • virtualenv will be used to create an isolated environment for our app

Lets get started!

In the root folder of our project we will create a conf folder that will contain our config files for. Nginx, uWSGI and WSGI. The folder has the tree stucture outlined below

.
├── nginx
│   └── live.conf
├── uwsgi
│   ├── live.ini
└── wsgi
    └── live.wsgi

Update packages on the machine

sudo apt update
sudo apt upgrade -y
sudo apt autoremove
sudo apt autoclean
sudo reboot

Install UFW

UFW is a lightweight firewall.

We will only allow traffic to 22 (SSH), 80 (HTTP) and 443 (HTTPS).

sudo apt install ufw
sudo ufw enable
sudo ufw allow 22
sudo ufw allow 80
sudo ufw allow 443

Install Redis

sudo apt install redis-server

Install PostgreSQL 12

Install some dependencies

sudo apt -y install bash-completion wget

Import the GPG key

wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -

Add the repository for Postgres 12

echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |sudo tee  /etc/apt/sources.list.d/pgdg.list

Update APT and install the necessary packages

sudo apt update
sudo apt -y install postgresql-12 postgresql-client-12

Check that Postgres is enabled for autostart when system boots up

systemctl is-enabled postgresql

Now create the database for the Django app

sudo su - postgres
psql

Create the Database djangoapp and the User djangoapp

CREATE DATABASE djangoapp;

It should return:

postgres=# CREATE DATABASE djangoapp;
CREATE DATABASE

Now create the User with a proper password

CREATE USER djangoapp WITH PASSWORD 'supertopsecretstuff';

It should return:

postgres=# CREATE USER djangoapp WITH PASSWORD 'supertopsecretstuff';
CREATE ROLE

Grant privileges on the Database You just created for the djangoapp User

GRANT ALL PRIVILEGES ON DATABASE djangoapp TO djangoapp;

It should return

postgres=# GRANT ALL PRIVILEGES ON DATABASE djangoapp TO djangoapp;
GRANT

You can exit the PSQL command line by typing \q

Your database has been setup now!

Create the django user and clone the project from Github

Create the user and make sure to create the home folder as well.

sudo useradd -m django -d /home/django -s /bin/bash

We will add our Django code to the /home/django folder under subfolder named project

Create a keypair without a passphrase. Add this to Your Github repo under the Settings tab and then Deployment keys to the menu on the right.

sudo su django
cd ~
mkdir .ssh
cd .ssh
keygen -t rsa -b 4096

Add a .ssh/config in order to create shortcuts and add the snippet below.

Host *
  IdentitiesOnly yes
  ServerAliveInterval 60
  ChallengeResponseAuthentication no
  VisualHostKey yes


host github
  user git
  hostname ssh.github.com

Finally install Git from apt and clone Your repo from Github into a folder named project under /home/django or any other repo service such as Bitbucket or Gitlab.

git clone github:<ORGANISATION>/<REPO>.git project

Replace <ORGANISATION> and <REPO> in the string above with a proper path for Your Git repo.

Create the dotenv file

We are going to make sure that our app follows the 12factor rules.

Create a new file named .env under /home/django

sudo su django
cd ~
nano .env

opy and paste the snippet below into Your newly created .env 

DATABASE_URL=postgres://djangoapp:supertopsecretstuff@localhost:5432/djangoapp

The string should follow the format described below and you should put a proper password in place in order to protect Your app.

DATABASE_URL=postgres://<USERNAME>:<PASSWORD>@<HOST>:<PORT>/<DATABASE>

NOTE: We are using a PostgreSQL string above but You can use MySQL if You want. Read more here about how You can integrate with other database engines.

Create the virtualenv that will hold the dependencies

  1. Change to the django user from Your normal user

sudo su django
cd ~

2. Create the virtualenv environment

python3 -m venv env
source env/bin/activate

3. Install the packages included in requirements.txt

cd ~
pip install -r project/requirements.txt

4. Install uWSGI

Since uWSGI might not be in Your requirements.txt You might have to install it manually.

sudo apt install build-essential python-dev
sudo su django
cd ~
source ~/env/bin/activate
pip install uwsgi

5. Add virtualenv activation snippet in ~/.bashrc

source ~/env/bin/activate
cd ~/project

The snippet above will activate the virtualenv and change directory to ~/project as soon as You run sudo su django

Create the logs folder

We will need to create a folder named logs under /home/django

sudo su django
cd ~
mkdir logs logs/nginx logs/uwsgi

You can then check what is going on with uWSGI for instance by running the command below

sudo tail -f /home/djangi/logs/uwsgi/uwsgi.log

Nginx config file

The Nginx config file is pretty straightforward. You can probably remove some of the clauses below if You do not need them. Follow the comments in the code sample below and tweak the conf file based on Your needs.

Remember to include the path to /home/django/project/conf/nginx/live.conf Your  in Your /etc/nginx/nginx.conf. See below for example.

You can comment out the default files that Nginx loads in order to have a cleaner setup.

File: /etc/nginx/nginx.conf

http {
    ...
    # include /etc/nginx/conf.d/*.conf;
    # include /etc/nginx/sites-enabled/*;
    include /home/django/project/conf/nginx/live.conf;
}

File: <REPO_ROOT>/conf/nginx/live.conf

# This will drop requests that do not match server_name below
server {
    listen       80 default_server;
    server_name  _;
    return       444;
}

server {
    listen 80;
    listen 443 ssl;
    server_name example.willandskill.eu;
    ssl on;

    # Read more at http://nginx.org/en/docs/http/configuring_https_servers.html
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;

    ssl_certificate /etc/letsencrypt/live/example.willandskill.eu/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.willandskill.eu/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.willandskill.eu/fullchain.pem;

    # Generate your dhparam.pem, run command below in the terminal
    # openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
    ssl_dhparam /etc/nginx/ssl/dhparam.pem;

    # Specify cipher suite
    # enables all versions of TLS, but not SSLv2 or 3 which are weak and now deprecated.
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    # Disables all weak ciphers
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

    # Add perfect forward secrecy
    ssl_prefer_server_ciphers on;

    # Add HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";

    access_log /home/django/logs/nginx/access.log;
    error_log /home/django/logs/nginx/error.log;

    # rewrite ^/favicon.ico$ /static/img/icons/favicon.ico last;
    # rewrite ^/robots.txt$ /static/robots.txt last;
    # rewrite /sitemap.xml$ /static/sitemap/sitemap.xml last;

    charset     utf-8;

    # Avoid disk writes, you can turn this on for debug purposes
    access_log on;

    # Max upload size
    client_max_body_size 20M;

    client_body_buffer_size 8K;
    client_header_buffer_size 1k;
    large_client_header_buffers 2 1k;

    client_body_timeout   10;
    client_header_timeout 10;
    keepalive_timeout     40;
    send_timeout          10;

    # Gzip
    gzip             on;
    gzip_comp_level  2;
    gzip_min_length  1000;
    gzip_proxied     expired no-cache no-store private auth;
    gzip_types       text/plain application/xml;
    gzip_disable     "MSIE [1-6]\.";

    # Leave this part if You want to be able to renew Your
    # SSL cert via Letsencrypt
    location '/.well-known/acme-challenge' {
        root /home/django/letsencrypt;
    }

    # Use this section if You are serving Django Admin's static files
    location /static/media/ {
        alias /home/django/env/lib/python2.7/site-packages/django/contrib/admin/static/admin/;
        expires 30d;
        access_log off;
    }

    # Use this section if You are serving media files
    location /media/ {
        alias /home/django/media/;
        expires 30d;
        access_log off;
    }

    # Use this section if You are serving static files
    location /static/ {
        alias /home/django/staticfiles/;
        expires 30d;
        access_log off;
    }

    location / {
        uwsgi_param Host $host;
        uwsgi_param X-Real-IP $remote_addr;
        uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
        uwsgi_param X-Forwarded-Proto $scheme;
        uwsgi_param UWSGI_SCHEME $scheme;

        add_header Cache-Control private;
        add_header Cache-Control no-cache;
        add_header Cache-Control no-store;
        add_header Cache-Control must-revalidate;
        add_header Pragma no-cache;

        uwsgi_pass unix:///tmp/uwsgi.sock;
        include    /etc/nginx/uwsgi_params;
        proxy_read_timeout 1800;
        uwsgi_read_timeout 1800;
    }
}

WSGI config file

File: <REPO_ROOT>/conf/wsgi/live.wsgi

Pythonimport os
import sys
import site
import dotenv

from django.core.wsgi import get_wsgi_application

# The home directory of the user
USERHOME_DIR = '/home/django'

dotenv.read_dotenv(os.path.join(USERHOME_DIR, '.env'))

# Django directory, where manage.py resides
DPROJECT_DIR = os.path.join(USERHOME_DIR, 'project/projectile')
os.environ['DJANGO_SETTINGS_MODULE'] = 'projectile.settings_live'
sys.path.append(DPROJECT_DIR)

# Site-packages under virtualenv directory
SITEPACK_DIR = os.path.join(USERHOME_DIR, 'env/lib/python3.6/site-packages')
site.addsitedir(SITEPACK_DIR)

application = get_wsgi_application()

The most important part in this file is the last line.

Pythonapplication = get_wsgi_application()

The uwsgi instance will look for this line when it will kickstart the app.

NOTE: In our projects we often add a file named settings_live.py in order to house some tweaks that will only be used in production mode ie keys for sending emails and push notifications, settings for application monitoring and so on. This not something You have to do, You can just set it to os.environ['DJANGO_SETTINGS_MODULE'] = 'projectile.settings' in Your live.wsgi if You do not need a separate file to house production specific things in Your project.

uWSGI config file

File: <REPO_ROOT>/conf/uwsgi/live.ini

[uwsgi]
# Define vars here...
base=/home/django
# Define directives here
master=true
processes=2
socket=/tmp/uwsgi.sock
chmod-socket=664
home=%(base)/env/
wsgi-file=%(base)/project/conf/wsgi/live.wsgi
logto=%(base)/logs/uwsgi/uwsgi.log
harakiri=180
disable-logging=false
listen=100
max-requests=1000
vacuum=true

# Lines below only needed for New Relic / Sentry
enable-threads=true
single-interpreter=true

You can test and debug the uWSGI ini file above by running the command below.

uwsgi --ini /home/django/conf/uwsgi/live.ini

Daemonize uwsgi via systemd

File: /etc/systemd/system/uwsgi.service

[Unit]
Description=uWSGI Service
After=syslog.target

[Service]
User=django
Group=www-data
WorkingDirectory=/home/django/project
Environment="PATH=/home/django/env/bin"
ExecStart=/home/django/env/bin/uwsgi --ini /home/django/project/conf/uwsgi/live.ini
Restart=always
RestartSec=3
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

[Install]
WantedBy=multi-user.target

TIP: If You want to keep track of changes in config files, You can check these into Your Git repo in order to save changes and propagate them on several machines. In order to make the maintenance easie You can create a symlink from /etc/systemd/system/uwsgi.service to /home/django/project/ubuntu/systemd/system/uwsgi.service with the command below

`ln -s /home/django/project/ubuntu/systemd/system/uwsgi.service /etc/systemd/system/uwsgi.service`

Logging

Redis

sudo tail -f /var/log/redis/redis-server.log

Postgres

sudo tail -f /var/log/postgresql/postgres-12-main.log

Nginx

Access logs

sudo tail -f /home/django/logs/nginx/access.log

Error logs

sudo tail -f /home/django/logs/nginx/error.log

uWSGI

sudo tail -f /home/django/logs/uwsgi/uwsgi.log

Start / Restart / Stop services

Redis

sudo systemctl start|restart|stop redis-server

Postgres

sudo systemctl start|restart|stop postgres

Nginx

sudo systemctl start|restart|stop nginx

uWSGI

sudo systemctl start|restart|stop uwsgi

Optional: Set up Celery to handle async tasks

Celery is a great task handler for things that might take some time to crunch and You do not want to have long running tasks in between Your requests. Have a look at this guide: How You can setup Celery with Your Python project on Ubuntu 18.04!