Introduction

Even though I haven’t written anything for a while, my self-hosting setup has seen quite a few changes behind the scenes. I’ve stopped using the Cloudflare IPFS Gateway, tried running an overengineered Kubernetes cluster in my living room, ported said cluster to Civo, before finally settling back to a simpler k3s cluster running on a Raspberry Pi.

This post is meant to give an insight on my current setup, and to be a starting point for anyone wanting to do anything similar. We’ll go through the steps to getting a static website up and running on IPFS.

I’m assuming the reader has at least a passing familiarity with the IPFS protocol.

Prerequisites

We’re going to make use of GitHub actions later in this post, so you’ll need your own GitHub repository. It’s a good idea anyway to version control a static website.

We’ll make use of IPFS pinning services, so an account with one of them is required. I’m going to make this demonstration using Pinata, but others are available.

We’re also going to need a domain name, and the ability to call the registrar’s API to update a DNS record.

A major step in hosting your content is getting some of said content. Any static content will do, so feel free to use this beautiful, hand-crafted sample:

<!DOCTYPE html>
<html>
  <head>
    <title>My Static Website</title>
  </head>
  <body>
    <header>
      <h1>This is a static website</h1>
    </header>
  </body>
</html>

Introducing GitHub Actions

GitHub Actions are GitHub’s flavor of automation pipelines. They can be used for a lot of things, but in this case we’re going to use them to perform CD on our website.

To get started, create a repository on GitHub, and add a .github/workflows/ directory to it. Add a file called ipfs.yaml inside it.

.
├── .git
├── .github/
│   └── workflows/
│       └── ipfs.yaml
├── website/
│   └── index.html
└── README.md

This file will contain the definition of our workflow. We’ll want this workflow to run on a push to master, so the beginning of the file should look like this:

name: IPFS
on:
  push:
    branches:
      - master
  workflow_dispatch:

The key on.workflow_dispatch means that we’ll also be able to trigger the workflow manually.

Workflows are composed of jobs, which are in turn composed of steps. In our case, we’ll have two such jobs.

Pinning our content on IPFS

There are many pinning services available. For this example I’ll demonstrate using Pinata.

Since it’s a really bad idea to add secrets and tokens in plain text inside a repository, we’re going to use Repository Secrets to store all sensitive info.

Add the following to the workflow definition file:

jobs:
  pinata:
    name: Pin on Pinata
    needs: build
    runs-on: ubuntu-latest
    outputs:
      cid: ${{ steps.pinata.outputs.cid }}
    steps:
      - uses: actions/checkout@v3
      - uses: aquiladev/ipfs-action@v0.2.1
        id: pinata
        with:
          path: ./website
          service: pinata
          pinataKey: ${{ secrets.PINATA_API_KEY }}
          pinataSecret: ${{ secrets.PINATA_API_SECRET }}
          pinataPinName: website
      - run: echo ${{ steps.pinata.outputs.cid }}

This jobs checks out the repository, pins the website on Pinata, and then outputs the corresponding CID.

What, what, what, what

– Macklemore & Ryan Lewis, Thrift Shop

DNSLink records are special DNS TXT records. They look like this:

_dnslink.my-website.xyz. IN TXT "dnslink=/ipfs/..."

They’re an alternative to IPNS and are meant to link static domain names to dynamic content stored on IPFS.

You can check the current DNSLink record for my blog by running the following command:

$ dig +short _dnslink.blog.bruyant.xyz txt
"dnslink=/ipfs/..."

The next steps depend on who controls the authoritative DNS server for your domain. In my case, I’m using OVH. They don’t provide GitHub Actions of their own, so I wrote a couple to call their API (see my GitHub page).

In the workflow file for this blog, the job looks like this:

update-dns:
  name: Update OVH DNS
  needs:
    - web3-storage
  runs-on: ubuntu-latest
  steps:
    - name: OVH alter DNS record
      uses: CrispyBaguette/ovh-dns-alter-action@v1.1.1
      with:
        application-key: ${{ secrets.OVH_APPLICATION_KEY }}
        application-secret: ${{ secrets.OVH_APPLICATION_SECRET }}
        consumer-key: ${{ secrets.OVH_CONSUMER_KEY }}
        dns-zone: bruyant.xyz
        record-id: 69420
        target: "dnslink=/ipfs/${{ needs.web3-storage.outputs.cid }}"
    - name: OVH refresh DNS zone
      uses: CrispyBaguette/ovh-dns-refresh-action@v1.0.1
      with:
        application-key: ${{ secrets.OVH_APPLICATION_KEY }}
        application-secret: ${{ secrets.OVH_APPLICATION_SECRET }}
        consumer-key: ${{ secrets.OVH_CONSUMER_KEY }}
        dns-zone: bruyant.xyz.xyz

Note that I’m once again using repository secrets to hide sensitive information.

The important part is that we’re updating a DNS TXT record that points to the CID of the website we’ve just pinned. We get the CID from the output of the previous pinning job.

Making our content accessible to the web

At that point, our content should be accessible to anyone on the web, either through their own IPFS node or through a public gateway:

ipfs get /ipns/${DNSLINK_ADDRESS}
https://ipfs.io/ipns/${DNSLINK_ADDRESS}

But that’s not very convenient. Ideally, we’d like anyone to be able to access our content, without having to install the CLI or having to use weird-looking URLs. That’s where private IPFS gateways come in.

IPFS Gateways come in many flavors. Some are public (https://ipfs.io/ipfs is one of them), some are private, most are read-only. We’re going to configure a site-specific gateway, meaning that this gateway will only serve content from our website.

Using the DNSLink record that we’ve just updated, the gateway will automatically retrieve data from the IPFS network, cache it, then serve it over HTTP.

I’m leaving the setting up of an internet-facing server as well as the configuration of a reverse proxy in front of an IPFS instance to you. There are too many options to count here. I’m currently running IPFS behind Traefik inside k3s, with Let’s Encrypt certificates.

The important part is that we have a running IPFS instance, whose included gateway is accessible from the internet (ideally using HTTPS). We’re going to take inspiration from a gateway recipe provided in official documentation, with a slight twist.

Run the following commands on your server:

# Disable DNSLink by default
$ ipfs config --json Gateway.NoDNSLink true
# Configure the gateway
$ ipfs config --json Gateway.PublicGateways '{
    "my-domain.xyz": {
      "NoDNSLink": false,
      "Paths": []
    }
  }'

Contrary to what’s presented in the link above, we do not disable the fetching of remote data. That’s because our content is primarily pinned on a third-party service, and is not present by default on the node.

With this setup, when our gateway receives a request, it resolves the DNSLink record to get the CID of our website. If the resolved CID is different from what’s present on the node, data will be fetched from the IPFS network (e.g. Pinata, or my laptop if it’s currently running). This solution slightly slows down the first request but simplifies the setup: we don’t have to open a connection from GitHub Actions to the IPFS node to directly pin the files.

Another solution would be to set up our own remote pinning service. This would be cleaner and more efficient, but a lot more work.

Conclusion

And we’re done! Your website is now online, and you can access it from anywhere. The IPFS Companion browser extension should automatically pick up that a DNSLink record is present and serve your website from your local node.

In this post, I’ve broadly covered the principles that I’ve followed to manage the hosting of my blog. I’ve walked you briefly through the prerequisites, as well as through the GitHub pipeline and the configuration of the HTTP gateway.

There’s much I haven’t covered, but I hope that you’ll find it useful. Feel free to reach out to me on Mastodon if you have any questions or comments (or leave a comment below if I ever come around to setting them up).