diff --git a/site/posts/2025-06-15-replacing-github-pages.md b/site/posts/2025-06-15-replacing-github-pages.md new file mode 100644 index 0000000..2f229c6 --- /dev/null +++ b/site/posts/2025-06-15-replacing-github-pages.md @@ -0,0 +1,300 @@ +--- +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 + } +} + +omitted.webhooks.subdomain.tld { # <<<< change this + reverse_proxy localhost:9000 +} + +http://localhost { + handle /check { + root * /var/www + @deny not file /{query.domain}/ + respond @deny 404 + } +} + +https:// { + tls { + on_demand + } + root /var/www + rewrite /{host}{uri} + # Block files that start with a . + @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. +### Webhook +I altered the systemd service definition for `webhook` so I could organize the hook definitions into separate files. I also set `User=joshua` and `Group=joshua` so the commands run as my user instead of root. + +`sudo mkdir /etc/webhook.conf.d/` + +`/lib/systemd/system/webhook.service` +```ini +[Unit] +Description=Small server for creating HTTP endpoints (hooks) +Documentation=https://github.com/adnanh/webhook/ +ConditionPathExists=/etc/webhook.conf + +[Service] +ExecStart=/usr/bin/webhook -nopanic -hooks /etc/webhook.conf.d/*.conf + +User=joshua +Group=joshua + +[Install] +WantedBy=multi-user.target +``` + +If you are debugging your webhook output, consider adding `-debug` next to `-nopanic` for more useful logs. + +After changing the service definition, reload systemd to run the updated service: +``` bash +sudo systemctl daemon-reload +``` +Then you can remove the now-unused config file: +``` bash +sudo rm /etc/webhook.conf +``` +Here are the three hook definitions: +#### Create pages +`/etc/webhook.conf.d/create-pages.conf` +```json +[ + { + "id": "create-pages", + "execute-command": "/home/joshua/webhooks/create-pages.sh", + "command-working-directory": "/var/www", + "pass-arguments-to-command": + [ + { + "source": "payload", + "name": "repository.name" + }, + { + "source": "payload", + "name": "clone_url", + } + ], + "trigger-rule": + { + "and": + [ + { + "match": + { + "type": "payload-hmac-sha256", + "secret": "(omitted)", + "parameter": + { + "source": "header", + "name": "X-Forgejo-Signature" + } + } + } + ] + } + } +] +``` +#### Remove pages +`/etc/webhook.conf.d/remove-pages.conf` +```json +[ + { + "id": "remove-pages", + "execute-command": "/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" + } + } + } + ] + } + } +] +``` +#### Update pages +`/etc/webhook.conf.d/update-pages.conf` +```json +[ + { + "id": "update-pages", + "execute-command": "/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" + } + } + } + ] + } + } +] +``` + +In my home directory I defined all three hook scripts: + +`webhooks/create-pages.sh` +```bash +#!/bin/bash +# parameter 1 is repo name, parameter 2 is clone URL +[[ "$1" == *"/"* ]] && exit 1; # no slashes in the name +[[ "$1" == *".."* ]] && exit 1; # no .. in the name +[[ "$1" == *"*"* ]] && exit 1; # no wildcards in the name +[ -d "/var/www/$1" ] && exit 1; # the directory must not exist +cd "/var/www"; +git clone -b gh-pages --single-branch "$2" "$1" || exit 1; +``` + +`webhooks/remove-pages.sh` +```bash +#!/bin/bash +# parameter 1 is repo name +[[ "$1" == *"/"* ]] && exit 1; # no slashes in the name +[[ "$1" == *".."* ]] && exit 1; # no .. in the name +[[ "$1" == *"*"* ]] && exit 1; # no wildcards in the name +[ -d "/var/www/$1" ] && exit 1; # the directory must exist +cd "/var/www"; +rm -rf "/var/www/$1"; +``` +`webhooks/update-pages.sh` +```bash +#!/bin/bash +# parameter 1 is repo name +[[ "$1" == *"/"* ]] && exit 1; # no slashes in the name +[[ "$1" == *".."* ]] && exit 1; # no .. in the name +[[ "$1" == *"*"* ]] && exit 1; # no wildcards in the name +[ -d "/var/www/$1" ] || exit 1; # the directory must exist +cd "/var/www/$1"; +git fetch origin gh-pages; +git reset --hard origin/gh-pages; +exit; +``` +### Forgejo +Forgejo supports running webhooks conditionally triggered by certain conditions. +Under my main user settings I set up each webhook: +#### Create pages +Target URL: https:// _your domain here_ /hooks/create-pages +HTTP Method: `POST` (the default) +POST content type: `application/json` (the default) +Secret: _omitted, use your own_ +Trigger on: Custom Events > Create +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` +#### 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` + +## 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/ + +There is room to make the scripts a little smarter. They don't handle renaming very well right now, and a few times I had to log in and manually run my webhook scripts, like this: +```bash +~/webhooks/create-pages.sh marklink.pages.seigler.net "https://git.apps.seigler.net/joshua/marklink.pages.seigler.net.git" +``` +The really important thing is that updates just require pushing to `gh-pages` which you can easily do with e.g. [gh-pages @ npm](https://www.npmjs.com/package/gh-pages). + +I'm also putting off rolling my own CI server, but I imagine that's the next stage here. Stay tuned. \ No newline at end of file