The big picture
We will use the technologies below
Nginx
as our frontserver to handle HTTPS, static files and route traffic to the uWSGI instanceuWSGI
will be used to serve the WSGI applicationDjango
to build lightweight models for authenticationDjango REST Framework
to build a simple and lightweight APIRedis
to store cached API dataPostgres
to store User datadotenv
will be used to store secrets on the servervirtualenv
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
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!