--- title: "Self-hosting this site" slug: /self-hosting-this-site/ date: 2025-07-21 tags: ["self-hosting"] --- [Previously](https://systemsobscure.blog/posts/how-I-deploy-this-site) this site was deployed as follows. I used [Gatsby.js](https://gatsbyjs.com) 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](https://forgejo.systemsobscure.net/thomasabishop). 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](https://vite.dev) React application that gets its content from a JSON index that is generated via a pre-build script. I [wrote a script](https://forgejo.systemsobscure.net/thomasabishop/systems-obscure/src/branch/main/scripts/generate-post-index.js) 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: ```json [ { "slug": "self-hosting-this-site", "title": "Self-hosting this site", "date": "2025-07-22T00:00:00.00Z", "tags": ["self-hosting"], "html": "

Previously this site was..." } ] ``` To convert the raw markdown to HTML I used [marked](https://github.com/markedjs/marked). I also added the following steps: - convert `` tags into syntax highlighted blocks using [shiki](https://shiki.style/) - 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](https://forgejo.systemsobscure.net/thomasabishop/systems-obscure/src/commit/43eec03edd2e6330362cc5605a48a91387eb49d3/src/templates/BlogTemplate.tsx). 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: ```yaml 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.](./img/systemsobscure-forgejo-runners.png) 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](https://forgejo.systemsobscure.net/thomasabishop/self-host/commit/8c380b71735f278f4309bbd14ad96cb9a29104b7) in the `docker-compose.` Then I added an [nginx `.conf` file for the blog](https://forgejo.systemsobscure.net/thomasabishop/self-host/src/branch/main/proxy/nginx/conf.d/systemsobscure.conf). 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](https://forgejo.systemsobscure.net/thomasabishop/self-host/commit/61cdbe43c2041d5961ef74416270794eb6fb91c6) 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.