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
- Buy your VPS
- Install Ubuntu 20.04 LTS or whatever is LTS when you’re reading this
- 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:
- Automatically configure caching.
- 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.