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:
- Minimalism. I want the least amount of maintenance, the least amount of bloat, the least amount of upgrades necessary and so on. Less is more.
- Security. I don't want my website to be a liability, I want to be reasonably sure that it won't get breached, defaced, etc. and ideally without me having to do my day job for my hobbies.
- Embedding pictures. Sometimes I might want to add a picture within a post, and the platform I use should support that.
- Writing with my editor. I don't want to use a visual editor (web based) to write, I want to primarily use my desktop editor, which happens to be Vim. This is simply because I want to fit the writing within my general workflow.
- RSS feed. I use RSS myself to follow blogs and resources I am interested of, so I wanted to offer the same for those who choose to read this blog.
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:
- A change on the
master
branch ingit
should trigger the publishing process. - The website should be packaged in a
docker
image which should serve it. Each version a new image. - Images are tagged with the latest commit, such as
loudwhisper:abc123
. - Docker-compose is used in a VPS where
traefik
is also running, and the image is updated anytime a new commit is found. - Once the
docker-compose.yml
file is updated with the new image, restart (down
andup -d
) it.
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:
The amount of requests per second, response time and simulated users can be seen in the image below:
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.