<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://chameth.com/">
    <title>Chameth.com - posts like artisanal-docker-images, just-a-nod but not debugging-beyond-the-debugger, migrating-from-github-to-forgejo, why-you-should-be-using-https</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/artisanal-docker-images,just-a-nod/unlike/debugging-beyond-the-debugger,migrating-from-github-to-forgejo,why-you-should-be-using-https/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2026-03-01T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <entry>
        <title>Monthly Meanderings: February 2026</title>
        <link href="https://chameth.com/monthly-meanderings-2026-02/"/>
        <updated>2026-03-01T00:00:00Z</updated>
        <id>https://chameth.com/monthly-meanderings-2026-02/</id>
        <content xml:lang="en" type="html">&lt;p&gt;It doesn’t feel like a whole month has gone past since I wrote &lt;a href=&#34;https://chameth.com/monthly-meanderings-2026-01/&#34;&gt;the last instalment of Monthly Meanderings&lt;/a&gt;, even allowing for how short a month February is. For more context on this series, you can check out &lt;a href=&#34;https://chameth.com/monthly-meanderings-2025-12/&#34;&gt;the introduction to the first edition&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;website-updates&#34;&gt;Website updates&lt;/h3&gt;
&lt;p&gt;I only wrote one new blog post this month: &lt;a href=&#34;https://chameth.com/just-a-nod/&#34;&gt;Just a nod&lt;/a&gt;, which is about the “nod” button I added to the bottom of most pages. I did a lot of other little bits and pieces, though:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My blog posts are now automatically posted to &lt;a href=&#34;https://bsky.app/profile/posts.chameth.com&#34;&gt;a dedicated Bluesky account&lt;/a&gt;, and posts now include syndication links at the bottom.&lt;/li&gt;
&lt;li&gt;The site now tracks how long each request takes to generate along with how many SQL queries were used. You can see the details for the current request by hovering/tapping over the pi symbol at the bottom right of a page. To understand why it’s a pi symbol, you should watch &lt;a href=&#34;https://chameth.com/films/the-net-1995/&#34;&gt;The Net&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;Speaking of films, I redid the star rating rendering so instead of unicode stars it uses some hand-drawn pixel art stickers. They’re somewhat randomised, so they don’t look repetitive. This is what it now looks like:
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
.&lt;/li&gt;
&lt;li&gt;I spent some time tweaking various styles and layouts so everything works nicely on smaller screens. I try to make sure these things work when I make them, but it’s easy to miss a particular interaction, or make an innocent-seeming change that breaks tiny screens.&lt;/li&gt;
&lt;li&gt;I added a new page that shows stats about &lt;a href=&#34;https://chameth.com/walks/&#34;&gt;my walks&lt;/a&gt;, based on my Apple Health data. I had a private view of this for a while, but decided there’s no reason it couldn’t be more public.&lt;/li&gt;
&lt;li&gt;The &lt;a href=&#34;https://chameth.com/films/&#34;&gt;films page&lt;/a&gt; has gained a search box, to directly find my review of a particular film. I used &lt;a href=&#34;https://css-tricks.com/css-anchor-positioning-guide/&#34;&gt;CSS anchor positioning&lt;/a&gt; for the results panel. Modern CSS is fun!&lt;/li&gt;
&lt;li&gt;I fixed the &lt;a href=&#34;https://chameth.com/prints/&#34;&gt;3D prints&lt;/a&gt; page being utterly broken in numerous ways. It showed up on my shiny new “how many SQL queries does each page do?” dashboard, as it was doing multiple queries for each individual print instead of just getting all the data in one go. Then I noticed a badly written redirect rule had broken all the images and download links. Whoops!&lt;/li&gt;
&lt;li&gt;I did a lot of under-the-hood cleaning, most of which won’t be visible. Besides just making me happy, this means the contact form and nod buttons now work with Javascript disabled.&lt;/li&gt;
&lt;li&gt;Finally, I added a page that shows my &lt;a href=&#34;https://chameth.com/boardgames/&#34;&gt;board game plays&lt;/a&gt;. The data comes from exports from &lt;a href=&#34;https://www.bgstatsapp.com/&#34;&gt;bgstats&lt;/a&gt;. I have a bunch of ideas for other things to do with that data, but haven’t got around to it yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Phew. After writing all that out it feels like a really long month, not a short one!&lt;/p&gt;
&lt;h3 id=&#34;other-projects&#34;&gt;Other projects&lt;/h3&gt;
&lt;p&gt;Not much going on with my other projects. The only big thing was &lt;a href=&#34;https://github.com/csmith/goplum&#34;&gt;goplum&lt;/a&gt;, my self-hosted monitoring/alerting tool, getting a 1.0 release. It adds a reminder feature so you can get alerted repeatedly if a service is down. It also reworks how plugins work so they’re compile-time instead of trying to be clever and dynamic, as that never really gave any benefits.&lt;/p&gt;
&lt;p&gt;I’m still continuing my migration of projects to a private &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt; instance. All of my personal repos are now sourced there, and mirrored out to GitHub, except one: my &lt;a href=&#34;https://github.com/csmith/dockerfiles&#34;&gt;Dockerfiles&lt;/a&gt;. I’m not sure how I want to approach that yet, as the action that does the updating is pretty involved. I also have some repositories split off into their own GitHub organisations which I haven’t got around to mirroring yet.&lt;/p&gt;
&lt;h3 id=&#34;entertainment&#34;&gt;Entertainment&lt;/h3&gt;
&lt;p&gt;Unlike the past few months, I didn’t touch many video games in February. The ones I’d been dabbling with didn’t really hook me enough to make me keep going back. I made up for it with lots of other media forms though. First up, a whole bunch of films:&lt;/p&gt;
&lt;ol class=&#34;watched-films&#34;&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/black-swan-2010/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/242/poster.jpg&#34; alt=&#34;Poster of Black Swan&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-02&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/the-net-1995/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/243/poster.jpg&#34; alt=&#34;Poster of The Net&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-03&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/office-space-1999/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/244/poster.jpg&#34; alt=&#34;Poster of Office Space&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-04&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/the-silence-of-the-lambs-1991/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/245/poster.jpg&#34; alt=&#34;Poster of The Silence of the Lambs&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-07&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/big-night-1996/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/246/poster.jpg&#34; alt=&#34;Poster of Big Night&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-10&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/the-cabin-in-the-woods-2012/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/247/poster.jpg&#34; alt=&#34;Poster of The Cabin in the Woods&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-11&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/knock-at-the-cabin-2023/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/248/poster.jpg&#34; alt=&#34;Poster of Knock at the Cabin&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-12&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/the-faculty-1998/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/249/poster.jpg&#34; alt=&#34;Poster of The Faculty&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-13&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/final-destination-2000/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/250/poster.jpg&#34; alt=&#34;Poster of Final Destination&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-13&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/get-out-2017/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/251/poster.jpg&#34; alt=&#34;Poster of Get Out&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-14&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/no-country-for-old-men-2007/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/252/poster.jpg&#34; alt=&#34;Poster of No Country for Old Men&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-17&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I won’t write too much about them here — you can click through to read my reviews if you want — other than to call out the three excellent horror films I hadn’t seen before: &lt;a href=&#34;https://chameth.com/films/black-swan-2010/&#34;&gt;Black Swan&lt;/a&gt;, &lt;a href=&#34;https://chameth.com/films/the-cabin-in-the-woods-2012/&#34;&gt;The Cabin in the Woods&lt;/a&gt;, and &lt;a href=&#34;https://chameth.com/films/get-out-2017/&#34;&gt;Get Out&lt;/a&gt;. Not so long ago I would’ve said that I didn’t enjoy horror films, now they’re some my favourites.&lt;/p&gt;
&lt;p&gt;I don’t have any TV show tracking on my website (yet?), as I haven’t really watched much recently. This month I started watching &lt;a href=&#34;https://www.themoviedb.org/tv/250307-the-pitt&#34;&gt;The Pitt&lt;/a&gt; though, and it rapidly become one of my favourite shows. By “started watching” I mean “watched every aired episode of both season 1 and season 2”. I highly recommend it. The only problem is that it’s hard to find anything to watch afterwards, as nothing else can quite measure up.&lt;/p&gt;
&lt;p&gt;I’ve also been binging a book series: &lt;a href=&#34;https://www.amazon.co.uk/dp/B0DT7G27HH&#34;&gt;Beneath the Dragoneye Moons&lt;/a&gt;. It’s a LitRPG series: basically a fantasy setting that borrows systems from an RPG, like classes and levelling. Something about the genre really works for me, and Beneath the Dragoneye Moons is one of the more interesting series I’ve come across in the genre. I’ve been going through the series at the rate of about a book a day.&lt;/p&gt;
&lt;p&gt;Finally, board games! Nothing particularly notable, but now I can embed a pretty display of what I actually played:&lt;/p&gt;
&lt;ul class=&#34;played-boardgames&#34;&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Can&amp;#39;t Stop (1980)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/41/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/41/image.png&#34; alt=&#34;Box art of Can&amp;#39;t Stop&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;6 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Cartographers (2019)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/263918/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/263918/image.png&#34; alt=&#34;Box art of Cartographers&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;4 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;My Shelfie: The Dice Game (2024)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/411543/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/411543/image.jpg&#34; alt=&#34;Box art of My Shelfie: The Dice Game&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;4 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Super Mega Lucky Box (2021)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/341530/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/341530/image.png&#34; alt=&#34;Box art of Super Mega Lucky Box&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;4 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;7 Wonders Dice (2025)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/446231/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/446231/image.jpg&#34; alt=&#34;Box art of 7 Wonders Dice&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Let&amp;#39;s Go! To Japan (2024)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/368173/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/368173/image.jpg&#34; alt=&#34;Box art of Let&amp;#39;s Go! To Japan&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Martian Dice (2011)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/99875/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/99875/image.png&#34; alt=&#34;Box art of Martian Dice&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Pickomino (2005)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/15818/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/15818/image.jpg&#34; alt=&#34;Box art of Pickomino&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Space Base (2018)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/242302/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/242302/image.jpg&#34; alt=&#34;Box art of Space Base&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Terraforming Mars (2016)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/167791/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/167791/image.jpg&#34; alt=&#34;Box art of Terraforming Mars&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Azul: Summer Pavilion (2019)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/287954/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/287954/image.jpg&#34; alt=&#34;Box art of Azul: Summer Pavilion&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Booty Dice (2023)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/408658/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/408658/image.png&#34; alt=&#34;Box art of Booty Dice&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Castles of Mad King Ludwig (2014)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/155426/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/155426/image.jpg&#34; alt=&#34;Box art of Castles of Mad King Ludwig&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Coffee Rush (2023)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/377061/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/377061/image.jpg&#34; alt=&#34;Box art of Coffee Rush&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Flip 7 (2024)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/420087/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/420087/image.jpg&#34; alt=&#34;Box art of Flip 7&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Fluxx (1997)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/258/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/258/image.jpg&#34; alt=&#34;Box art of Fluxx&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Lodge (2026)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/452300/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/452300/image.jpg&#34; alt=&#34;Box art of Lodge&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Next Station: Paris (2024)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/403116/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/403116/image.jpg&#34; alt=&#34;Box art of Next Station: Paris&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Next Station: Tokyo (2023)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/380165/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/380165/image.jpg&#34; alt=&#34;Box art of Next Station: Tokyo&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Obsession (2018)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/231733/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/231733/image.png&#34; alt=&#34;Box art of Obsession&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Ticket to Ride (2004)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/9209/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/9209/image.jpg&#34; alt=&#34;Box art of Ticket to Ride&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these were online via &lt;a href=&#34;https://boardgamearena.com/&#34;&gt;Board Game Arena&lt;/a&gt;. I haven’t really had time for in-person games with all the TV, films and reading!&lt;/p&gt;
&lt;h3 id=&#34;around-the-web&#34;&gt;Around the web&lt;/h3&gt;
&lt;h4 id=&#34;start-all-of-your-commands-with-a-commahttpsrhodesmillorgbrandon2009commands-with-comma&#34;&gt;&lt;a href=&#34;https://rhodesmill.org/brandon/2009/commands-with-comma/&#34;&gt;Start all of your commands with a comma&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;An old post that was doing the rounds again on Hacker News. It’s an interesting idea. I recently aliased &lt;code&gt;cc&lt;/code&gt; to launch Claude Code with some custom parameters, and it horribly backfired when I tried to compile a C program and remembered that &lt;code&gt;cc&lt;/code&gt; was part of GCC. I haven’t transitioned all my personal commands to use a prefix yet, but I might.&lt;/p&gt;
&lt;h4 id=&#34;tactical-tornado-is-the-new-defaulthttpsolanodevblogtactical-tornado&#34;&gt;&lt;a href=&#34;https://olano.dev/blog/tactical-tornado/&#34;&gt;Tactical tornado is the new default&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;An interesting post about software developers who act as “tactical tornadoes”, and how that’s basically the role that all LLMs fill. Whenever I use coding agents, I often find myself following behind them in my IDE cleaning up bits and pieces. It’s often faster than prompting them to do it. I definitely get the “cleaning up after a tornado” metaphor.&lt;/p&gt;
&lt;p&gt;Amusingly, the agents will sometimes notice that you’re tidying up after them and ascribe it to an automated process, outputting something like “the automated linter hasn’t run yet” if you fall behind. There’s probably a deeper philosophical discussion about what it means when the agent thinks that the human is the automated part of the system…&lt;/p&gt;
&lt;h4 id=&#34;my-smart-sleep-mask-broadcasts-users-brainwaves-to-an-open-mqtt-brokerhttpsaimiliosbearblogdevreverse-engineering-sleep-mask&#34;&gt;&lt;a href=&#34;https://aimilios.bearblog.dev/reverse-engineering-sleep-mask/&#34;&gt;My smart sleep mask broadcasts users’ brainwaves to an open MQTT broker&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Another one for the “the ’s’ in ‘IoT’ stands for security” books. I find it hard to actually comprehend how these things get to market. Does nobody actually think about it, or do they know full well how bad it is and just launch it anyway?&lt;/p&gt;
&lt;h4 id=&#34;the-indieweb-and-small-webhttpschristianodevpostindieweb-smallweb&#34;&gt;&lt;a href=&#34;https://christiano.dev/post/indieweb_smallweb/&#34;&gt;The IndieWeb and Small web&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Ask three people to define ‘IndieWeb’ and ‘Small web’ and you’ll get about 20 different answers. This post by Christiano almost exactly nails my own views on the terms, and explains them well. I think most personal sites (my own included) straddle both terms to some extent, but having the labels is useful in discussions.&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Just a nod</title>
        <link href="https://chameth.com/just-a-nod/"/>
        <updated>2026-02-11T00:00:00Z</updated>
        <id>https://chameth.com/just-a-nod/</id>
        <content xml:lang="en" type="html">&lt;p&gt;Last month I added a new feature to the bottom of almost all pages on this site: a “nod” button. It’s a bit like the upvote buttons you see on &lt;a href=&#34;https://bearblog.dev/discover/&#34;&gt;bear blog posts&lt;/a&gt;, or the ubiquitous like button you get on social media, with one key difference: it doesn’t track or count anything. Before I get into the whys and hows, this is what it currently looks like:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/just-a-nod/nod.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/just-a-nod/nod.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/just-a-nod/nod.png&#34; alt=&#34;Screenshot of the &amp;#39;nod&amp;#39; button on chameth.com. A blue rectangle with rounded corners, with an arcade button on the left with the caption &amp;#39;nod&amp;#39;, and handwritten text alongside it reading &amp;#39;Liked this page? Just want to declare that you reached the end? Like pressing buttons? Give me a nod to let me know you were here. No tracking, no counters, etc, just a nod in passing.&amp;#39;.&#34; loading=&#34;lazy&#34; width=&#34;811&#34; height=&#34;180&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The nod button in all its glory. If you want a clickable version you’ll need to scroll a bit more.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So why bother? I like hearing from people who have looked at my website. I have a &lt;a href=&#34;https://chameth.com/contact/&#34;&gt;contact form&lt;/a&gt; that’s also embedded at the bottom of all my blog posts; it’s always a delight when people reach out. Actually writing a message is a pretty high bar that most visitors won’t clear, though. A like/upvote/whatever-you-want-to-call-it is a much less demanding interaction.&lt;/p&gt;
