Quick Django VPS Install

This is a minimal guide for setting up a minimal host server for Django.

When testing this I used a USD$5/month VPS from Vultr, but any common VPS service will suffice.

Note: that link is my afiliate link; See here for more details.

Table of contents

Assumptions

  1. You know Linux moderately well.
  2. You are familiar with Django and virtualenv.
  3. You have bought a VPS instance and installed Debian on it.

If you want to use a different OS/Distro, you will need to adjust some steps to suit.

Conventions

# this is a root prompt

$ this is a user prompt

(venv)$ this is a user prompt with the virtualenv active

Preparing

First, we're going to do a little house cleaning and ensure our system is up to date:

# apt update
# apt autoclean
# apt dist-upgrade -y

Next we insall the packages we need:

# apt install -y h2o python3-venv fail2ban git htop gnupg

Last, we create a regular service user for every day use:

# useradd -G www-data -m {username}
# passwd {username}

Postgresql

I always use Postgresql. Unless you have a good reason otherwise, you probably ought, too.

We're going to follow the instructions from Postgresql

# echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
# wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
# apt update
# apt install -y postgresql

Let's take this opportunity to clean up:

# apt autoremove

Next step, we'll create a DB user for our app, and for ourselves:

# su - postgres -c "createuser gunicorn"
# su - postgres -c "createuser -g gunicorn -d {username}"

The Web Server

As a departure from the common place, I'm using a relatively new web server: h2o.

It provides an impressive list of features:

Beyond that, it is by far the simplest web server to configure I've found yet.

We need to replace the default config file /etc/h2o/h2o.conf with our own:

server-name: "My site"
user: www-data
access-log: "|rotatelogs -l -f -L /var/log/h2o/access.log -p /usr/share/h2o/compress_logs /var/log/h2o/access.log.%Y-%m-%d 86400"
error-log: "|rotatelogs -l -f -L /var/log/h2o/error.log -p /usr/share/h2o/compress_logs /var/log/h2o/error.log.%Y-%m-%d 86400"
pid-file: /run/h2o.pid

listen:
  port: 80

hosts:
  default:
    paths:
      "":
        proxy.reverse.url: "http://[unix:/srv/www/gunicorn.sock]"
        proxy.preserve-host: ON
      "/static":
        file.dir: /srv/www/html/static
      "/media":
        file.dir: /srv/www/html/media

Now we can ask systemd to restart with our updated config:

# systemctl restart h2o

The App

We need to make space for our app to live:

# cd /srv
# mkdir www
# chown www-data:www-data www
# chmod g+w www

We can use su to switch to our user:

# su - {username}
$

When we exit (or use ctrl-D) it will exit back to our root shell.

Now as our regular user:

$ cd /srv/www/

Make all the directories we need:

$ mkdir -p code html/static html/media
$ chgrp -R www-data html
$ chmod -R g+w html

Create the virtualenv, and install some essentials:

$ python3 -m venv venv
$ . venv/bin/activate
(venv)$ pip install gunicorn psycopg2-binary

Checkout your code using git, and ensure the root of the Django project (i.e. where manage.py is) is at /srv/www/code. Either check out the repo there, or use a symlink.

For this example, we'll create a fresh Django project instead:

(venv)$ pip install django
(venv)$ django-admin startproject sample code

Edit code/sample/settings.py to add:

STATIC_ROOT='/srv/www/html/static/'
MEDIA_ROOT='/srv/www/html/media/'

And update the DATABASES entry:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'sample',
        'OPTIONS': {
            'options': '-c role=gunicorn',
        }
    }
}

The OPTIONS section there is more for using manage.py than runtime; It tells libpq (which psycopg2 uses to talk to Postgres) to switch to the gunicorn when connecting. We can do this because we added ourselves to the gunicorn role when creating our DB user.

We must also remember to set DEBUG to False, and add our hostname to the ALLOWED_HOSTS.

Before going further, let's create the DB instance for this app:

$ createdb -O gunicorn sample

And our regular django deploy steps:

(venv)$ python manage.py migrate
(venv)$ python manage.py collectstatic
(venv)$ python manage.py createsuperuser

The App Server

For simplicity we'll use gunicorn

It's fast, stable, flexible, and well supported.

We're going to use the gunicorn systemd recipe

As root, add the file /etc/systemd/system/gunicorn.service:

[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
Type=notify
Group=www-data
DynamicUser=yes
RuntimeDirectory=gunicorn
WorkingDirectory=/srv/www/code
# Path to our venv install of gunicorn
ExecStart=/srv/www/venv/bin/gunicorn sample.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Next, add /etc/systemd/system/gunicorn.socket:

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/srv/www/gunicorn.sock
SocketUser=www-data
SocketMode=600

[Install]
WantedBy=sockets.target

And at long last, we can ask systemd to start our socket service:

# systemctl enable --now gunicorn.socket

Finally, to make sure it's all safe and reliable, we'll reboot:

# reboot

And... that's it?

To summarise, we have:

This is a very basic setup. We've not:

But as all of those are dependent upon your application, I won't delve into them here.

Final comments.

The dynamic user feature of sytemd was new to me, and somewhat handy. It means systemd will clean up after us, and our task won't have any persistent files.

Using the role switch was the only 'novel' step in this process, and one I'm thankful to RhodiumToad on IRC for showing me.

My main issue with this setup is it requires root to restart the service when you update/change your code/settings.

Todo

Some refinements I'd like to make to this:

  1. make it easier to restart the app as our normal user [perhaps via PID file?]
  2. include instructions for TLS, and enabling HTTP/2
  3. LetsEncrypt, anyone?