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 objective
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 and 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 domain names and the email. Email could be removed, but it is there so that SSL certificate issuers 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
#!/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
cd "/var/www/$1";
git fetch origin gh-pages;
git reset --hard origin/gh-pages;
exit;
fi;
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;
[[ "$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
[
{
"id": "update-pages",
"execute-command": "/usr/bin/sudo",
"pass-arguments-to-command":
[
{ "source": "string", "name": "-u"},
{ "source": "string", "name": "joshua"},
{ "source": "string", "name": "/home/joshua/webhooks/update-pages.sh"},
{ "source": "payload", "name": "repository.name" },
{ "source": "payload", "name": "repository.clone_url" }
],
"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": "/usr/bin/sudo",
"pass-arguments-to-command":
[
{ "source": "string", "name": "-u"},
{ "source": "string", "name": "joshua"},
{ "source": "string", "name": "/home/joshua/webhooks/remove-pages.sh"},
{ "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!
Here is the marklink repo in my Forgejo instance and its contents on my Caddy server.
That repo is just HTML and JS with only a gh-pages branch, but for repos with npm build scripts, I use gh-pages @ npm to push the build to a 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.