&lt;p&gt;And why no counting? I’d been thinking about some kind of upvote button for a while, and counting was always the thing that put me off. Having a number attached to a post like that feels like I’d end up invoking &lt;a href=&#34;https://en.wikipedia.org/wiki/Goodhart%27s_law&#34;&gt;Goodhart’s law&lt;/a&gt; and start &lt;em&gt;aiming&lt;/em&gt; for a certain number of upvotes or likes or whatever. I don’t want to be anxiously watching numbers, or feel pressured to make a post “good enough” (I already &lt;a href=&#34;https://chameth.com/blogging-and-the-imaginary-quality-bar/&#34;&gt;self-filter too much&lt;/a&gt;…).&lt;/p&gt;
&lt;p&gt;As for not tracking people, that feels like table stakes to me: I’m an individual concerned about privacy, not a giant mega-corporation who wants to slurp up all the data to serve ads or engagement bait feeds. I don’t really need to know someone’s identity to receive a message from them. In the olden days of the internet, you just knew everyone by screen names which could change on a whim and didn’t reveal anything about the real you. On the internet, nobody knows you’re a dog, as &lt;a href=&#34;https://en.wikipedia.org/wiki/On_the_Internet,_nobody_knows_you%27re_a_dog&#34;&gt;Peter Steiner said&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All these constraints felt insurmountable for a while. I considered just making a button that sent a contact form submission with a pre-filled message, but those end up in my e-mail inbox. I don’t really want to have to process them, especially if there’s a big traffic spike like when &lt;a href=&#34;https://news.ycombinator.com/&#34;&gt;Hacker News&lt;/a&gt; notices one of my posts. I could store the button presses in a database, but then how would I actually know they happened, especially if I didn’t want to watch a number go up?&lt;/p&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/just-a-nod/amazon-dash.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/just-a-nod/amazon-dash.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/just-a-nod/amazon-dash.png&#34; alt=&#34;An Amazon Dash button with the &amp;#39;Finish&amp;#39; detergent logo on the front. It&amp;#39;s a thin rectangle with rounded corners, with a circular button inset on the right-hand side.&#34; loading=&#34;lazy&#34; width=&#34;356&#34; height=&#34;265&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;An Amazon Dash button&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So I did what I usually do with these types of problems: ignored it for a bit. The idea rattles around at the back of my head, and sometimes I’ll come up with an answer. I’ve seen this called &lt;a href=&#34;https://fs.blog/focused-diffuse-thinking/&#34;&gt;diffuse thinking&lt;/a&gt;. Eventually the idea bounced to the forefront of my head while I was looking at my IRC client, and the solution suddenly clicked. If you’ve not heard of it, IRC is a chat protocol that’s almost as old as I am. &lt;a href=&#34;https://daniel.haxx.se/irchistory.html&#34;&gt;Daniel Stenberg has compiled a nice history of IRC&lt;/a&gt; that I won’t repeat here. I use IRC to stay in touch with friends, hang around the channels for some open source projects, and I also run a private server with just me and some notification bots.&lt;/p&gt;
&lt;p&gt;The notifications I have on IRC are all “transient”: they’re handy to see in the moment, but it doesn’t matter if I miss them. I have some high-volume news feeds, log messages from some self-hosted services, details about git commits, and so on. Basically just background noise of things that are happening that don’t need action. Perfect for the kind of interaction I want!&lt;/p&gt;
&lt;p&gt;Now that I’d finally figured out that the transient-notification-shape goes in the transient-notification-hole, I just needed to actually implement it. As part of that private IRC setup I already have a bot that accepts messages over HTTP and sends them to a channel, so the backend was pretty simple. For the frontend I was imagining something like the ill-fated Amazon Dash buttons. My first attempt was… underwhelming:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/just-a-nod/original-nod.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/just-a-nod/original-nod.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/just-a-nod/original-nod.png&#34; alt=&#34;Screenshot of the initial nod button on chameth.com: a flat blue rectangle with a flat circle inset.&#34; loading=&#34;lazy&#34; width=&#34;800&#34; height=&#34;181&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Flat UIs are in right now, right?… Right?&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I guess I shouldn’t give up the day job to become a graphic designer. Fortunately, I came across &lt;a href=&#34;https://codepen.io/thierrymichel/pen/Pwzbmd&#34;&gt;this codepen&lt;/a&gt; from Thierry Michel with some very fun skeuomorphic arcade buttons. I spent some time and LLM tokens on converting the SCSS to CSS, changing the colour, and making a few other tweaks. I really love how this looks and feels.&lt;/p&gt;
&lt;p&gt;This is what it looks like in my IRC client when someone nods (and when containers get pushed to the GitHub Container Registry for my projects…):&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/just-a-nod/irc-nods.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/just-a-nod/irc-nods.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/just-a-nod/irc-nods.png&#34; alt=&#34;Screenshot of an IRC client, showing four lines from a user called &amp;#39;ircjag&amp;#39;. Two are prefixed with &amp;#39;[GHCR]&amp;#39; in magenta, and report containers being pushed for projects on GitHub. Interleaved are two prefixed with &amp;#39;[CHAMETH.COM]&amp;#39; in teal that report &amp;#39;Someone nodded at&amp;#39; and then a URL. The bottom-most is a link to the blog post titled &amp;#39;An app can be a ready meal&amp;#39; and has a preview image rendered in a grey box below it.&#34; loading=&#34;lazy&#34; width=&#34;774&#34; height=&#34;370&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Receiving some nods on IRC via a bot&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;When I added the button I wasn’t sure if I’d keep it, but I’ve very much enjoyed seeing the “nods” come in every now and then. I’ve been surprised by how many come in for the various &lt;a href=&#34;https://chameth.com/snippets/&#34;&gt;snippets&lt;/a&gt; and other non-blog pages on the site. It’s a nice reminder that people are out there browsing the site, without all the baggage of monitoring analytics or tracking likes.&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Monthly Meanderings: January 2026</title>
        <link href="https://chameth.com/monthly-meanderings-2026-01/"/>
        <updated>2026-02-01T00:00:00Z</updated>
        <id>https://chameth.com/monthly-meanderings-2026-01/</id>
        <content xml:lang="en" type="html">&lt;p&gt;Welcome to the second edition of my monthly meanderings. For a bit of context, you can check out &lt;a href=&#34;https://chameth.com/monthly-meanderings-2025-12/&#34;&gt;the introduction to the first edition&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;website-updates&#34;&gt;Website updates&lt;/h3&gt;
