We have GitHub Pages at home
I don't have it in me to write a proper story with an intro, so here's the brief.
In late 2025, having been radicalized by YouTube commentary on AI and big tech (and my own Windows PC misbehaving on me!), I switched to Manjaro and later Arch Linux. No regrets after 6 months of daily driving Arch! Though, to be fair, most of what I'm using is KDE and Arch's incredibly well documented surface-level stuff.
At the same time, I started to move away from services like GitHub (for the same reason), but since my website was hosted on GitHub Pages, that meant I either had to swallow my pride, or self-host. After a few months filled with much thinking and very little trying, I finally got a working result, and I figured the best way to share and gloat is to write a tutorial-slash-retrospective.
Bear in mind that this tutorial is all over the place in terms of its verbosity, prerequisite knowledge, and maybe even accuracy - again, the main goal is to gloat.
Replace theleftexit in any commands/scripts that you see with your own identifiers.
Step 1 - Security
I have to self-host, which means I have to use SSH, which means I need an SSH key because the Internet says passwords are bad. Well, I say that SSH keys are also not perfect - you have to store a file on your PC and that file is basically your master key for all services where you imported your fingerprint. That makes it an easy target for an attack (it's just a file in your home folder), and an easy thing to lose (I've lost too much important stuff re-installing OS-es on a whim). But you know what's harder to steal and lose? A YubiKey.
Install libfido2 and yubico-authenticator, then insert your key, run Yubico Authenticator and initialize the FIDO2 (Passkeys) module with a PIN. Then open the terminal and type this:
$ ssh-keygen -O resident -t ed25519-sk -O user=theleftexit -O application=ssh:theleftexit
It might prompt you to enter the YubiKey PIN and set a passphrase for the key.
The functionally important bit is ssh-keygen -O resident -t ed25519-sk. The application option changes how the key is displayed in Yubico Authenticator, and the user option probably does something too (presumably it stores its value in the metadata, but I've never seen it show that value in plain text).
The key never leaves the device, and you can run ssh-keygen -K to import its handle (make it usable) on any machine. Also note the ~/.ssh/XXX.pub file - that's the public key you need to import on other machines/services to allow the YubiKey to authenticate you.
Further reading on SSH keys on physical authenticators: yubico.com, ArchWiki.
Step 2 - Setting up a VPS
Get a VPS! I'm using Hetzner, but I don't feel like writing a provider-specific tutorial. In whichever VPS you use, if it ever prompts you for an SSH key, paste the contents of the ~/.ssh/XXX.pub file.
If you're like me - used to the Arch ecosystem and don't want to leave it - ArchWiki has a nice article on how to quickly deploy Arch on a few different VPS providers: Arch Linux on a VPS.
For security, I added a regular user, imported my public key to ~/.ssh/authorized_keys, enabled passwordless sudo (since I don't have a password to authenticate myself with), and restricted sudo to interactive sessions.
Footnote: passwordless sudo is considered a security risk, but this setup limits the attack surface to just the commands that you execute. In other words, you still can't run rm -rf /* without sudo, but if you run a script that calls sudo rm -rf /*, calling that script will wipe your system without additional prompts. Probably. Either way, I'm not sharing the step-by-step, so you have the time to reconsider this setup while you're looking it up.
Step 3 - Setting up Git
On the server, create a new directory (reponame.git) in your regular user's home folder, cd into it and run git init --bare. Congratulations, you can now clone it if you run git clone yourusername@yourservername:reponame.git on your home machine. If I knew it was that easy, I never would've bothered with Forgejo.
Now for the secret sauce - create a post-update hook (~/reponame.git/hooks/post-update) with the following content:
TMPDIR=$(mktemp -d)
ORIGIN=$(pwd)
cd $TMPDIR
git clone $ORIGIN .
ls
cd
rm -rf $TMPDIR
Make sure to chmod +x it.
Now, whenever you push into this repository, it will run ls (or any number of commands!) in a temporary folder that contains the repository's files, and all output will be visible on the client machine. Here's what my post-update hook looks like:
TMPDIR=$(mktemp -d)
ORIGIN=$(pwd)
cd $TMPDIR
git clone $ORIGIN .
# My .NET static content generator outputs to the `publish` folder
dotnet run
# My Caddy serves from `~/publish`
rm -rf ~/publish
mv publish ~/publish
cd
rm -rf $TMPDIR
To seed the repository, run these commands in the folder that you wish to use as the repository root on the client:
git init
git add .
git commit -m "Initial commit"
git remote add origin yourusername@yourservername:reponame.git
git push --set-upstream origin master
If you've been nagged into changing the default branch to main by the message that shows up every time you run git init, make sure you change the default branch on both the server and the client, and replace master in the last line with main.
That's it - you pushed, it published, and hopefully your web server served it. Here's a Caddyfile if you don't feel like writing one:
yourDomain {
root yourPublishFolder
try_files {path}.html {path} {path}/index.html
handle_errors {
rewrite * /404.html
file_server
}
file_server
}