The cheapest way to host your Django app

The cheapest way to host your Django app

When you’re trying to launch a SaaS product while broke, minimising running costs will allow you to keep your product online as long as possible.

The cheapest way to host your Django app is by using a VPS.

Instead of money, it requires a little bit of time spent on the following:

  1. Typing linux commands.
  2. Creating config files.
  3. Setting up backups.
  4. Exiting Vi.

Let’s do this.

Things to note

  • If you get permission denied errors when running the commands below, just switch to the root user with the su command and try again.
  • Read the commands before typing.
  • There’s no need to learn about a command that you don’t understand unless you’re trying to do something special.
  • If you run into a problem, use Google DuckDuckGo to figure it out on your own.

Finally, this is mostly a set and forget setup.

After setting this up, your only recurring task would be to update the server’s packages every week or so.

Grab a cheap VPS

  • If you’re from Europe, I recommend Hetzner.
  • For Americans, go with Ramnode.
  • Asians can choose UpCloud because it has servers around Asia.

With about $5 (or $10 if using UpCloud) a month, you can get a CPU Core and 2GB of RAM to run your app.

If you code your app well (use caching and optimal database queries, etc), you can serve thousands of users using a small server like that.

It’s much cheaper than paying $100 per month on a PaaS but of course, it costs a little bit of time.

Next steps

  1. Buy your VPS
  2. Install Ubuntu 20.04 LTS or whatever is LTS when you’re reading this
  3. Follow my linux first time setup guide.

Don’t forget to allow port 80 and 443 in your firewall.

Prepare the web server

First, install nginx:

apt install nginx

Then, grab some better defaults:

systemctl stop nginx
rm -rf /etc/nginx
git clone https://github.com/h5bp/server-configs-nginx.git /etc/nginx

Setup an nginx config:

cp /etc/nginx/conf.d/templates/example.com.conf /etc/nginx/conf.d/yourdomain.io.conf
sed -i 's/example.com/yourdomain.io/g' /etc/nginx/conf.d/yourdomain.io.conf

We’ll serve Django media files from the same server, so we’ll add a new block inside yourdomain.io.conf:

server {
listen 443 ssl http2;

server_name media.yourdomain.io;

include h5bp/tls/ssl_engine.conf;
include h5bp/tls/certificate_files.conf;
include h5bp/tls/policy_intermediate.conf;

location / {
  alias /var/www/yourdomain_media/;
  }
}

Create the media directory:

mkdir /var/www/yourdomain_media

To handle static files, I recommend you use a package called whitenoise.

It’s better than using Nginx because it:

  1. Automatically configure caching.
  2. Required barely any configuration.

Setup TLS

Install Certbot:

snap install certbot --classic

Learn how to generate an SSL certificate by following the certbot instructions.

I recommend you use Cloudflare as your DNS provider and the cloudflare certbot plugin. Cloudflare is free and provides protection and speedups for your app — things you need when you’re trying to save money.

Make sure you setup the certficate files in /etc/nginx/h5bp/tls/certificate_files.conf.

The certificate file is available at /etc/letsencrypt/live/yourdomain.io/fullchain.pem

And the private key is at /etc/letsencrypt/live/yourdomain.io/privkey.pem

If you used the cloudflare plugin, you need to generate a dh_params file manually:

openssl dhparam -out /etc/ssl/dhparam.pem 4096

Then add it to /etc/nginx/h5bp/tls/certificate_files.conf:

ssl_dhparam /etc/ssl/dhparam.pem;

Prepare for Django

A user named django will run our app:

useradd -r django
usermod -a -G django www-data

Setup database

We’ll use PostgreSQL as our database in this example. Skip this if you’re using Sqlite or Mysql:

apt install postgresql postgresql-contrib
su postgres
psql

Once you type psql, you will be dropped inside the PostgreSQL shell. Create a user for your app:

CREATE ROLE myappuser WITH LOGIN CREATEDB ENCRYPTED PASSWORD 'yourpassword';
\q

Then, connect again as your user and create a database:

psql -U myappuser -d postgres
CREATE DATABASE myapp;
\q
exit

Bring source code

You need to get your source code on the server. I like to use git but you can also use sftp to keep things simple.

Create a place to store source code:

mkdir /srv/myapp
chown -R user:user /srv/myapp

Replace user with the user you created when you followed my linux setup guide.

Now all you have to do is copy your source code to /srv/myapp

If you’re using Github, setup a deploy key:

ssh-keygen -t ed25519

Then simply clone your code to /srv/myapp

Setup Django app

To run your app, you’ll use Gunicorn and a virtual environment:

apt install python3-virtualenv
su user
cd /srv/myapp
virtualenv venv
source venv/bin/activate

Install your app’s dependencies as usual using pip.