&lt;p&gt;It’s been a pretty busy month for chameth.com. Three blog posts: &lt;a href=&#34;https://chameth.com/the-meaning-of-life/&#34;&gt;The Meaning of Life&lt;/a&gt; — an entry into the &lt;a href=&#34;https://www.jeremiahlee.com/posts/2026-01-indieweb-carnival/&#34;&gt;IndieWeb carnival&lt;/a&gt; where I mostly review a book on Stoicism —  &lt;a href=&#34;https://chameth.com/surge-protectors-marketing-vs-reality/&#34;&gt;Surge Protectors: Marketing vs Reality&lt;/a&gt; which is a dump of a research rabbit hole I fell into, and &lt;a href=&#34;https://chameth.com/tailscale-docker-gotcha/&#34;&gt;An Interesting Tailscale + Docker Gotcha&lt;/a&gt; which documents a fun problem I encountered.&lt;/p&gt;
&lt;p&gt;I’ve also made a new &lt;a href=&#34;https://chameth.com/films/&#34;&gt;films&lt;/a&gt; section, which is basically my Letterboxd data but free range instead of cooped up in a silo. I imported my 200-odd reviews, and for the last few I wrote the review on my site and then copied it over to Letterboxd. That’s what the IndieWeb folks call &lt;a href=&#34;https://indieweb.org/POSSE&#34;&gt;POSSE&lt;/a&gt;: publish on your own site, syndicate elsewhere. I like the idea that my website is the canonical source of truth for all the public things I generate. I want to expand this to some other areas in the future: music, video games, board games, etc.&lt;/p&gt;
&lt;p&gt;I made most of the film components into “shortcodes”, so I can drop them into arbitrary pages or posts, like so:&lt;/p&gt;
&lt;a class=&#34;film-list raised-box plain-link&#34; data-title=&#34;Film list&#34; href=&#34;https://chameth.com/films/lists/ranking/&#34;&gt;
  &lt;h3 class=&#34;plain-header&#34;&gt;Watched films ranked&lt;/h3&gt;
  &lt;div class=&#34;poster-stack&#34;&gt;
    &lt;img src=&#34;https://chameth.com/films/265/poster.jpg&#34; alt=&#34;Poster of Midsommar&#34;/&gt;&lt;img src=&#34;https://chameth.com/films/4/poster.jpg&#34; alt=&#34;Poster of Easy A&#34;/&gt;&lt;img src=&#34;https://chameth.com/films/97/poster.jpg&#34; alt=&#34;Poster of Hackers&#34;/&gt;&lt;img src=&#34;https://chameth.com/films/25/poster.jpg&#34; alt=&#34;Poster of The Matrix&#34;/&gt;&lt;img src=&#34;https://chameth.com/films/202/poster.jpg&#34; alt=&#34;Poster of Sinners&#34;/&gt;
  &lt;/div&gt;
  &lt;div class=&#34;description&#34;&gt;&lt;p&gt;Every film I’ve watched since I started logging, ranked.&lt;/p&gt;
