systems-obscure/posts/self-hosting-this-site.md

5.7 KiB

title slug date tags
Self-hosting this site /self-hosting-this-site/ 2025-07-21
self-hosting

Previously this site was deployed as follows. I used Gatsby.js to generate a static website from React JavaScript. When changes to the main branch were pushed to the remote GitHub repository, a GitHub action would execute, building the source files and transferring them to an S3 bucket. The HTML generated was then served using AWS Cloud Front.

As I am now trying to self-host as many services as I can, it was time remove the dependency on AWS and GitHub and instead serve the site from my VPS and manage deployment via my self-hosted Forgejo instance.

As part of this process I went on a bit of a side-quest to rebuild the site without Gatsby.

When I tried to update to the lastest version of Gatsby I faced several problems mostly due to Gatsby trying to foist server-side rendering on every project. For such a small blog site with next to zero traffic this is unnecessary. Moreover, I realised that I could also do without all the bloat that Gatsby adds via its plugin ecosystem. (Perhaps if frontend bundles weren't so large, SSR wouldn't be needed lol.)

So I decided to simplify the blog and build it as a Vite React application that gets its content from a JSON index that is generated via a pre-build script.

I wrote a script in JavaScript that loops through all the blog posts in Markdown format from a /posts directory at the project root. It creates a JSON array with an entry for each post. For example, this post would be represented as follows:

[
  {
    "slug": "self-hosting-this-site",
    "title": "Self-hosting this site",
    "date": "2025-07-22T00:00:00.00Z",
    "tags": ["self-hosting"],
    "html": "<p>Previously this site was..."
  }
]

To convert the raw markdown to HTML I used marked. I also added the following steps:

  • convert <code> tags into syntax highlighted blocks using shiki
  • compress and resize images and then transfer them to the public/ directory where Vite sources static assets when deployed
  • find and replace all image src attributes in the resulting HTML with the public/ file path

When I run npm run build:posts this generates the post-index.json file and saves it to /public so that the React application can read from it when rendering the site content.

Running the pre-build script to generate the site content.

I created a custom hook called usePosts that fetches the index and saves it to the session storage to avoid unnecessary network requests. You can see this being invoked in the template component for blog posts.

Having rebuilt the site without the dependency on Gatsby, the next step was to deploy it.

I wanted to retain the "push and deploy" model that I previously achieved via the GitHub Action. This was easy because the syntax for Forgejo Actions is practically identical. You place the YAML declaration in the .forejo/ directory of the project and Forejo picks it up on each push to the remote.

Here's the file:

name: Deploy Blog
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    container: node:18
    steps:
      - uses: actions/checkout@v3
      - run: npm install
      - run: npm run build:posts
      - run: npm run build
      - run: |
          echo "${{ secrets.SSH_FORGEJO_KEY }}" > /tmp/ssh_key
          chmod 600 /tmp/ssh_key
          ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{ vars.VPS_USER }} "bash -c 'rm -rf /var/www/systemsobscure.blog/*'"
          scp -i /tmp/ssh_key -o StrictHostKeyChecking=no -r dist/* ${{ vars.VPS_USER }}:/var/www/systemsobscure.blog/
          rm /tmp/ssh_key

Before compiling the build I first run the pre-build script to generate the post index. Then I remove the existing source files for the site in var/www/ over ssh and then transfer the new source files using scp. (While I run most third-party services on my VPS via Docker, I prefer to keep it simple with my own applications and not use containers.)

Forgejo Actions runner executions.

In order for the site to be served from var/www/systemsobscure.blog, I had to give my Docker instance of nginx running on the VPS access to this directory by adding it to the volume mappings in the docker-compose.

Then I added an nginx .conf file for the blog. This file specifies the SSL certificate to use and sets index.html as the document root. It also adds some default caching and compression, along with security headers.

Finally I updated my SSL certificate generation script to include systemsobscure.blog.

And that's it. My personal website is now self-hosted on my VPS and is automatically deployed via pushes to my self-hosted Git forge. No more GitHub, no more AWS.