# My Very Own GitHub Pages - joshua.seigler.net
How to self-host Forgejo and automatically serve your web build branches with SSL.
Keywords: posts, how to, technical, selfhosting

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` 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 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.