Christopher Doyle

Migrating from GitHub Pages to Forgejo + Cloudflare Pages

Jul 25, 2026

I decided to migrate from GitHub pages to hosting the repository on a self-hosted instance of Forgejo, and hosting the site on Cloudflare pages. It is not hard to migrate but I found it hard because I am bad.

Aside: Forgejo + Nginx + Tailscale

Forgejo is easy to deploy. It is not essential to add Nginx Proxy Manager and Tailscale to the mix, but it does make it nicer. I set up DNS A records to point a custom domain name to the internal IP address of my Nginx Proxy Manager instance which listens on ports 80 and 443. It is then configured with LetsEncrypt for a wildcard certificate using DNS TXT challenge via a Cloudflare token. This will keep an updated SSL certificate on Nginx Proxy Manager without exposing anything to the internet. I then add a reverse proxy configuration for git.example.com to the internal IP and port of my Forgejo instance.

At this point, I can access https://git.example.com from my network, and hit my Forgejo instance with a HTTPS certificate that is automatically trusted.

To get this to work with Tailscale is a little more work because of how Tailscale internal IP addresses work. On a Tailscale instance on an internal network server, I add this argument to the command:

--advertise-routes=192.168.1.0/24

or equivalent, and configure sysctl for forwarding. On the client side I enable subnet routing. On the tailscale admin side, I approve the route. Then with Tailscale on, I can access the internal Forgejo instance with https://git.example.com and a valid HTTPS. Tidy.

Forgejo-Runner

Forgejo supports actions and runners, but the runner needs to run in a separate container (or indeed multiple containers). My needs are small, so I deploy it alongside Forgejo. I configure it to point to https://git.example.com and it is happy because the cert is a beautiful cert. I configure runner labels, including:

ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04

With this configuration one can say runs-on: ubuntu-latest and get a very similar host to what the same tag offered on GitHub gives. Indeed, my existing workflows required essentially zero changes.

Deployment Workflow

What did need changing was a deployment workflow, because it did not exist, and the jekyll configuration. In Gemfile, I remove “github-pages”, and replace it with “jekyll-feed”. I think it would work without “jekyll-feed” but I did want that. In the _config.yml, I add:

permalink: pretty
defaults:
  - scope:
      path: ""
      type: "posts"
    values:
      layout: "post"

The permalink setting provides that things like /about work, and not needing about.html. The defaults provides that things in _posts automatically get the layout of posts, otherwise they display without layout.

For the deployment workflow, I use the following:

on:
  workflow_dispatch:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Ruby
        run: apt-get update && apt-get install -y ruby ruby-dev

      - name: Install bundler
        run: gem install bundler

      - name: Generate static files w/ Jekyll
        run: |
          bundle install
          bundle exec jekyll build -s . -d _site/

      - name: Deploy to Cloudflare Pages
        run: npx wrangler@latest pages deploy ./_site --project-name=mywebsite-page
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

Of course this requires creating a Cloudflare Page, which is perfunctory, and creating a Cloudflare token scoped to read and edit Pages. The token details go into Forgejo project secrets, and everyone is happy.


← Back to all articles