Posted on the 3rd of Apr 2021 · By Sidharth Shanmugam · In Programming Adventures

Deploying a Wagtail CMS Site on a Raspberry Pi


Recently, I got my hands on a brand new Raspberry Pi 4 Model B, with a quad-core 64-bit SoC clocked at 1.5GHz and 8GB of RAM, its an incredible improvement over my old Raspberry Pi 1 Model B, which I spoke about in my other post where I show you how I built this website using Wagtail CMS. With access to the Pi 4, I am now able to set up a proper web hosting environment for my website. In this post, I will guide you on how to do this and show you how an inexpensive, credit card sized computer is more than sufficient for a good web hosting server.

The Stack We Will Be Implementing

Below are the software that my environment runs on, I've got Gunicorn serving my website and Nginx handling requests, and SSL with PostgreSQL as a data storage. Certbot, for creating SSL certificates and renewing them automatically, UFW as a firewall. I'm also using the Cloudflare free CDN, this blocks a lot of malicious attacks and helps speed up the website by caching and serving files from their data centres around the world.

The fundamentals

  • Python
    • As Wagtail CMS (Django) runs on Python
  • Gunicorn
    • The application web server that runs and serves the Wagtail app
  • Nginx
    • Acts as a reverse-proxy handles requests between Gunicorn and the client's browser, and handles SSL site encryption
  • PostgreSQL
    • A database system that Wagtail CMS will use to store and retrieve data

Security optimisations

  • Certbot
    • To generate SSL certificates for our site so we can use HTTPS instead of the non-secure HTTP
  • UFW
    • Stands for "Uncomplicated Firewall" and is a really simple but effective firewall system to protect the Pi

Content delivery network (CDN)

  • Cloudflare
    • A free CDN which not only speeds up your website with caching but also protects it from hackers and attacks, a really cool thing it does, apart from giving you free SSL certificates, is that it hides your Pi's IP address, this is great for safety. It's basically a giant VPN and it's great.

Equipment

Recommended

  • Raspberry Pi (Preferable a Pi 4, with at least 2GB of RAM)
  • SD Card (8GB+)
  • USB Type C Power Adapter (15.3 W)
  • Internet Connection With Port Forwarding Access (Preferably a Wired Connection)

Optional

  • Raspberry Pi Fan Case such as this one (The RPi 4 CPU can get quite hot at times)
  • USB 3.1 Flash Drive Instead of SD Card, this one has 128GB with read speeds of up to 400MB/s (Much much faster than SD Cards)
  • USB Keyboard and Mouse (We will be setting our Pi up to be headless, so this is not needed and is optional)

Preparing the Raspberry Pi

Installing the OS

Start off by installing the Raspberry Pi OS Lite image from the official Raspberry Pi website. While that's downloading, head over to https://www.balena.io/etcher/ and install the Etcher app, we will be using this app to flash our SD Card/USB Drive with the OS image.

With the USB Drive/SD Card plugged in to your computer, run the Balena Etcher app, select the correct drive and OS image file and flash. This should take a few minutes depending on the speeds of the storage medium that you are using.

Enabling SSH

Once the storage medium has been flashed, we need to enable SSH so we can communicate with the Pi remotely without a keyboard, mouse and monitor. Navigate to the partition named boot on the storage medium, in the root directory create a new file called ssh.

Installing the Software

With the OS set up complete, we can now move on to installing then setting up the software. Before we do that, it is highly recommended that you change the default Raspberry Pi password for added security.

If you've never used a Raspberry Pi before, the default login details are:

username: pi

password: raspberry

You can change the credentials using:

sudo raspi-config

raspi-config is a CLI tool that allows you to change many of the Pi's parameters, it is very simple to use but is only available on the official Raspberry Pi OS.

Installing Python

We can now install Python, we will be installing it through pyenv and we will be installing the pyenv-virtualenv to set up a virtual environment for our Project.

First let's start off by installing the dependencies required by pyenv, these dependencies will allow us to download and compile Python without issues.

sudo apt update
sudo apt install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev

With the dependencies installed, we can now download pyenv itself. We first need to install git. Then, we'll use that to clone the pyenv repository into the /home/pi/.pyenv/ directory. We can also compile the dynamic bash with the make command to speed up pyenv, however, if this fails, don't worry as pyenv will still be able to run without any issues.

sudo apt install git
git clone https://github.com/pyenv/pyenv.git ~/.pyenv
cd ~/.pyenv && src/configure && make -C src

Now pyenv should be installed, we can define the environment variables to the terminal knows what to do when we enter pyenv commands.

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n  eval "$(pyenv init -)"\nfi' >> ~/.profile

