article about my own github pages alternative
This commit is contained in:
parent
78034d1de2
commit
ed692e5587
1 changed files with 300 additions and 0 deletions
300
site/posts/2025-06-15-replacing-github-pages.md
Normal file
300
site/posts/2025-06-15-replacing-github-pages.md
Normal file
|
@ -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.
|
Loading…
Add table
Add a link
Reference in a new issue