+ How to self-host Forgejo and automatically serve your web build + branches with SSL +
+My Very Own GitHub Pages
+ ++ 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
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.
+ 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/
+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: +
+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. +
+ + + +