Make sure you log-out then SSH back into the Pi so the new bash entries are read.

Let's install the pyenv-virtualenv plugin.

git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.profile

Log out then SSH back in again to reload the shell so the pyenv-virtualenv entries can be loaded in.

After that, we can now install Python. I will be installing Python 3.9.1, as this is a newer version and my Wagtail CMS website supports it. You can install any version you'd like by changing 3.9.1 with any other version number.

pyenv install 3.9.1

After the Python version installs, we can create a virtual environment for our Wagtail project, specify the version number you just installed, then name it something memorable. I'll be calling my virtual environment sidsidsid-venv-3.9.1 as my website is called sidsidsid and I'm using Python 3.9.1

pyenv virtualenv 3.9.1 sidsidsid-venv-3.9.1

Downloading the Wagtail CMS Project

The main Wagtail CMS project files should be downloaded on to the Pi. The best way to do this is by using GitHub, create a repository then from your development computer, push the project directory to the repository, then from the Pi, clone the repository.

We need to make a folder where we're going to store the project directory, I'll be creating a folder called websites in the user directory (/home/pi/).

cd
mkdir websites
cd websites
mkdir sidsidsid_webapp
cd sidsidsid_webapp

Now I have a directory path: /home/pi/websites/sidsidsid_webapp, the sidsidsid_webapp is where I will be cloning my Wagtail project directory. Be sure to change the URL below to your git repository.

git clone https://github.com/SidSidSid16/sidsidsid.git

Your folder structure should look something like below, of course the folder names will be different depending on what you've named it.

home/pi
└── websites
    └── sidsidsid_webapp
        └── sidsidsid

Installing pip packages

We need to install the required pip packages for the Wagtail project. Make sure that you've run the pip freeze > requirements.txt and the repository that you've just clones has the requirements file.

We will first activate the virtual environment that we just created, remember to change the name to whatever you've set.

pyenv activate sidsidsid-venv-3.9.1
pip install -r requirements.txt

Running a quick test

With the pip packages installed, we can run a quick test to make sure that the Wagtail project runs.

With the virtual environment activated, run the following

python manage.py migrate
python manage.py runserver 0.0.0.0:8000

Head over to a web browser and enter http://<Pi's IP address>:8000 and make sure to replace <Pi's IP address> with the local IP address of your Raspberry Pi. You should be able to see your website.

If everything looks good, proceed onwards to set up PostgreSQL

Installing and Setting Up PostgreSQL

We can install PostrgreSQL and its dependencies with:

sudo apt install postgresql libpq-dev postgresql-client postgresql-client-common python-psycopg2

After those packages install, we need to create a new PostgreSQL user to be able to create, delete and alter databases. Enter the following to log into the PostgreSQL terminal as root, then create a user names pi. Make sure the new role is not a superuser but is able to create databases and roles, PostgreSQL will ask you this.

sudo su postgres
createuser pi -P --interactive

After that, we can now connect to PostgreSQL and create a new database for your Wagtail project. I've named my database db_sidsidsid.

psql
create database db_sidsidsid;

Now we need to set up our Wagtail project to be able to connect to the PostgreSQL database, to do this, first install the psycopg2 pip package.

pip install psycopg2

Then, open the production.py file in the settings directory and add the following:

Make sure you change the database name and password to what you have set.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': '<database name>',
        'USER': 'pi',
        'PASSWORD': '<password>',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

Now we need to migrate our models to the new database,

python manage.py migrate --run-syncdb

With that done, Wagtail will now use the new PostgreSQL database we just created. We can move on to installing Gunicorn.

Installing and Setting Up Gunicorn

To install Gunicorn, make sure your virtual env is activated, then enter the following command:

pip install gunicorn

Now that Gunicorn has installed, let's set up a systemd service which allows us to start/stop/restart and start on boot the Gunicorn server for our website.

For that, we need to make a new service file in the /etc/systemd/system/ directory and open it with the nano editor. You can call is whatever you want, I'm calling it gunicorn_sidsidsid.service as its consistent with my Wagtail project name.

sudo nano /etc/systemd/system/gunicorn_sidsidsid.service

Once nano opens it, paste the following in to the file:

[Unit]
Description=Gunicorn Daemon for the SidSidSid Web App
After=network.target

[Service]
User=pi
Group=www-data
WorkingDirectory=/home/pi/websites/sidsidsid_webapp/sidsidsid
ExecStart=/home/pi/.pyenv/versions/sidsidsid-venv-3.9.1/bin/gunicorn --access-logfile - --workers=2 --threads=2 --bind unix:/home/pi/websites/sidsidsid_webapp/sidsidsid/sidsidsid.sock sidsidsid.wsgi:application