&lt;p&gt;Obviously super subjective, and subject to change often and arbitrarily.&lt;/p&gt;
&lt;/div&gt;
  &lt;p class=&#34;count&#34;&gt;264 films&lt;/p&gt;
&lt;/a&gt;
&lt;p&gt;Finally, I added a new widget to the bottom of most pages: a “nod” button. It’s effectively a “like” or “kudos” button but deliberately does not track how many times it’s been pressed, has no social network connotations, and won’t flood my inbox or hit rate limits if it gets spammed. If you click the button, it simply sends a message to a private IRC channel that says “Someone nodded at $URL”. I’ll probably write a bit more about it in a blog post in the future.&lt;/p&gt;
&lt;h3 id=&#34;other-projects&#34;&gt;Other projects&lt;/h3&gt;
&lt;p&gt;I only have one small non-chameth.com update: a new project called &lt;a href=&#34;https://github.com/csmith/pigeonhole/&#34;&gt;pigeonhole&lt;/a&gt; which provides a really basic web service that lets you leave messages in a named cubby, and retrieve them at a later date. I’m using it to automate adding things to my Obsidian notes from my Apple Watch. There’s lots of reasons why that isn’t as simple as it sounds. I went through a bunch of iterations to try and make it work before just writing code to fix it.&lt;/p&gt;
&lt;p&gt;I wanted to do this because I’m trying to replace Todoist with just having tasks in &lt;a href=&#34;https://obsidian.md/&#34;&gt;Obsidian&lt;/a&gt;. I already used Obsidian for longer form notes, but I’ve now also started using it for drafting blog posts (I’m typing this in Obsidian right now!), and trying to keep more useful day-to-day notes. I’ll probably write a blog post with more details once I’ve figured everything out.&lt;/p&gt;
&lt;p&gt;Finally, I’ve been moving a bunch of projects to a private &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt; instance, with the ultimate goal of having all of my projects live there. I’m going to mirror the contents out to GitHub and maybe other places, but Forgejo will be the single source of truth, and where all the workflows/dependency wrangling/etc happens. I’ve got &lt;a href=&#34;https://renovatebot.com/&#34;&gt;Renovate&lt;/a&gt; configured to do dependency bumps, and I’m very much liking the whole setup so far. I’m saying it a lot but again: I’ll blog about it in more detail when it’s a bit further along.&lt;/p&gt;
&lt;h3 id=&#34;entertainment&#34;&gt;Entertainment&lt;/h3&gt;
&lt;p&gt;I mentioned last month that I was playing Hogwarts: Legacy. I finished that off, and have been struggling to find something to scratch the same itch. I’ve tried a whole bunch of open world games, and basically none of them work for me. They have cumbersome control schemes (yay console ports), really awkward or sluggish movement, and so on. I think I’m just too fussy, but if I’m going to sink dozens of hours into a game it shouldn’t feel annoying or like a chore!&lt;/p&gt;
&lt;p&gt;I did play through &lt;a href=&#34;https://store.steampowered.com/app/2366980/Thank_Goodness_Youre_Here/&#34;&gt;Thank Goodness You’re Here&lt;/a&gt;, which is delightfully funny but sadly only took 3.6 hours to finish with 100% of the achievements. Still highly recommend picking it up, especially when it’s on sale for under ‘10 bob’.
I’ve also started playing through Control, and I’m enjoying the ambience. There’s an insanely sharp difficulty spike for the first boss, though, which nearly made me put it down.&lt;/p&gt;
&lt;p&gt;In other entertainment, I’ve been playing a load of turn-based games on &lt;a href=&#34;https://boardgamearena.com/&#34;&gt;Board Game Arena&lt;/a&gt; , and I’ve watched a bunch of films after barely watching any last month. Here’s another of those shortcodes to show them off:&lt;/p&gt;
&lt;ol class=&#34;watched-films&#34;&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/bugonia-2025/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/234/poster.jpg&#34; alt=&#34;Poster of Bugonia&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-01-13&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/arrival-2016/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/235/poster.jpg&#34; alt=&#34;Poster of Arrival&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-01-15&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/ender-s-game-2013/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/236/poster.jpg&#34; alt=&#34;Poster of Ender&amp;#39;s Game&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-01-16&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/bill-ted-s-excellent-adventure-1989/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/237/poster.jpg&#34; alt=&#34;Poster of Bill &amp;amp; Ted&amp;#39;s Excellent Adventure&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-01-19&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/upgrade-2018/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/238/poster.jpg&#34; alt=&#34;Poster of Upgrade&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-01-20&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/blow-the-man-down-2019/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/239/poster.jpg&#34; alt=&#34;Poster of Blow the Man Down&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-01-21&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/psych-2-lassie-come-home-2020/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/241/poster.jpg&#34; alt=&#34;Poster of Psych 2: Lassie Come Home&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-01-26&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;around-the-web&#34;&gt;Around the web&lt;/h3&gt;
&lt;h4 id=&#34;remotely-unlocking-an-encrypted-hard-diskhttpsjyndevremotely-unlocking-an-encrypted-hard-disk&#34;&gt;&lt;a href=&#34;https://jyn.dev/remotely-unlocking-an-encrypted-hard-disk/&#34;&gt;Remotely unlocking an encrypted hard disk&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;A fun post documenting using Tailscale in a very weird way to solve a relatable problem. Very much my cup of tea.&lt;/p&gt;
&lt;h4 id=&#34;i-charged-18000-for-a-static-html-pagehttpsidiallocomblog18000-dollars-static-web-page&#34;&gt;&lt;a href=&#34;https://idiallo.com/blog/18000-dollars-static-web-page&#34;&gt;I charged $18,000 for a static HTML page&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;This one’s actually from 2019, but it’s the first time I’ve come across it, and it’s a fun story. As a contractor, deciding what to bill for and what not to can be a bit messy. Downtime between meetings? The ten minutes in the shower you spent thinking through a bug? It’s a lot more clear cut if the client wants you to sit in an office, twiddling your thumbs, though.&lt;/p&gt;
&lt;h4 id=&#34;oxide-rfd-using-llms-at-oxidehttpsrfdsharedoxidecomputerrfd0576&#34;&gt;&lt;a href=&#34;https://rfd.shared.oxide.computer/rfd/0576&#34;&gt;Oxide RFD: Using LLMs at Oxide&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;A wonderfully well thought out piece about using LLMs in a respectful, sensible manner. I’d been thinking about writing something similar, but it wouldn’t have been anywhere near as well crafted as this. While I basically agree with it all, the part about LLM-generated text really resonates:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LLM-generated prose undermines a social contract of sorts: absent LLMs, it is presumed that of the reader and the writer, it is the writer that has undertaken the greater intellectual exertion.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&#34;your-app-subscription-is-now-my-weekend-projecthttpsrselbachcomyour-sub-is-now-my-weekend-project&#34;&gt;&lt;a href=&#34;https://rselbach.com/your-sub-is-now-my-weekend-project&#34;&gt;Your app subscription is now my weekend project&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Roberto points out that monthly SaaS subscriptions start looking even less appealing when you can just prompt an alternative into existence. &lt;a href=&#34;https://chameth.com/an-app-can-be-a-ready-meal/&#34;&gt;I talked a bit about using LLMs for one-person software before&lt;/a&gt;, and it becomes even more appealing when the alternative is paying monthly for a service that keeps shoe-horning in features you don’t want, making you agree to share data with 329 “trusted partners”, and/or repeatedly ratcheting up the price.&lt;/p&gt;
&lt;h4 id=&#34;ekuls-internet-spacehttpsekulme&#34;&gt;&lt;a href=&#34;https://ekul.me/&#34;&gt;Ekul’s Internet Space&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;I love the aesthetics of this personal site, and the data tracking that goes along with it. I’m also slightly aghast at the amount of Monster consumed! Makes me want to track more things and expose them on my own site.&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Reproducible Builds and Docker Images</title>
        <link href="https://chameth.com/reproducible-builds-docker-images/"/>
        <updated>2022-02-18T00:00:00Z</updated>
        <id>https://chameth.com/reproducible-builds-docker-images/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image left&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/reproducible-builds-docker-images/dependency.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/reproducible-builds-docker-images/dependency.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/reproducible-builds-docker-images/dependency.png&#34; alt=&#34;Comic showing all modern digital infrastructure is built upon one project by a random person in Nebraska&#34; loading=&#34;lazy&#34; width=&#34;385&#34; height=&#34;489&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;XKCD 2347: Dependency&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;a href=&#34;https://reproducible-builds.org/&#34;&gt;Reproducible builds&lt;/a&gt; are builds which you are able to reproduce byte-for-byte,
