article about my own github pages alternative

This commit is contained in:
Joshua Seigler 2025-06-15 01:11:15 -04:00
parent 78034d1de2
commit ccf85c1408

View file

@ -0,0 +1,220 @@
---
title: My Very Own GitHub Pages
slug: my-very-own-github-pages
description: How to self-host Forgejo and automatically serve your web build branches with SSL
---
I recently started self-hosting [Forgejo](https://forgejo.org/), but I wanted something to replace GitHub pages, which has been very convenient for publishing little website projects. My server runs Debian, so I decided to use [webhook](https://github.com/adnanh/webhook) and [Caddy](https://caddyserver.com/). I'm very happy how it turned out.
## The result
When I push a `gh-pages` branch to any public repository on my Forgejo instance, the name of the repo is used as a domain name (e.g. [marklink.pages.seigler.net](https://marklink.pages.seigler.net/)) and the branch contents are automatically served with SSL. If I push updates to the branch, they are automatically published. If the branch or repo is deleted, the site is taken down.
## How to do it
### Debian server preparation
In case you don't have a basic server setup routine yet, this is a good start:
- Change the default root password
- Create a new user, add it to the sudo group. In my examples below the user is `joshua`.
- Use `ssh-copy-id` to install your ssl pubkey for easier login
- Disable/lock root's password
- Disable root login over ssh and change the SSL port number. Pick a new port lower than 1024.
- Edit your local `~/.ssh/config` so you don't have to specify the port number every time you connect.
- On the server, install and enable `ufw` and `fail2ban`. In addition to allowing your custom SSL port, be sure to enable ports 80 and 443 with `sudo ufw allow "WWW Full"`.
### Caddy
I usually use nginx, but I wanted to give Caddy a shot, and it has been a great experience. I installed Caddy using the [official instructions](https://caddyserver.com/docs/install).
Here is the Caddyfile I made - you will need to change the domains names and the email. Email could be removed, but it is recommended so SSL certificate issues can contact you if there is a problem with your certificates.
`/etc/caddy/Caddyfile`
```
# Global options block
{
email you@example.com # <<<< CHANGE THIS <<<<
on_demand_tls {
ask http://localhost/check
}
}
# Webhooks
https://webhooks.subdomain.here.tld { <<<< CHANGE THIS <<<<
reverse_proxy localhost:9000
}
# Filter for which SSL certs we will create. Prevents abuse.
http://localhost {
handle /check {
root * /var/www
@deny not file /{query.domain}/
respond @deny 404
}
}
# This automatically handles upgrading http:// requests with a redirect
https:// {
tls {
on_demand
}
root /var/www
rewrite /{host}{uri}
@forbidden {
path /.*
}
respond @forbidden 404
file_server
}
# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfile
# This config based on information at
# https://caddy.community/t/on-demand-tls-with-dynamic-content-paths/18140
# checked and corrected with `caddy validate`
```
I also took ownership of `/var/www` with `chown -R joshua:joshua /var/www` since the webhooks will run as my login account.
### Webhooks
In my home directory I defined two hook scripts:
`~/webhooks/update-pages.sh`
```bash
#!/bin/bash
# parameter 1 is repo name, parameter 2 is clone url
[[ "$1" == *"/"* ]] && exit 1;
[[ "$1" == *".."* ]] && exit 1;
[[ "$1" == *"*"* ]] && exit 1;
if [ -d "/var/www/$1" ]; then
git clone -b gh-pages --single-branch "$2" "$1" || exit 1;
exit;
fi;
cd "/var/www/$1";
git fetch origin gh-pages;
git reset --hard origin/gh-pages;
exit;
```
`~/webhooks/remove-pages.sh`
```bash
#!/bin/bash
# parameter 1 is repo name
[[ "$1" == *"/"* ]] && exit 1;
[[ "$1" == *".."* ]] && exit 1;
[[ "$1" == *"*"* ]] && exit 1;
[ -d "/var/www/$1" ] || exit 1;
cd "/var/www";
rm -rf "/var/www/$1";
```
To trigger these hooks I am using `webhook` which is in the default Debian repository.
Here are the hook definitions: one for creating/updating a site, and one for deleting. You will need to generate one or two secret values that the server can use to know that the webhook is authorized to run. I used linux command `uuidgen -r` to create mine. Save these values so you can enter them in Forgejo later.
Also make sure to replace your execute-command lines with ones referencing your username and script paths.
`/etc/webhook.conf`
```json
[
{
"id": "update-pages",
"execute-command": "su joshua /home/joshua/webhooks/update-pages.sh",
"command-working-directory": "/var/www",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "repository.name"
},
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "payload-hmac-sha256",
"secret": "(omitted)",
"parameter":
{
"source": "header",
"name": "X-Forgejo-Signature"
}
}
},
{
"match":
{
"type": "value",
"value": "refs/heads/gh-pages",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
]
}
},
{
"id": "remove-pages",
"execute-command": "su joshua /home/joshua/webhooks/remove-pages.sh",
"command-working-directory": "/var/www",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "repository.name"
},
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "payload-hmac-sha256",
"secret": "(omitted)",
"parameter":
{
"source": "header",
"name": "X-Forgejo-Signature"
}
}
}
]
}
}
]
```
### Forgejo
Forgejo supports running webhooks conditionally triggered by certain conditions.
Under my main user settings I set up each webhook:
#### Update pages
Target URL: https:// _your domain here_ /hooks/update-pages
HTTP Method: `POST` (the default)
POST content type: `application/json` (the default)
Secret: _omitted, use your own_
Trigger on: Push events
Branch filter: `gh-pages`
#### Remove pages
Target URL: https:// _your domain here_ /hooks/remove-pages
HTTP Method: `POST` (the default)
POST content type: `application/json` (the default)
Secret: _omitted, use your own_
Trigger on: Custom Events > Repository > Delete
Branch filter: `gh-pages`
## Conclusion
It works!
This repo is in my Forgejo instance: https://git.apps.seigler.net/joshua/marklink.pages.seigler.net
And its contents are visible here on my Caddy server: https://marklink.pages.seigler.net/
For repos with npm build scripts, I use [gh-pages @ npm](https://www.npmjs.com/package/gh-pages) to push the build to the gh-pages branch and up to the server.
I'm putting off rolling my own CI server, but I imagine that's the next stage here. Stay tuned.