[Install]
WantedBy=multi-user.target

Before exiting nano with ctrl+x make sure the directory names are correct and match with your project. Once you've saved and exit nano, run the following to make sure that this service is started on boot:

sudo systemctl daemon-reload
sudo systemctl enable gunicorn_sidsidsid.service

Installing and Setting Up Nginx

After the previous step, where we installed and set up Gunicorn, Gunicorn will now serve out Wagtail project by creating a .sock file. Nginx can connect to the .sock file to handle requests, acting as a reverse-proxy.

Let's first install Nginx.

sudo apt install nginx

Once that installs, head over to the sites-available directory and create a new file. I've named the file sidsidsid, you should name it something more related to your project.

sudo nano /etc/nginx/sites-available/sidsidsid

Inside the file add the following:

Make sure you set the server_name, static and media directory locations and the .sock file name and location correctly.

server {
    server_name sidsidsid.cf www.sidsidsid.cf;

    proxy_set_header HOST $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /home/pi/websites/sidsidsid_webapp/sidsidsid;
    }
    location /media/ {
        root /home/pi/websites/sidsidsid_webapp/sidsidsid;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/pi/websites/sidsidsid_webapp/sidsidsid/sidsidsid.sock;
    }
}

With that file saved, we need to enable this configuration so Nginx can serve it. Enter the following to enable this config:

Make sure you change the command to whatever you've called your configuration file.

Reload Nginx so the new configuration can be loaded with the following: http://<Pi's IP address> and make sure to replace <Pi's IP address> with the local IP address of your Raspberry Pi.

With everything working, let's

ln -s /etc/nginx/sites-available/sidsidsid /etc/nginx/sites-enabled/sidsidsid
sudo systemctl reload nginx

Now, the Pi should be serving your Wagtail website using Gunicorn, Nginx and PostgreSQL. We can test if everything is working as it should be by heading over to a web browser and going to the following URL: http://<Pi's IP address> and make sure to replace <Pi's IP address> with the local IP address of your Raspberry Pi.

With everything now working, we can install and set up a firewall (UFW), then set up Cloudflare CDN, and finally create SSL certificates with Certbot.

Installing and Setting Up UFW

To install UFW, enter the following command:

sudo apt install ufw

Now we need to enter an additional command to make UFW work:

sudo update-alternatives --set iptables /usr/sbin/iptables-legacy

After that, you can enable the following rules:

sudo ufw allow 22
sudo ufw allow 80
sudo ufw allow 443

Finally, reboot the Pi. Don't worry, you don't have to restart the Pi every time you add or remove UFW rules, we just have to do it this time once so that the update-alternatives command is set up properly.

Setting Up Cloudflare

To set up Cloudflare, you first need to create an account at https://www.cloudflare.com/. The free plan works perfectly and that is what I use for all of my websites.

After creating an account, you'll have to set the domain name's nameservers to the ones Cloudflare gives you. You can set the nameservers directly from your domain registrar's website.

My nameserver settings

Once the nameservers have been set, Cloudflare will enable your website. You will then have to set DNS records which points to the public IP address of your Raspberry Pi. You need to port-forward both port 80 and 443 of your Raspberry Pi.

My DNS record settings

Create an A record called www that points to your public IP address. Then, create a CNAME record with your root domain name (without the www) that points to the URL of your website (with the www).

Make sure you disable the Cloudflare proxy so we can create SSL certificates through Certbot. After we create these certificates, we can enable the proxy.

Creating SSL Certificates With Certbot

As Cloudflare can generate SSL certificates for us, this step is completely optional. However, I like to do this step to make sure that if Cloudflare's SSL certificates start giving us issues, we will have a fail-safe.

We need to install Certbot first, then we can use it to create certificates. Make sure you set the domain names to the ones you will be using.

sudo apt install certbot python-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Wrapping Up

The Raspberry Pi should now be hosting your Wagtail project. Make sure you enable the Cloudflare DNS proxy and change the Cloudflare settings to your preferences.

In my previous Wagtail blog post, I did talk about setting up a mail server with MailCow.

I am also planning to host an email server using MailCow, it’ll be nice to have an email on this domain.

Unfortunately, I was hit with bad luck. It turns out, my ISP blocks port 25. This means, although I can receive email (through port 465 and 587), I will not be able to send emails. This is very annoying and has saddened me a lot as I cannot have a custom email address. I will have to pay for a cloud email hosting service.

In the future, I'll be setting up Redis and Memcached for caching so my website can run faster.