given the same source input. Your initial reaction to that statement might be “Aren’t nearly all builds
‘reproducible builds’, then? If I give my compiler a source file it will always give me the same binary, won’t it?”
It &lt;em&gt;sounds&lt;/em&gt; simple, like it’s something that should just be fundamentally true unless we go out of our way to break it,
but in reality it’s actually quite a challenge. A group of Debian developers have been working on reproducible packages
for the best part of a decade and while they’ve made fantastic progress,
&lt;a href=&#34;https://isdebianreproducibleyet.com/&#34;&gt;Debian still isn’t reproducible&lt;/a&gt;. Before we talk about why it’s a hard problem,
let’s take a minute to ponder why it’s worth that much effort.&lt;/p&gt;
&lt;h3 id=&#34;on-supply-chain-attacks&#34;&gt;On supply chain attacks&lt;/h3&gt;
&lt;p&gt;Suppose you want to run some open-source software. One of the many benefits of open-source software is that anyone
can look at the source and, in theory, spot bugs or malicious code. Some projects even have sponsored audits or
penetration tests to affirm that the software is safe. But how do you actually deploy that software? You’re probably
not building from source - more likely you’re using a package manager to install a pre-built version, or downloading
a binary archive, or running a docker image. How do you know whoever prepared those binary artifacts did so from
an un-doctored copy of the source? How do you know a
&lt;a href=&#34;https://en.wikipedia.org/wiki/SourceForge#Controversies&#34;&gt;middle-man hasn’t decided to add malware to the binaries to make money&lt;/a&gt;?&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;Even worse: if the software you’re trying to use includes any dependencies, you have the same issue of trust
with them. Maybe &lt;em&gt;your&lt;/em&gt; supplier isn’t compromising the software, but that doesn’t mean &lt;em&gt;their&lt;/em&gt; supplier isn’t. The
beauty-cum-horror of a supply chain attack is that it can target the weakest link anywhere along the supply chain.
Even if there aren’t any binary files involved, dependencies can still be attacked: what if &lt;code&gt;npmjs.com&lt;/code&gt; or
&lt;code&gt;proxy.golang.org&lt;/code&gt; or &lt;code&gt;github.com&lt;/code&gt; return a different version of a dependency-of-a-dependency when the request
comes from your IP address? It doesn’t even need to be a modified dependency, it could be a perfectly un-tampered,
properly signed copy of the source, just from an older version with a known vulnerability.&lt;/p&gt;
&lt;p&gt;Enter stage left: reproducible builds, here to save the day! If the build process is reproducible then you - or anyone
else on the internet - can perform the same build on the same source and validate the output has the same checksum or
hash. If Debian publish a binary package and an independent re-builder comes up with the exact same build artifact,
there’s a reasonably good chance that the build is good. An attacker would have to compromise both the build machine
and the re-build machine to do anything nefarious. The more re-builders there are, the less feasible a supply chain
attack is.&lt;/p&gt;
&lt;h3 id=&#34;so-why-isnt-software-just-reproducible&#34;&gt;So why isn’t software just reproducible?&lt;/h3&gt;
&lt;h4 id=&#34;compilers&#34;&gt;Compilers&lt;/h4&gt;
&lt;p&gt;As a bit of an experiment, I asked some friends to run the following for me and report the answer:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;echo&lt;/span&gt; -e &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;#include &amp;lt;stdio.h&amp;gt;\nint main() { printf(\&amp;#34;Hello\&amp;#34;); return 0; }&amp;#34;&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  gcc -x c -o hello.out - &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  sha256sum hello.out
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This compiles a super-simple hello world program and then prints the SHA-256 hash of the resulting binary. Here are
the results:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hash&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;GCC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1f62feab5a06861dc575201d807781926d1ae49fb113da018fde8b670a1346f7&lt;/td&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;11.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b8e6f2c7082be69f65ffa5e7a3d749eb47866a1b2e1ec19efb63cc59a8b160cd&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;8.3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cbad2e47a22c234b5e7fa55e029a8db4d64ac7a962e2176bd2e1373d78954088&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;8.3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e0f6bbc13b29fea8cfa2a975ba4661e781323298aec166c8311d342e6f93c4a6&lt;/td&gt;
&lt;td&gt;Alpine&lt;/td&gt;
&lt;td&gt;10.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e379156895e06c7a0bf18ac4d648860edcb2655576b0ab9fab172bd6c8b92075&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;10.2.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7ffdaee4eb64e016b89dc5e54d2c8eebab3cebafe2c7aa97de627b5972ecea46&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;11.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8ae52cc166743b6ae1eb3e14179ef33de5061a04237f8f97088c896c41a2f698&lt;/td&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;11.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8ae52cc166743b6ae1eb3e14179ef33de5061a04237f8f97088c896c41a2f698&lt;/td&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;11.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As you can see, there are barely any duplicates. Even the same version of GCC on the same OS sometimes produces
different results. And this is the most basic program I could write! Differences arise from the compiler version,
the build flags, the libraries installed, and a whole host of other factors. If you compile a Go application instead of
a C one, then by default the compiler will include debug information in the binary. This includes the full path to the
source file on disk, so building a project in &lt;code&gt;/home/chris/&lt;/code&gt; will produce a different binary to building the same
source in &lt;code&gt;/tmp&lt;/code&gt;. Future versions of Go are also going to stamp in other meta-data such as VCS info, so building inside
and outside a Git repository will produce different binaries.&lt;/p&gt;
&lt;h4 id=&#34;archives&#34;&gt;Archives&lt;/h4&gt;
&lt;p&gt;Compilers are only half the problem. Build processes are usually multistep, involving compiling, moving, compressing,
and so on. Consider creating an archive of a file:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;repeat &lt;span class=&#34;chroma-m&#34;&gt;4&lt;/span&gt; touch hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; tar zcf hello.tgz hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum hello.tgz &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sleep 0.5
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;f3d5c56f6b8089de95d62d060e6ffcbbad26875807ae7bc253f07cd097ea61be  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;ab67f2e865b5afa87d9b2434d92b0c271b3cf730fa85988f84852551749ba6ed  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;ab67f2e865b5afa87d9b2434d92b0c271b3cf730fa85988f84852551749ba6ed  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;738678c9650b10fd83636997dd1aba4016bbf0ec5ebf3dfd4ef75d770b56e23b  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Any file added to a tar takes with it a timestamp, so the build is only reproducible if it happens at the exact same
time! We can make this reproducible by forcing &lt;code&gt;tar&lt;/code&gt; (and the same goes for &lt;code&gt;zip&lt;/code&gt; and most other archive formats) to
set a certain timestamp on the files:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;repeat &lt;span class=&#34;chroma-m&#34;&gt;4&lt;/span&gt; touch hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; tar --mtime 2022-02-18T01:00 -zcf hello.tgz hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum hello.tgz &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sleep 0.5 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In a real build there are basically two approaches here: you can set it to a pre-defined value (like the unix epoch),
or you can set it to match the modification time of the source files. The former is easiest, but the latter is more
cosmetically and semantically appealing.&lt;/p&gt;
&lt;h4 id=&#34;iteration-order&#34;&gt;Iteration order&lt;/h4&gt;
&lt;p&gt;So we’ve pinned our build environment, we’re manipulating timestamps when adding files to archives, now what? Imagine
part of the build process involves looping through all the files in a directory and doing &lt;em&gt;something&lt;/em&gt;. What order do
these files get iterated in? Well, that very much depends on the filesystem and perhaps when the files themselves
were created. To ensure this is reproducible we need to explicitly sort any such operation so that it’s always
consistent. This iteration could be happening in a tool that’s called by another tool that’s called by a build script,
so the fix isn’t necessarily straight-forward.&lt;/p&gt;
&lt;aside class=&#34;sidenote raised-box&#34;&gt;
  &lt;h5 class=&#34;plain-header&#34;&gt;Side note: a bug war story&lt;/h5&gt;
  &lt;p&gt;I’ve personally been victim to this kind of non-determinism. I was working on an Android app, and committed a new
