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
- You know Linux moderately well.
- You are familiar with Django and virtualenv.
- 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 install 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:
- HTTP2 native
- HTTP / HTTPS
- FastCGI / Reverse proxy
- Strong focus on security
- Impressive performance
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
Now let's confirm that the Django application is accessible, at least inside the server:
$ wget -O - http://localhost 2>/dev/null | grep title
<title>The install worked successfully! Congratulations!</title>
Finally, to make sure it's all safe and reliable, we'll reboot:
# reboot
And... that's it?
To summarise, we have:
- installed h2o
- installed Postgresql
- installed gunicorn
- installed our codebase
- configured gunicorn to launch on startup
This is a very basic setup. We've not:
- tuned our Postgres settings
- enabled pre-compressed assets
- determined an appropriate number and type of gunicorn workers
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:
- make it easier to restart the app as our normal user [perhaps ia PID file?]
- include instructions for TLS, and enabling HTTP/2
- LetsEncrypt, anyone?