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
}
}
# 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
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
#!/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": "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 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.