9.2 KiB
title | slug | description | draft |
---|---|---|---|
My Very Own GitHub Pages | my-very-own-github-pages | How to self-host Forgejo and automatically serve your web build branches with SSL | true |
I recently started self-hosting Forgejo, 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 and Caddy. 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) 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
andfail2ban
. In addition to allowing your custom SSL port, be sure to enable ports 80 and 443 withsudo 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. 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
[Unit]
Description=Small server for creating HTTP endpoints (hooks)
Documentation=https://github.com/adnanh/webhook/
ConditionPathExistsGlob=/etc/webhook.conf.d/*
[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:
sudo systemctl daemon-reload
Then you can remove the now-unused config file:
sudo rm /etc/webhook.conf
Here are the three hook definitions:
Create pages
/etc/webhook.conf.d/create-pages.conf
[
{
"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
[
{
"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
[
{
"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
#!/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
#!/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
#!/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:
~/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.
I'm also putting off rolling my own CI server, but I imagine that's the next stage here. Stay tuned.