A few years ago I wrote about deploying a Hugo website to Firebase using Github Actions. That setup worked, but my website was tied to a Google account, and I never liked depending on a free tier that Google can change whenever it wants. So I moved ldej.nl to statichost.eu. It runs on European infrastructure, and unlike most “European” hosting it is a European company rather than a European region of an American cloud.
The documentation is good, but it describes one thing at a time. Putting it together took me an afternoon, and two traps took most of that afternoon. This is the guide I would have liked to read.
How statichost works
statichost has two parts, and it is worth knowing which one you are talking to. A builder clones your git repository, builds your site in a Docker container that you choose, and publishes the output directory. An edge network then serves those files, with a certificate per domain.
There is no integration that builds on push unless you register a webhook yourself. That is the first trap, and there is a section about it below.
Your site also gets a managed domain, <site-name>.statichost.page, and that name has to be unique among all their customers. Mine is ldej.statichost.page, which is what ldej.nl now points at.
Connect the repository
In the dashboard you add a site and connect a repository.
If your repository is public, use its HTTPS URL, because cloning then needs no credentials: https://github.com/you/your-site.git. If it is private, use the SSH URL and paste the site’s public key into your repository as a read-only deploy key. I used SSH, because that is what my local remote already was, and it works fine as long as you do not forget the key.
Submodules are cloned for you
Tell the builder how to build
Build configuration lives in a statichost.yml file in the root of your repository:
| |
All three keys are optional. image is any public Docker image, built for linux/amd64. Leave it out and nothing is built, so the public directory is published as it is, which is useful if you build in your own CI. command runs inside that container, with your repository at /repo as the working directory, through /bin/sh, so && works. public is the directory to publish.
Pin your build image
hugo-version: latest in 2020. By the time I pushed again, the latest Hugo had removed a field that my theme’s feed template used, so that push built nothing and deployed nothing, and the failure was only visible if you went looking for it. Pinning hugomods/hugo:0.165.0 keeps a build reproducible months later, and when it breaks you know who to blame.You can reproduce what the builder does on your own machine, which is the fastest way to iterate:
| |
Trigger a build
Connecting a repository does not make statichost build on push. You have to send a webhook:
| |
You can register that URL as a webhook in your git provider. I put it in the repository instead, where it is versioned and where it is obvious which branch deploys:
| |
The last lines exist because of the lesson above. If the response is not the one you expect, the workflow fails. A deploy that does not happen is worse than one that fails loudly.
Locking down the webhook
Point your domain at it
Add the domain in the site settings, then change your DNS. Do it in that order, because the certificate is issued on the first request that arrives with your domain.
www.example.orggets aCNAMErecord pointing atyour-site-name.statichost.page.example.orggets anALIASorANAMErecord pointing at the same name, if your DNS provider has one, because a root domain cannot be aCNAME.
If your DNS provider has no ALIAS
95.217.26.94 and an AAAA record to 2a01:4f9:c01f:8002::. HTTPS and IPv6 both work, but unlike an ALIAS you are the one who has to notice if those addresses change.While you are editing DNS, leave alone everything that is not a web record: your MX, the SPF and verification TXT records, _dmarc, and the autodiscover and autoconfig entries that mail clients use. Moving a website should not move an inbox. Use a TTL of 300 seconds for the first day, so a mistake is cheap to undo, and raise it afterwards.
Verify the deploy
Two commands tell you whether statichost is really serving the site:
| |
Caching can make a good deploy look broken
Pages come back with Cache-Control: public, max-age=0, must-revalidate, but their edge can still hand you a stale copy, and adding ?v=123 to the URL does not reliably bust it. This is what made me conclude that a working pipeline was broken. Use the request header instead, and give a build a few minutes before you give up on it:
| |
Keep the mirror out of search results
Your site stays reachable on your-site-name.statichost.page after you add your own domain, so every page exists twice. There is no way to mark only the mirror as noindex with a header, because the _headers file matches paths and not hosts, which means an X-Robots-Tag: noindex there would also deindex the real domain.
A canonical URL is the supported answer for this. Check that yours is absolute, so that the mirror points search engines at the real domain:
| |
My own meta.html had {{ .RelPermalink }} there, which produces ../about/, so the mirror was telling search engines that the mirror was the original.
What modern Hugo broke in my 2020 setup
I did not expect to write this section. Moving my build to a current Hugo broke a few things, and a successful build is no proof that the pages exist.
- A root-level page such as
content/about.mdno longer has its templatetypedefaulted topage, so it stopped matchinglayouts/page/single.htmland disappeared frompublic/, while still being listed in the sitemap. Addingtype: "page"to the front matter fixed it. Compare your URL set against the live site after every upgrade. .Site.Authorwas removed, which broke the feed template in my theme. I override it from my ownlayouts/rss.xmlinstead of patching a submodule.[indexes]became[taxonomies]and changed which taxonomy pages were built, so an empty/categories/disappeared.languageCodebecamelocale..Site.Databecamehugo.Data, which on older Hugo drops the page that reads it without an error.
Conclusion
Statichost is a smaller product than Firebase, and that is why I like it. There is no project to create, no interview with firebase init, and no analytics or database services tempting me to couple my website to a platform. It is a git repository, a Docker image, and a directory to publish.
What you have to do yourself is the trigger and the DNS, and then check both. That is one afternoon, once you know that nothing builds on push until you ask it to, and that a stale cache can make a working pipeline look dead.
This website is the example. The source is github.com/ldej/ldej-nl, with the build configuration in statichost.yml and the deploy trigger in .github/workflows/deploy-statichost.yml.