Freyja's Blog

First blog post built with blag

Because every new blog needs a new post.

A new post for a new blog

This is a personal project built… because i can. I have this odd fascination with the concept of gitops - that is, keeping entire codebases + their documentation within a single self-contained git repository. I also have an interest in self-hosting my own services/infrastructure because it gives me a better fundamental understanding of the hardware/software behind things like GitHub Actions. Thus, instead of being run/deployed with any number of cloud services, i’ve done my best to self-host my own stuff.

Because I finally have a stable system setup to run Gitea and Gitea Actions Runners, I’ve decided to migrate my blog to this new format.

Previously, https://blog.raer.me/ was an html-only website. The pages were created based off an html template by hand. This meant I could easily fuck up things like themeing or backlinks cus there wasn’t any single source of ‘truth’ for all these things. If I changed a file location, or the name of a navigation button, I’d have to find and change every single instance of that information in the whole project. That could be dozens of files that need to be edited at once.

That’s terribly inconvenient. To boot, the thing wasn’t version managed and it was deployed entirely manually directly to a folder on my reverse proxy server (see more…) Yikes! None of this was ideal at all!

Fixing that old mess

So building the blog with html manually was a pain in the ass. But doing something like an MVC framework or a CMS for a simple blog seemed like too much hassle as well. I don’t want a WYSIWYG, or something that’s browser-based. I hate dealing with browser frontends. Afterall, a blog is mostly - if not entirely - text-based. Why should I have to deal with the overhead of a server-side scripted website? I just want to write my blog in markdown - like i do with all my documentation already. Then I could even keep it in a git repo, backed up to my private gitea instance.

The answer to all of that, is static site generation. Turns out, there are plenty of other people out there who have looked at available tools, thought something similar to me, then built their own new tool that can take markdown, then generate a whole-ass website with it. Simple, and clean. You write content, maybe tweak some CSS/HTML templates, then the generator handles all the dirty work. No more searching for dozens of instances of a link when I change something in the navbar. That navbar is now a single template file that’s reused by the generator.

Static Site Generation

This all sounds very complicated, yes? Well, sure. But really, its not.

Ultimately, there’s only a few things happening here:

  1. An update to the blog is committed to the git repo.
  2. An automation is triggered that builds the static site, packages it into a docker image, then pushes the image to a registry.
  3. A deployment host running docker receives a “docker pull” command then redeploys the image.

These steps can all be automated quite easily with gitea actions1.

So we have the “how”. But we still don’t have a tool. What should we use? Well, I want something that works well with blogs, but isn’t terribly complex or commercial. I’d also like to be able to extend it, so python is ideal since I’m most familiar with python.

After a bit of searching, I discovered blag which checks all those boxes. It uses a templating engine I’m already familiar with, and the project structure is quite simple to work with. It does everything I could need for a blog, I can easily edit the layout by changing a handful of template files, and I could even fork it + modify the code if I so choose. Once I’m ready to build the site, I simply need to run blag build. Blag will then generate a brand-spanking-new static site based off the markdown files contained in my project. Preem.

Using blag

So with all the basics figured out, it was a simple matter to test things out. First, I used pipenv to install blag inside a test directory. Then, following the blag documentation I then executed a blag quickstart inside my new test dir. This spits out a handful of folders:

These folders contain example content that makes it quite clear what they do (see the docs for more info). Running blag build then spits out another folder called build containing a fully static HTML version of the site. This folder can then be served with any HTTP server of your choosing. You can get setup quickly with a test server by running blag serve to start a temporary dev server. This has the added bonus of automatically rebuilding the site for you if any changes are made to the content - perfect for local development.

Automation

The great thing about this process is its easily reproducible. Now comes the fun part: automation.

Its a relatively simple matter to create a gitea actions workflow that will execute when the documentation is updated. Then, we need to make sure the environment is set up properly on the runner, so we install python3.11 and pipenv. Then, we can checkout the repo with actions/checkout@v3. From there, the process described above plays out: A series of commands are issued that build the static site, package it into a docker image, then push that image to a registry. Then the deployment host is accessed via ssh, the image is pulled, and its redeployed.

Here’s what that workflow looks like:

on:
  push:
    paths:
      - "content/**"
      - "static/**"
      - "templates/**"
    branches:
      - "main"