test that worked fine on my machine, and worked fine on the CI server. But it failed consistently for a colleague.&lt;/p&gt;
&lt;p&gt;We both did fresh checkouts of the source, and ran the tests. Mine passed, his failed. He sent me an archive of
his checkout in case there was something weird going on there, and the tests passed on my machine. We compared
hashes of our checkouts, and they were the same. It was obviously environmental somehow, but everything else worked
fine, and the build system went to great pains to ensure things were the same.&lt;/p&gt;
&lt;p&gt;After a &lt;em&gt;lot&lt;/em&gt; of debugging, I worked out that his test was running with a different version of a library to me,
despite the libraries being defined in the build files and the build files being identical. After &lt;em&gt;even more&lt;/em&gt;
debugging it turned out there were two versions of the library on the classpath, and the ordering of them was
different between my machine and his.&lt;/p&gt;
&lt;p&gt;The actual issue turned out to be that the build tool generated the classpath by iterating over the library
files, and that iteration was done in order of file creation time. The two libraries were added at different points
in the project history, so the creation time in your local cache depended on which versions of the app you’d built
in the past. With no cache everything worked as expected but there was a slim range of commits where only one
library was in use, and if you had run the tests during that period your cache was effectively poisoned.&lt;/p&gt;
&lt;p&gt;We fixed the issue by excluding the older version of the library (which was being pulled in as a transient dependency),
and filed a bug against the build tool to make the classpath properly deterministic. I think that stands as the most
difficult to diagnose bug I’ve ever dealt with.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;Interestingly, if you iterate over a map in Go, the iteration is &lt;em&gt;deliberately&lt;/em&gt; non-deterministic. That’s an attempt
to defeat &lt;a href=&#34;https://www.hyrumslaw.com/&#34;&gt;Hyrum’s Law&lt;/a&gt; and prevent developers from relying on whatever the current
behaviour happens to be. This actually makes it easier to make things reproducible as the problem is loud and
in-your-face, rather than subtle and hard to spot.&lt;/p&gt;
&lt;h4 id=&#34;other-sources&#34;&gt;Other sources&lt;/h4&gt;
&lt;p&gt;There’s an awful lot of other places that non-determinism can come from. If the app pulls in dependencies, their
versions have to be pinned, otherwise your build changes depending on the latest release of that dependency. If
the build process pulls any information from a website, it’s liable to change. Hopefully the website is under your
control so that you can version the resource and pin that version. Obviously, anything to do with dates or the
current user will probably cause problems. Timezones and locales can cause subtle differences.&lt;/p&gt;
&lt;h3 id=&#34;what-about-docker&#34;&gt;What about Docker?&lt;/h3&gt;
&lt;p&gt;Docker comes with some good and some bad points for reproducibility. The biggest advantage is that it inherently
completely describes the build environment; it should work exactly the same from one system to another, even across
different OS families. The biggest drawback is it sprays timestamps around like no-one’s business. Each layer in
a container image is a &lt;code&gt;.tar.gz&lt;/code&gt; file, meaning each file within it is timestamped as discussed above. Making an image
involves a lot of copying of files around, so these timestamps invariably end up causing reproducibility issues.&lt;/p&gt;
&lt;p&gt;Even worse than timestamps in the filesystem, the image format also contains some meta-data that includes the
timestamp at which each layer was built. That means even if you go out of your way to set the timestamp of every
single file in your image, the image itself will be different every time you rebuild it. There is no way to deal
with this in Docker, which is a very sad state of affairs. Fortunately, &lt;a href=&#34;https://buildah.io/&#34;&gt;Buildah&lt;/a&gt; provides
a &lt;code&gt;--timestamp&lt;/code&gt; flag for &lt;em&gt;its&lt;/em&gt; build commands; this not only sets the layer timestamp but also the creation
timestamp of any file within the layer.&lt;/p&gt;
&lt;p&gt;The other major issue that affects Docker images is the pinning of packages pulled in by package managers. An awful
lot of images are based on Alpine or Debian derivatives, and use &lt;code&gt;apk&lt;/code&gt; or &lt;code&gt;apt&lt;/code&gt; to install dependencies. These need
to have a version specified as otherwise the package manager will just pull in the latest at the time of the build.
But this isn’t quite enough: you also need to pin the version of any packages that they depend on, recursively.
This means flattening the entire package hierarchy and installing all the packages explicitly and with pinned
versions.&lt;/p&gt;
&lt;p&gt;One more wrinkle in the package management space is that Alpine don’t keep old packages in their main repositories.
If you have a Docker image with pinned alpine packages in, it will stop building if the package is updated. This
isn’t necessarily fatal to making a reproducible build — as long as it’s reproducible for its useful lifetime,
I don’t really see an issue.&lt;/p&gt;
&lt;p&gt;Honestly, though, the biggest issue with making Docker images reproducible is getting people to care. Dockerfiles
are a relatively new way of packaging software, and there’s no centralised organisation like you find with Linux
distributions. There are enough challenges that most casual packagers aren’t going to bother, and no real
incentive for them to. That won’t stop me trying, though!&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Artisanal Docker images</title>
        <link href="https://chameth.com/artisanal-docker-images/"/>
        <updated>2022-02-05T00:00:00Z</updated>
        <id>https://chameth.com/artisanal-docker-images/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/artisanal-containers.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/artisanal-containers.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/artisanal-docker-images/artisanal-containers.jpg&#34; alt=&#34;Shelf showing a variety of artisanal containers&#34; loading=&#34;lazy&#34; width=&#34;300&#34; height=&#34;432&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Artisanal containers…&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I run a fair number of services as docker containers. Recently, I’ve been moving away from pre-built images