Also set Django settings either in your settings.py file or .env.

To run our app as a daemon, we’ll use systemd. Create a config:

vi /etc/systemd/system/myapp.service

With the following contents:

[Unit]
Description = myapp Gunicorn
After = network.target

[Service]
PIDFile = /run/myapp/django.pid
User = django
Group = django
WorkingDirectory = /srv/myapp
ExecStartPre = +/bin/mkdir -p /run/myapp
ExecStartPre = +/bin/chown -R django:django /run/myapp
ExecStartPre = /srv/myapp/venv/bin/python manage.py collectstatic --noinput
ExecStartPre = /srv/myapp/venv/bin/python manage.py migrate
ExecStart = /srv/myapp/venv/bin/gunicorn myapp.wsgi -b 127.0.0.1:8000 --pid /run/myapp/django.pid --workers=2 --chdir=/srv/myapp
ExecReload = +/bin/kill -s HUP $MAINPID
ExecStop = +/bin/kill -s TERM $MAINPID
ExecStopPost = +/bin/rm -rf /run/myapp/django.pid
PrivateTmp = true
TimeoutSec=900

[Install]
WantedBy = multi-user.target

This will automatically perform Django model migrations and collect your static files.

You can tweak this config by changing the number of workers depending on how many cpu cores you have. Also, don’t forgot to change things like myapp to your own app name.

It’s time to start the app:

systemctl daemon-reload
systemctl enable myapp.service
systemctl start myapp.service

You can check if the app is running using curl:

apt install curl
curl -H "Host: yourdomain.io" http://127.0.0.1:8000"

You should get a response or 500 error back. If you get a connection error, figure out what’s wrong by typing journalctl -u myapp.service to see logs.

Setup Nginx

We’ll use nginx to forward requests to our app. Edit the file at /etc/nginx/conf.d/yourdomain.io.conf.

Add this to the top the file:

upstream myapp {
    server 127.0.0.1:8000;
}

Change the location / block to the following:

location / {
    proxy_pass http://myapp;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    client_max_body_size 30m;
}

Start nginx:

systemctl start nginx

Your app should now be accessible at yourdomain.io.

Offsite backups

If you lose data, you’ll probably end up going out of business. That’s why we’ll setup a robust backup system using restic and Backblaze B2 .

Download the latest restic from github:

wget https://github.com/restic/restic/releases/download/v0.9.1/restic_0.9.1_linux_amd64.bz2
bzip2 -d restic_0.9.1_linux_amd64.bz2
mv restic_0.9.1 restic
mv restic /usr/bin
chmod +x /usr/bin/restic

Create a B2 bucket from your dashboard and a corresponding API key and initialize your restic repo:

RESTIC_PASSWORD=yourpassword B2_ACCOUNT_ID=youraccountid B2_ACCOUNT_KEY="youraccountkey" RESTIC_REPOSITORY="b2:reponame:folder" restic init

Create a .pgpass file:

vi /root/.pgpass

Add the following:

127.0.0.1:5432:myapp:myappuser:mypostgrespassword

Secure it:

chmod 600 /root/.pgpass

Replace the values with the ones you created earlier.

Create a backup script:

vi /root/backup.sh

With the contents:

#!/bin/bash
export RESTIC_PASSWORD=myresticpassword
export B2_ACCOUNT_ID=myaccountid
export B2_ACCOUNT_KEY="myaccountkey"
export RESTIC_REPOSITORY="b2:reponame:folder"

pg_dump > /tmp/myapp.sql

restic backup \
    /var/www/myapp_media \
    /tmp/myapp.sql

restic forget \
    --keep-hourly 24 \
    --keep-daily 7 \
    --keep-weekly 4 \
    --keep-monthly 12 \
    --prune

rm -f /tmp/myapp.sql

Allow execution:

chmod +x backup.sh

To run our backups, we’ll use a systemd timer:

vi /etc/systemd/system/backblaze.timer

Contents:

[Unit]
Description=Backup to Backblaze every hour

[Timer]
OnBootSec=15min
OnUnitActiveSec=1h

[Install]
WantedBy=timers.target

And a corresponding service:

vi /etc/systemd/system/backblaze.service

Contents:

[Unit]
Description=Backblaze restic backup

[Service]
ExecStart=/bin/bash /root/backup.sh

[Install]
WantedBy=default.target                        

Enable and start timer:

systemctl daemon-reload
systemctl enable backblaze.timer
systemctl start backblaze.timer

Your server is now safe from data loss and you can sleep peacefully at night.

Many VPS providers also provide full image backups for an additional but low fee. Set these up for more protection.

Conclusion

If you did everything correctly, you should now have a robust system running your Django app for less that $20 per month.

Every week or so, ssh into your server and type:

apt update && apt upgrade

This will keep packages updated.