jobs:
  job1:
    name: Build static site, docker image, upload artifact...
    runs-on: catthehacker-ubuntu
    steps:
      -
        name: Get current date
        id: date
        run: echo "::set-output name=date::$(date +'%Y%m%d%H%M%S')"
      -
        name: Checkout the git repo...
        uses: actions/checkout@v3
      -
        name: Set up docker buildx...
        uses: docker/setup-buildx-action@v3
      -
        name: Login to gitea registry
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.PRODUCTION_GITEA_HOST }}
          username: ${{ secrets.PRODUCTION_REGISTRY_USERNAME }}
          password: ${{ secrets.PRODUCTION_REGISTRY_TOKEN }}
      -
        name: Install required system packages...
        run: |
          export DEBIAN_FRONTEND=noninteractive
          apt update
          apt upgrade -y
          apt install -y curl tar p7zip-full python3.11 pip pipx
      -
        name: Install pipenv, build blog...
        run: |
          pip install pipenv
          pipenv install
          pipenv run blag build
      -
        name: Create artifact...
        run: 7z a -mx=9 ./artifact.7z build
      -
        name: Upload artifact...
        uses: actions/upload-artifact@v3
        with:
          name: artifact_${{ steps.date.outputs.date }}
          path: ./artifact.7z
          retention-days: 7
      -
        name: Build and push docker image to gitea package store
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          platforms: linux/amd64
          tags: ${{ secrets.PRODUCTION_GITEA_HOST }}/${{ gitea.repository }}:${{ gitea.ref_name }}
  job2:
    needs: job1
    name: Connect to deployment host, update, and redeploy docs website.
    runs-on: ubuntu-latest
    steps:
      -
        name: Install required system packages...
        run: |
          export DEBIAN_FRONTEND=noninteractive
          apt update
          apt upgrade -y
          apt install -y iputils-ping
      -
        name: Configure SSH...
        env:
          SSH_USER: ${{ secrets.PRODUCTION_SSH_USER }}
          SSH_KEY: ${{ secrets.PRODUCTION_SSH_KEY }}
          SSH_HOST: ${{ secrets.PRODUCTION_SSH_HOST }}
        run: |
          mkdir -p ~/.ssh/
          echo "$SSH_KEY" > ~/.ssh/staging.key
          chmod 600 ~/.ssh/staging.key
          cat >> ~/.ssh/config <<END
          Host staging
            HostName $SSH_HOST
            User $SSH_USER
            IdentityFile ~/.ssh/staging.key
            StrictHostKeyChecking no
          END
          cat ~/.ssh/config
      -
        name: Test SSH Host...
        env:
          SSH_HOST: ${{ secrets.PRODUCTION_SSH_HOST }}
        run: |
          ping -c 3 $SSH_HOST
          ssh staging 'ls'
      -
        name: Safety check (ensure dirs exist and repo has been cloned)...
        run: |
          echo "Adding ci dir if it doesn't exist..."
          ssh staging 'bash -c "[ -d ci ] || mkdir ci"'
          echo "Cloning git repo if it isn't already cloned..."
          ssh staging 'cd ci; bash -c "[ -d ${{ gitea.event.repository.name }} ] || git clone https://${{ secrets.PRODUCTION_API_TOKEN }}@${{ secrets.PRODUCTION_GITEA_HOST }}/${{ gitea.repository }}.git"'
      -
        name: Deploy testing script on remote...
        run: |
          ssh staging '\
            cd ci/${{ gitea.event.repository.name }}; \
            git remote remove origin; \
            git remote add origin https://${{ secrets.PRODUCTION_API_TOKEN }}@${{ secrets.PRODUCTION_GITEA_HOST }}/${{ gitea.repository} }.git; \
            git checkout ${{ gitea.ref_name }}; \
            git reset --hard HEAD; \
            git pull origin ${{ gitea.ref_name }}; \
            git remote remove origin;'
      -
        name: Pull new image and redeploy...
        run: |
          ssh staging '\
            echo "${{ secrets.PRODUCTION_REGISTRY_TOKEN }}" | docker login --password-stdin --username ${{ secrets.PRODUCTION_REGISTRY_USERNAME }} ${{ secrets.PRODUCTION_GITEA_HOST }}; \
            docker stop blog.raer.me-prod; \
            docker rm blog.raer.me-prod; \
            docker pull ${{ secrets.PRODUCTION_GITEA_HOST }}/${{ gitea.repository }}:${{ gitea.ref_name }}; \
            docker run -d --name blog.raer.me-prod -p ${{ secrets.PRODUCTION_DEPLOYMENT_HOST }}:4020:80 ${{ secrets.PRODUCTION_GITEA_HOST }}/${{ gitea.repository }}:${{ gitea.ref_name }}; \
            docker logout ${{ secrets.PRODUCTION_GITEA_HOST }};'

Conclusion

Bear in mind, this all required a bit of setup and learning to self-host. But, when the hosts & runners are all set up and running properly, with the above workflow, updating this blog is a simple matter of committing to a git repo then pushing it to my remote. The runners handle everything else.

Ain’t gitops grand?

- Freyja



  1. Gitea actions is a self-hosted alternative to GitHub actions using a similar syntax and featureset see more