December Adventure Day 22
Infrastructure and Site Design
Having set an intention to wrap up some projects and focus on more retro-related explorations, I immediately found myself distracted by other things on day 22 of my December Adventure: I’ve been thinking a lot about the seemingly bleak future of GitHub and am keen to start experimenting with Forgejo locally with a view to potentially maintaining a self-hosted instance. In preparation for this, I decided to finally try to wrap my head around using Let’s Encrypt to generate SSL certificates on machines that aren’t Internet-facing—something I’d like for self-hosted infrastructure as well as using Cloudflare to proxy my this website. I also started putting together a website for my partner to showcase her art.
Caddy, Let’s Encrypt, and Cloudflare
Some time ago, I switched to using Caddy for hosting this website, projects like Anytime, StatusPanel, and WriteMe, and for local bits of infrastructure. I was sold by the dream of built-in, auto-provisioning SSL using Let’s Encrypt, and it worked well. However, one thing I’ve never taken the time to set up is out-of-band challenges like DNS-01 that allow me to take advantage of things like Cloudflare’s proxy, or generate SSL certificates for private local infrastructure.
The DNS-01 challenge works by writing a DNS TXT record as a proof of ownership. Enabling this in Caddy is a simple matter of specifying the dns cloudflare challenge. For this website, the whole configuration looks like this:
jbmorley.co.uk {
root * /var/www/jbmorley.co.uk
encode zstd gzip
file_server {
index index.html index.json
}
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}
As you would expect from Caddy, this is wonderfully simple, and my hope was that I could use it for both my public services as well as my personal infrastructure, which I would achieve by adding local DNS entries for valid jbmorley.co.uk subdomains. Unfortunately, it turns out the whole thing is significantly more complex: the version of Caddy packaged with Ubuntu doesn’t include any of the DNS challenge plugins and, since Caddy’s plugins are compiled-in, you can’t just install additional package.
I ended up spending much of the day trying to come up with an approach to this, ultimately settling on a community maintained Docker image which builds-in the Cloudflare DNS-01 plugin. My docker-compose.yml for this is as follows:
services:
caddy:
image: ghcr.io/caddybuilds/caddy-cloudflare:latest
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
volumes:
- /etc/caddy:/etc/caddy
- /var/www:/var/www
- caddy_data:/data
- caddy_config:/config
environment:
- CLOUDFLARE_API_TOKEN=...
volumes:
caddy_data:
external: true
caddy_config:
This configuration is slightly tweaked from the one in the project README: it specifies network_mode: host to allow Caddy to see host ports provided by other apps and Docker containers, rather than requiring me to manage network interfaces between Docker containers.
While it ‘works’, deploying Caddy like this concerns me as it introduces another component with a security update life cycle independent of the host OS, and exposes a critical part of my infrastructure to third-party management and build infrastructure. With that in mind, although I switched my staging server (which hosts this website) over to this new approach, I decided to sleep on it and take stock in the morning.
Website Design
In the afternoon, I took some time to work on a lightweight website to allow my partner Sarah to showcase her paintings. This gave me a nice excuse to spend a little more time with Hugo, which I last used to set up the Psion community website.
Keeping things simple, we decided to use a grid for the home page. Iterating over all the pages in a section is easy to do in Hugo templates and, while I still prefer the InContext1 approach to multimedia for more involved sites, it’s great to be able to specify image dimensions directly in templates:
{{ define "main" }}
{{ .Content }}
<div class="gallery-grid">
{{ range (where .Site.Pages "Section" "art") }}
{{ range .Pages }}
<div class="gallery-item">
{{ if .Params.thumbnail }}
{{ $image := .Resources.Get .Params.thumbnail }}
<a href="{{ .RelPermalink }}">
<img src="{{ ($image.Fit "600x600").RelPermalink }}" alt="{{ .Title }}" title="{{ .Title }}">
</a>
{{ else }}
Missing Thumbnail
{{ end }}
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
This template iterates over all the top-level pages in the ‘art’ section and, for each page, generates a thumbnail matching the one specified in the that page’s Frontmatter. I was briefly caught out by Hugo’s need to differentiate between branch and leaf pages / bundles (branches should be called _index.md, and leaves index.md), but I got there in the end. The corresponding directory structure looks like:
content
├── _index.md
├── about
│ └── index.md
├── art
│ ├── _index.md
│ ├── basil-and-maverick
│ │ ├── basmav.png
│ │ └── index.md
│ ├── kale-flower
│ │ ├── index.md
│ │ └── preview.jpg
│ ├── manu-o-ku
│ │ ├── index.md
│ │ └── manuoku.png
│ ├── na-pali
│ │ ├── index.md
│ │ └── na-pali.png
│ ├── passion-orange-guava
│ │ ├── index.md
│ │ └── pog.png
│ └── red-flower
│ ├── index.md
│ └── preview.jpg
└── contact
└── index.md
With this this template in place, it’s now a matter of slowly fleshing the content out and cleaning up the layout. We’re going to have to be especially careful when generating preview images as paintings can look incredibly flat with the wrong color profiles. I’ve also noticed that some of the built-in macOS apps aren’t able to correctly render the high-resolution TIFF scans we have.
-
My personal static site builder. ↩