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.