Loudwhisper

Creating a Blog in 2023

"Table of Contents"

I was not around in the 90's and therefore I will refrain from making comparisons to some golden age or glorious time of the web. However, I can describe my experience in spinning up a blog in 2023, which is what I want to do in this post.

Introduction

Firstly, it's important to mention that mine is the perspective of someone who has decent technical skills, so obviously it doesn't apply to someone who is just looking for a quick way of writing and publishing (in which case, perhaps I would recommend write.as).

Nowadays there is an incredible number of options to start a blog, from proprietary platforms (such as medium, boo) to federated ones (writefreely instances, as I mentioned above) to a wide range of self-hosting platforms (wordpress, SSGs, Ghost and many more).

However, I had a few requirements in mind for whatever platform I was going to choose:

Choosing the Platform

The above requirements immediately forced me to abandon the idea of using tools like Wordpress (way overkill), and immediately pushed me towards Static Site Generators. I have used Jekyll in the past, and also Hugo. I wouldn't use Jekyll again, I don't want to fight with ruby dependencies again as I have to do with other sites I have. Hugo is great, and it was definitely one of my initial ideas. However, I decided to use Zola instead, simply because it is rust based, and I am trying to learn rust lately.

Not that I had to write any code, but it is also a bit simpler compared to Hugo (which now is fairly complex!).

In terms of themes, I like the idea behind bearblog and thankfully kind humans created both Hugo and Zola themes that mimic its minimalism but allow me to self-host my own website, which I like as in the future I might want to change things, add different pages, etc.

Technical Setup

Once the platform was chosen, I had to work a little bit in automating the publishing part, as I want to just write something, push the changes to the corresponding git repository and be done with it.

To do this, I have designed the following flow:

Now, this is not the cleanest approach, it has small downtimes when updates happen (in the order of seconds), it requires a hacky script etc., but I think it is simple and stable for a low-maintenance project. Maybe in the future I will polish it and figure out a better way, but until then this will work.

Git Setup

I use my own Gitea instance, and for a few version now this supports actions similar to Github Actions. I don't like Github Actions, but there are no many alternatives, so I will have to accept it.

The repository has the following at .gitea/workflows/build.yaml:

name: ci
on:
  push:
    branches:
      - 'master'
jobs:
  docker:
    runs-on: ubuntu-latest
    container: catthehacker/ubuntu:act-22.04
    steps:
      -
        name: Checkout
        uses: actions/checkout@v4
      -
        name: Login to registry
        uses: docker/login-action@v3
        with:
          registry: [REDACTED]
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      -
        name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: [REDACTED]/[REDACTED_OWNER]/loudwhisper:latest,[REDACTED]/[REDACTED_OWNER]/loudwhisper:${{ gitea.sha }}

The first thing I would notice is that catthehacker/ubuntu:act-22.04 image. Well, apparently the stock Ubuntu image used by the runner does not have docker tools installed and therefore it doesn't work well with docker related tasks. This image is way overkill (almost 2GB!) though and it will be one of the first things I will get rid of. Also, I am extremely uneasy running this image terms of the security impact of a possible supply chain attack (not against me, surely, but maybe against someone else).

The first step downloads the repository content, the second logs in into my OCI registry (Gitea instance) and the last builds and pushes the images using the tags specified. Note that since I am using Gitea as the registry, I will use my Gitea URL in place of the first [REDACTED] and the name of the Gitea user in place of the [REDACTED_OWNER].

This action triggers everytime I push to master or merge a PR to master, and generates a new image.

Docker Setup

Zola has an example on how to package a website in a Docker image right in their documentation, so I did not invent anything myself. I did not know the static-web-server, but I have looked into it, and it seems a nice option, especially considering the Docker image is really minimal.

The result Dockerfile is the following:

FROM ghcr.io/getzola/zola@sha256:[HASH]  as zola

COPY . /project
WORKDIR /project
RUN ["zola", "build"]

FROM ghcr.io/static-web-server/static-web-server@sha256:[HASH]
WORKDIR /
COPY --from=zola /project/public /public

The only difference with the example is the use of sha256 hashes to identify the images instead of the tags. I think this has a small security advantage, as I don't know if the tags for those images could be overwritten (maybe by a malicious collaborator).

Since the image is built, all it's left is to write the Docker-compose file.

version: '3'
services:
  loud-whisper:
    image: [REDACTED]/[REDACTED_OWNER]/loudwhisper:abc
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.loudwhisper.entrypoints=http"
      - "traefik.http.routers.loudwhisper.rule=Host(`loudwhisper.me`)"
      - "traefik.http.middlewares.loudwhisper-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.routers.loudwhisper.middlewares=loudwhisper-https-redirect"
      - "traefik.http.routers.loudwhisper-secure.entrypoints=https"
      - "traefik.http.routers.loudwhisper-secure.rule=Host(`loudwhisper.me`)"
      - "traefik.http.routers.loudwhisper-secure.tls=true"
      - "traefik.http.routers.loudwhisper-secure.tls.certresolver=http"
      - "traefik.docker.network=traefik"
networks:
  traefik:
    external: true

As I have mentioned earlier, I am using traefik as reverse-proxy on the VPS, so I don't have to map any port, I just have to add some labels, in this case to match the host I want to serve and to configure the redirect from HTTP to HTTPs.

Redeploying on Push

The last step of the setup is, in fact, the ugliest. This involves a cron job (that I run every 5 minutes), and a bash script.

The script is more or less the following:

#!/bin/bash
APP_DIR="/volume0/loudwhisper"
COMPOSE_FILE="${APP_DIR}/docker-compose.yml"

SHA1=$(curl -s -X 'GET' \
  'https://[GITEA_INSTANCE]/api/v1/repos/Personal/loudwhisper/commits/master/status?access_token={{ gitea_token }}' \
  -H 'accept: application/json' | jq .sha | tr -d '"')

echo "Latest hash of the blog: $SHA1"

echo "Checking if the docker-compose file needs updating..."
grep -q "${SHA1}" $COMPOSE_FILE
if [[ $? -eq 0 ]]; then
  echo "Image is already correct. Exiting"
  exit 0
else
  echo "Compose file needs to be updated..."
fi

echo "Checking if image is already present in local cache..."

until docker images | grep ${SHA1};
do
  echo "No match, sleeping 10s"
  sleep 10
done

echo "Replacing image tag"
sed -i.back "/image:/s/:[0-9a-z].*/:${SHA1}/g" $COMPOSE_FILE

cd $APP_DIR
docker-compose down
docker-compose up -d

I am deploying this script with ansible to the VPS, and that's why you can see a templated value for the Gitea token. It is possible to scope the tokens permissions in Gitea, so I recommend to just add read permissions to repositories for this token, since this is all that is needed.

Some Numbers

I have almost no content on the website, but so far I am fairly happy of the results. The homepage weights just a little less than 2KB, while the post pages range from few KB to a few hundreds of KB (especially with images).

I have also made some quick loadtest using locust.

You can see the results in the table below:

loadtest

The amount of requests per second, response time and simulated users can be seen in the image below:

metrics

Considering the amount of users that this website has (or better, doesn't have), it might be able to survive even an improbable Hug of Death. I hope I did not speak too soon.

Categories: #technical #writing Tags: #zola #meta