pulled from Docker Hub in favour of those I’ve hand-crafted myself. If you’re thinking “that sounds like a
lot of effort”, you’re right. It also comes with a number of advantages, though, and has been a fairly fun
journey.&lt;/p&gt;
&lt;h3 id=&#34;the-problems-with-docker-hub-and-its-images&#34;&gt;The problems with Docker Hub and its images&lt;/h3&gt;
&lt;h4 id=&#34;rate-limits&#34;&gt;Rate limits&lt;/h4&gt;
&lt;p&gt;For the last few years, I’ve been getting increasingly unhappy with Docker Hub itself. Docker-the-technology
is wonderful, but Docker-the-company has been making some rather large missteps. The biggest and most impactful
of these has been introducing “pull rate” limits. At the time of writing, if you want to just pull a public image
without logging in then you are limited to 100 pulls every 6 hours. If you log in then you’re limited to 200 pulls
per 6 hours, but it’s account wide. This might seem like a big enough number, but I repeatedly hit it and there
is no way to actually audit what is causing it. I have various containers that may all pull images at arbitrary
times (e.g. continuous integration build agents), and the only information you get back from Docker Hub is the
number of pulls remaining.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;Obviously, I could start paying Docker Hub for a “Pro” plan. That gets you 5,000 pulls per day for $7/month.
The downside is that every docker client would have to be authenticated, which presents a fair annoyance in
terms of credential management. I also don’t really like how they positioned the service as a public utility
with special treatment in the docker software, and then start tightening the ratchet to make money.&lt;/p&gt;
&lt;h4 id=&#34;bad-images&#34;&gt;“Bad” images&lt;/h4&gt;
&lt;p&gt;I’m fairly opinionated about what a container image should look like: most importantly it should run just a
single process, and only include the bare minimum dependencies required for that. Other people think differently,
and it’s very hard to tell at a glance whether an image on Docker Hub contains just the application you want,
or whether it also bundles MySQL, Redis, Elasticsearch, and a partridge in a pear tree. Some people want that
kind of thing, but I really don’t. It’s also very hard to tell whether an image is officially endorsed by the
upstream project, and where the source Dockerfile is. This used to be better because most projects used Docker Hub’s
automatic builds, but they’re now a “pro” feature.&lt;/p&gt;
&lt;p&gt;I quite often found that I’d be looking for an image for X, and there would be 5-10 images from different users.
None of them looked official, some of them were out-of-date, some bundled the kitchen sink. Even when one looked
good, it’s a bit of a gamble whether the author is going to keep it updated or not.&lt;/p&gt;
&lt;h4 id=&#34;doijanky&#34;&gt;Doijanky&lt;/h4&gt;
&lt;p&gt;The rate limits and other problems were annoying, but they weren’t really annoying enough to force me to do
anything about it. The straw that broke the camel’s back came later: I was looking at the
&lt;a href=&#34;https://hub.docker.com/_/golang&#34;&gt;official golang images&lt;/a&gt;, and noticed that all the tags were pushed by a
random user account called “doijanky”:&lt;/p&gt;
&lt;figure class=&#34;image center&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/doijanky.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/doijanky.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/artisanal-docker-images/doijanky.png&#34; alt=&#34;An &amp;#39;official&amp;#39; Docker Hub image pushed by user &amp;#39;doijanky&amp;#39;&#34; loading=&#34;lazy&#34; width=&#34;786&#34; height=&#34;249&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;An ‘official’ Docker Hub image pushed by user ‘doijanky’&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I, perhaps naively, assumed that official images were built on Docker Hub’s own infrastructure. Why would
all the Golang images be attributed to this user? Checking out their profile, they’re simply identified as
a “Community User” like everyone else, with no repositories of their own. The only thing in the profile is
their homepage, which is a link to a Jenkins dashboard: &lt;a href=&#34;https://doi-janky.infosiftr.net/&#34;&gt;https://doi-janky.infosiftr.net/&lt;/a&gt;. It appears
legitimate: “Infosiftr” are a container consultancy and the dashboard is linked to from the README in the
official images git repository, but I find it baffling that they’re using third-party infrastructure and
a normal user account (with a dubious name) to push these images. There doesn’t seem to be a good way to
verify what you pull corresponds to the Dockerfile it came from; if infosiftr wanted to inject something
into the build they could happily do so, and who knows how good their infosec posture is? If someone got
access to the “doijanky” account, how long could they upload malicious images before someone noticed?&lt;/p&gt;
&lt;p&gt;This little roller-coaster ride from “are all the official images compromised?!” to “oh, no, they’re not,
it’s all just awful” finally convinced me to look at building my own images from scratch.&lt;/p&gt;
&lt;h3 id=&#34;the-implementation-templating-with-contempt&#34;&gt;The implementation: templating with contempt&lt;/h3&gt;
&lt;p&gt;One of the big issues I needed to tackle was how to deal with updates. I didn’t want to have to go and
edit a file every time some minor release was made of some software, or every time there was a security
vulnerability in a common library. The official images use a shell-scripting based system to check for
updates and generate Dockerfiles, I decided to do something similar but with Go templates. The result is
a tool called &lt;a href=&#34;https://github.com/csmith/contempt&#34;&gt;contempt&lt;/a&gt;. It takes a template like:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;{{image&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;golang&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt; AS build&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;ARG&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;TAG&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;{{github_tag &amp;#34;&lt;/span&gt;example/project&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;}}&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;RUN&lt;/span&gt; apk add --no-cache &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-o&#34;&gt;{{&lt;/span&gt;range &lt;span class=&#34;chroma-nv&#34;&gt;$key&lt;/span&gt;, &lt;span class=&#34;chroma-nv&#34;&gt;$value&lt;/span&gt; :&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt; alpine_packages &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;git&amp;#34;&lt;/span&gt; -&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-o&#34;&gt;{{&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;$key&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;}}={{&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;$value&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-o&#34;&gt;{{&lt;/span&gt;end&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Contempt has support for getting information from a variety of sources. In this case, it’s getting
the latest digest of another Docker image, the latest tag from a Git repository, and the latest version
of an alpine package and all its dependencies. The resulting Dockerfile looks something like this:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c&#34;&gt;# Generated from https://github.com/csmith/dockerfiles/blob/master/miniflux/Dockerfile.gotpl&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c&#34;&gt;# BOM: {&amp;#34;apk:brotli-libs&amp;#34;:&amp;#34;1.0.9-r5&amp;#34;,&amp;#34;apk:busybox&amp;#34;:&amp;#34;1.34.1-r4&amp;#34;, &amp;lt;snip&amp;gt; }&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;reg.c5h.io/golang@sha256:ac8fa5f4078b0a697796b5d741&lt;/span&gt;... AS build&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;ARG&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;TAG&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;2.0.35&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;RUN&lt;/span&gt; apk add --no-cache &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        brotli-libs&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;1.0.9-r5&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nv&#34;&gt;busybox&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;1.34.1-r4&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-c1&#34;&gt;# &amp;lt;snip&amp;gt;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nv&#34;&gt;pcre2&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;10.39-r0&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nv&#34;&gt;zlib&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;1.2.11-r3&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I’ve cut out the longer parts for readability. You can see that it pins all versions of the alpine packages in use,
as well as the base image. This ensures that if you build from the same Dockerfile at a later time it will build the
same image (or will fail entirely, as Alpine doesn’t keep their old packages around indefinitely). It also produces
a “bill of materials” as a really long JSON-encoded comment. If you let contempt commit the Dockerfile it uses the
BOM to generate useful commit messages like: &lt;code&gt;[project] apk:busybox: 1.34.1-r3-&amp;gt;1.34.1-r4&lt;/code&gt;, so you can see exactly
what changed.&lt;/p&gt;
&lt;p&gt;Contempt also has support for building and pushing images whenever it changes the Dockerfile. I use it in a
GitHub action that runs daily to check all my images are up-to-date and push those that aren’t. It understands
the dependencies between images (by pre-analysing the templates) so it will always check and build base images
before ones that require them. This means an update to, say, the “alpine” base image will cause anything that
depends on it to get updated at the same time, ensuring security updates are rolled out promptly.&lt;/p&gt;
&lt;h3 id=&#34;the-result&#34;&gt;The result&lt;/h3&gt;
&lt;p&gt;You can see my collection of lovingly hand-crafted Dockerfiles in my &lt;a href=&#34;https://github.com/csmith/dockerfiles&#34;&gt;dockerfiles&lt;/a&gt;
repository.&lt;/p&gt;
&lt;p&gt;There are a number of advantages to handwriting all the images I use. The obvious one is that they’re all built
how I want: there are no extraneous dependencies, they’re all based on the same small set of base images (rather
than pulling around 10 different versions of debian), nothing tries to also run a DBMS in its container, etc.&lt;/p&gt;
&lt;p&gt;This level of customisation goes further, though. Because I’m packaging the software myself, I can tweak how it’s
built to fit my needs. A couple of things I run need their own TLS certificates separate from my normal HTTPS
setup, so I bake my &lt;a href=&#34;https://github.com/csmith/certwrapper/&#34;&gt;certwrapper&lt;/a&gt; tool in to manage those; I can even set the
build flags on certwrapper to only enable the particular DNS provider I personally need (thus avoiding dragging in
clients for AWS, GCP, etc). Some software like Hashicorp Vault has an optional web interface that I don’t need,
so I simply don’t enable it in the build. These changes save build time, reduce image sizes, in some cases improve
runtime performance, and generally reduce the attack surface of what’s running in the container.&lt;/p&gt;
&lt;p&gt;It’s also been a great way to learn more about how software is distributed. Writing a Dockerfile is not that distant
from writing a PKGBUILD file for an Arch Linux package, or the equivalent for other distributions. In a couple of
instances I’ve googled how to solve a particular issue, and found an Arch or Void linux maintainer asking the upstream
project about the exact same issue.&lt;/p&gt;
&lt;p&gt;Finally, all the images I build I push to my own registry so there are obviously no rate limiting issues.
Standing up a service (assuming the Dockerfile has been written!) is amazingly quick because the base layers are all
shared and cached, and the registry is a lot physically closer than Docker Hub. Bootstrapping this whole thing becomes
an interesting problem because the image for the registry is stored on the registry, but I’ll leave that discussion for
another post…&lt;/p&gt;
</content>
    </entry>
</feed>
