<?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 curse-of-knowledge-blogging, debugging-beyond-the-debugger, docker-proxying-redux, why-you-should-be-using-https</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/curse-of-knowledge-blogging,debugging-beyond-the-debugger,docker-proxying-redux,why-you-should-be-using-https/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2026-08-27T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <entry>
        <title>Building a family of single-user services</title>
        <link href="https://chameth.com/building-a-family-of-single-user-services/"/>
        <updated>2026-08-27T00:00:00Z</updated>
        <id>https://chameth.com/building-a-family-of-single-user-services/</id>
        <content xml:lang="en" type="html">&lt;p&gt;Last month I &lt;a href=&#34;https://chameth.com/tracking-pbs-without-the-dark-patterns/&#34;&gt;made and wrote about&lt;/a&gt; a personal service
to track my workouts and record personal bests. Since then, things have escalated. I now have eight different
services running in the same “family”:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Collegiate Room&lt;/td&gt;
&lt;td&gt;Simple weather overview for my current location&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;German Mall&lt;/td&gt;
&lt;td&gt;LLM broker for use by other services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gloss White&lt;/td&gt;
&lt;td&gt;Weight tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Olive Crosses&lt;/td&gt;
&lt;td&gt;Central service hub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pompei Band&lt;/td&gt;
&lt;td&gt;Workout and personal best tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed Carp&lt;/td&gt;
&lt;td&gt;Spaced repetition system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiger Island&lt;/td&gt;
&lt;td&gt;Reading history and library tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical Horse&lt;/td&gt;
&lt;td&gt;Location history and analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The names are all anagrams of something related to their function. Finding those anagrams brings me an unreasonable
amount of joy. Unscrambling them is an exercise left to the reader. On top of those eight, I currently have four other
service ideas sat in my todo list. It’s going to be a big family.&lt;/p&gt;
&lt;h3 id=&#34;a-postmortem-on-the-previous-hodge-podge&#34;&gt;A postmortem on the previous hodge-podge&lt;/h3&gt;
&lt;p&gt;Most of this functionality isn’t new to me. When I started out playing around with LLMs, I created a “personal data
aggregator” that just kept getting things bolted on. I &lt;a href=&#34;https://chameth.com/an-app-can-be-a-ready-meal/#what-ive-made&#34;&gt;said at the time&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;My biggest just-for-me project is a web app that I started to help me aggregate film recommendations. It’s since morphed into a general personal data aggregation service: it deals with data from GitHub, Todoist, Letterboxd, TMDB, Healthkit, and others. It also lets me make re-orderable lists, store recipes, and more. Parts of this could definitely be open sourced, and I might carve them out at some point, but it’s mostly a glorious hodge-podge of things specific to me. Having all these services in one place lets me make quick and dirty automations, for example: when I create a Todoist note on my phone or watch, I often forget to set the due date, so it doesn’t show up in the “Today” view. It was literally a few lines of code to plumb things together so any inbox task without a due date gets set to today automatically.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In a twist that &lt;em&gt;nobody&lt;/em&gt; could see coming, it turns out that a “glorious hodge-podge” is actually quite hard to maintain
and very annoying to work with. It didn’t help that it had a fairly extensive Svelte frontend and I really am not a fan
of JavaScript. I ended up using it less and less just because it wasn’t fun to use or work on, and I didn’t want to
waste the limited LLM tokens on doing massive refactors or reworks.&lt;/p&gt;
&lt;p&gt;I’ve got almost two decades of professional software development experience under my belt, so you’d think I would
have learnt the lessons on how to write maintainable software. Hell, my CV even mentions that as something I’m
particularly good at. So what went wrong?&lt;/p&gt;
&lt;p&gt;There were a few factors. One of the obvious things to blame is that it was written mostly with LLMs.
Most of that blame isn’t very honest, though. In the year or so since, LLMs have definitely got better at writing
good code; but I was manually reviewing all the code at the time, and telling it quite carefully what to implement
and how to do it. So the buck really stops with me.&lt;/p&gt;
&lt;p&gt;I think it’s actually more because it was my &lt;em&gt;first&lt;/em&gt; larger project using LLMs. It’s a bit like learning a new
programming language. I don’t think the first sizable project I’ve written in any language has ever been any
good. You can read and learn as much as you want, but you need to actually run into some problems headfirst to
get a grip on how the language works. Or at least I do. LLM coding agents are basically the same deal.&lt;/p&gt;
&lt;p&gt;What the LLMs actually did was mask several problems. The architecture wasn’t right for how the service developed.
It started out as a single-purpose app and gradually got things bolted on, without any major changes. Everything
ended up in horizontal slices: a massive database layer with every query used by every part of the app, a massive
HTTP layer, and so on. Working on a single feature became a tap dance all over the source tree. This sort of
friction is normally quite apparent when you’re writing code by hand, and at some point it hurts enough that
you throw in the towel and do some refactoring. But the LLM will happily plod on without exposing that pain,
so the project needed a more active kind of architecture management that it just didn’t get.&lt;/p&gt;
&lt;p&gt;The other problem they covered up was how brittle and unmaintainable the JavaScript frontend was. I’m not
even sure how I ended up with a Svelte frontend in hindsight. It’s my preferred framework, but I still only
reach for it as a last resort when there’s no other way to avoid using one. It seems like there were lots of
ways to avoid it here, but I seem to have… well… avoided them. Anyway, each feature got its own slew of
Svelte components, without much thought to reusability, shared patterns, or anything sensible. Some of them
were just thin wrappers around data supplied by Go, others had vast swathes of logic in them, with no real
rhyme or reason as to which way that would fall. It’s the same issue as before: a human would look at it,
say “WTF?” and suggest tidying it up; instead, the LLM just gets on with what you tell it to do.&lt;/p&gt;
&lt;p&gt;There’s also a big non-LLM factor: one of the primary reasons all of this unrelated functionality got
tossed into the same app was that I’d previously worked on a few projects with a microservice architecture
and really didn’t like it. My conclusion was they only really worked in large environments where you can
dedicate people or teams to working on all the boilerplate that happens from maintaining lots of
interconnected projects. Dumping everything and the kitchen sink into one badly architected monolith was,
perhaps, not the best rebound from that.&lt;/p&gt;
&lt;h3 id=&#34;pompei-band-and-beyond&#34;&gt;Pompei Band and beyond&lt;/h3&gt;
&lt;p&gt;When I wanted to implement &lt;del&gt;Strava at home&lt;/del&gt; my PB tracking app, it seemed fairly natural to build it out
as a new service, rather than bolt it on to the slowly decomposing hodge-podge that was giving off a funny
smell. Then when I wanted a weather dashboard, why not do it in the same style as Pompei Band? The same
serving architecture, the same page layout… I could even come up with a similar weird two-word name!&lt;/p&gt;
&lt;p&gt;Then wouldn’t it be nice if the weather dashboard knew where I was, so it could show the local weather?
I could just build that functionality in, but it’d also be a good opportunity to carve out the location
history feature from my old service and make a new, shinier one. Then basically every time I even thought
about going near the old service, I mentally sketched out a replacement service.&lt;/p&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/building-a-family-of-single-user-services/oc.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/building-a-family-of-single-user-services/oc.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/building-a-family-of-single-user-services/oc.png&#34; alt=&#34;A screenshot of Olive Crosses, showing links to other services with icons based on their two-word names&#34; loading=&#34;lazy&#34; width=&#34;1230&#34; height=&#34;619&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Olive Crosses showing links to the other services&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;At one point I had the thought that it’d be nice if the services could all link to one another, but I
didn’t want to have to touch every service whenever I added a new one. So I made a “hub” service, which
maintains the One True Catalogue, and provides a bit of JavaScript the others can all embed to add a
navigation sidebar.&lt;/p&gt;
&lt;p&gt;So after complaining about microservices and JavaScript, it sounds like I have landed on microservices
(or at least miniservices) and JavaScript? Sort of yes, but sort of no. Each service is still its own
thing and makes sense independent of the others. There are some cross-service dependencies, but they’re
very simple and don’t have the same cognitive or boilerplate overhead as a login service that talks to
an authentication service that queries a user manager and so on. And as for the JavaScript, it’s almost
exclusively used for progressive enhancement and a few limited client-side visual things like
maps and graphs. All of the logic lives in Go, which is a lot easier to maintain and test, and all of
the pages are simple Go templates.&lt;/p&gt;
&lt;p&gt;Having these services built in a way that I actually like has motivated me to play around a lot more
with new features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My workout tracking app pulls photos from my private &lt;a href=&#34;https://immich.app&#34;&gt;Immich&lt;/a&gt; server that were
taken during workouts, and shows them alongside. Now I can see all my obligatory end-of-run selfies
in one place.&lt;/li&gt;
&lt;li&gt;The location history app can detect trips away from home, and get an LLM to automatically title them.
My most recent, very exotic, trip has the title “A fortnight in Milton Keynes and Coventry”. Each
trip gets its own little map, and in the future I want to pull in images like with workouts.&lt;/li&gt;
&lt;li&gt;My weight tracking app now exports weigh-ins to Garmin, as it doesn’t fully integrate with Apple
Health (it will export its own weights, but not import others, like the ones from my Withings scales).
Stupid walled gardens, but thankfully I can fix it myself.&lt;/li&gt;
&lt;li&gt;My book tracking app automatically cleans up the titles of books from the nonsense provided by Amazon
to something more palatable. “Killing Eve: Long Shot: A twist on obsession, betrayal and revenge from
Luke Jennings, author of the TV sensation Killing Eve” becomes “Long Shot (Killing Eve #5)”. It does
this by passing the title to an LLM with a bunch of rules.&lt;/li&gt;
&lt;li&gt;The hub generates me a snippet of Tailscale configuration that defines the minimal grants needed to
allow the intra-service dependencies to work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The LLM integrations were inspired by my friend &lt;a href=&#34;https://dataforce.org.uk/&#34;&gt;Shane&lt;/a&gt; who recently
demonstrated an inventory tracking app he’d made, which used Haiku to process images of boxes and
describe the contents. I’ve dabbled with LLM integration before, and always disliked the process
of fiddling with the prompts, and debugging when the LLM gave a nonsensical response. So I built
German Mall, which makes it a lot easier. It lets me define a “workflow” consisting of a system
prompt, initial message, and expected response format. I can browse past runs, tweak the prompt,
rerun them to see the change, all without touching the services using them. Quick iteration loops
are the best. German Mall also provides markdown documentation for each workflow, so integrating
it downstream is simply a case of pointing a coding agent at the right documentation endpoint.&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/building-a-family-of-single-user-services/gm.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/building-a-family-of-single-user-services/gm.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/building-a-family-of-single-user-services/gm.png&#34; alt=&#34;A screenshot of German Mall, showing the workflow configuration for tidying book titles: a form with fields for description, system prompt, user template, output schema, model, max tokens and temperature.&#34; loading=&#34;lazy&#34; width=&#34;1482&#34; height=&#34;1080&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The “tidy book titles” workflow in German Mall&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It’s not all fun and games: having a bunch of services all following the same pattern means
you need to do cross-cutting maintenance on them now and then. But that’s surprisingly easy with LLMs.
I put all the projects in a common folder, and I launch cross-cutting tasks from that folder. Usually
I’ll make the change in one project, and then the cross-cutting task is simply “Look at commit XYZ in
./foo, make the same change to the other projects”.&lt;/p&gt;
&lt;p&gt;I’ve also ended up with a shared library used by all the projects to cut down on some boilerplate.
That’s normally something I’d resist, especially with personal projects, just because the extra overhead
of working in a separate project, managing releases of it, and so on, is a bit annoying. But, again,
LLMs can do almost all of that grunt work. They effectively reduce the “cost” of maintaining separate
services to a negligible level, leaving me to focus on the more fun aspects like coming up with new names.&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>The Case of the Unchanging Config</title>
        <link href="https://chameth.com/the-case-of-the-unchanging-config/"/>
        <updated>2026-04-09T00:00:00Z</updated>
        <id>https://chameth.com/the-case-of-the-unchanging-config/</id>
        <content xml:lang="en" type="html">&lt;p&gt;Last week I was attempting to make it so I could share pictures on IRC directly from my client. This sounds simple, but it involves a bouncer&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; that proxies the request to a standalone image hosting service that I had to modify to be compatible. At one point my testing loop was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make a change to the hosting service&lt;/li&gt;
&lt;li&gt;Commit it&lt;/li&gt;
&lt;li&gt;Tag a new release&lt;/li&gt;
&lt;li&gt;Wait for it to build&lt;/li&gt;
&lt;li&gt;Update the version I’m running on my server&lt;/li&gt;
&lt;li&gt;Reconfigure the bouncer to pass the new parameter or change the URL or whatever&lt;/li&gt;
&lt;li&gt;Try uploading a photo from my phone&lt;/li&gt;
&lt;li&gt;Realise I’ve overlooked something and go back to step one&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I &lt;em&gt;could&lt;/em&gt; have set up a local copy of everything and tested it sensibly. I &lt;em&gt;should&lt;/em&gt; have set up a local copy of everything and tested it sensibly. But it seemed like such a trivial change, and setting up the whole environment seemed like such a pain. After the third or so iteration of failure I was pretty annoyed with myself, computers, and basically everything.&lt;/p&gt;
&lt;p&gt;My biggest annoyance was that my bouncer would not pick up the new URL from the config when I changed it. It’s meant to reload the config when it receives a &lt;code&gt;SIGHUP&lt;/code&gt;, and it claimed to in the logs, but I could clearly see it was still hitting the old URL. Restarting the bouncer to update the config is a pain, as it disconnects me from all the IRC networks, and has to reconnect to them all, reauthenticate, etc. It also mildly spams everyone who shares a channel with me&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;When I finally got everything working I had a look at the bouncer source, and thought I’d spotted the issue. I raised a bug report, ending in this remark:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It looks like the config is reloaded properly, but the handler for uploads is created once at startup and has its own copy of the uploader, so effectively snapshots the config to whatever it is at startup:&lt;/p&gt;
&lt;/blockquote&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-nx&#34;&gt;fileUploadHandler&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;HandlerFunc&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;w&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;ResponseWriter&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;Request&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;chroma-w&#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-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;cfg&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;srv&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;Config&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;chroma-w&#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-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;h&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;fileupload&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;Handler&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;chroma-w&#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-w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;Uploader&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;cfg&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;FileUploader&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#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-w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;DB&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#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-w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;Auth&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;cfg&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;Auth&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#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-w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;HTTPOrigins&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;cfg&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;HTTPOrigins&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#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-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-w&#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-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;h&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;ServeHTTP&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;w&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;chroma-w&#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-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I’m not quite sure what I thought I saw there, but that’s not right. I want to call it a hallucination, but that feels like a very overloaded word these days. I guess I was just seeing what I wanted to see, and that was a good excuse to stop investigating. Still, even if I misidentified the cause, the bug was still present, right? … Right?&lt;/p&gt;
&lt;h3 id=&#34;the-science-of-deduction&#34;&gt;The Science of Deduction&lt;/h3&gt;
&lt;p&gt;When the developer got back and said he couldn’t reproduce my issue, I went back to my install and immediately reproduced it. With a calmer head, I figured it was probably something with my particular set up. First thing to check: am I actually running the version I think I am?&lt;/p&gt;
&lt;p&gt;Unfortunately the bouncer doesn’t actually seem to expose the version anywhere that I can see. It’s not in the logs, it doesn’t have a &lt;code&gt;-version&lt;/code&gt; flag, and none of the IRC-based status commands seem to include it. But I know it’s a Go app, and I know Go embeds the version information. &lt;code&gt;go version -m &amp;lt;binary&amp;gt;&lt;/code&gt; will dump it all out, but the binary is inside a Docker image, and the Docker image is one of my &lt;a href=&#34;https://chameth.com/artisanal-docker-images/&#34;&gt;nice, minimal, artisanal ones&lt;/a&gt; so doesn’t ship a &lt;code&gt;go&lt;/code&gt; binary. No problem, &lt;code&gt;docker compose cp bouncer:/bnc ./bnc&lt;/code&gt; yoinks the binary out of the container, and then dumping the version shows that, yes, I am running the version I thought I was. Hmm.&lt;/p&gt;
&lt;p&gt;The next tool I reached for in &lt;a href=&#34;https://chameth.com/debugging-beyond-the-debugger/&#34;&gt;my toolbox&lt;/a&gt; was &lt;code&gt;strace&lt;/code&gt;. Maybe it’s not actually reading the file for some reason? I immediately executed &lt;code&gt;strace -p &amp;lt;pid&amp;gt; -e trace=openat,open,read,pread64 -f&lt;/code&gt; without having to look any part of that up. Yep. Definitely. Then I edited the config, &lt;code&gt;HUP&lt;/code&gt;’d the bouncer, and saw that it was… reading the config file. As it’s meant to. By default &lt;code&gt;strace&lt;/code&gt; truncates strings to 32 bytes, so I couldn’t actually see the line I’d changed. Some more definitely-not-RTFMing later, and rerunning it with an extra &lt;code&gt;-s 65536&lt;/code&gt; let me see the full config. Surprise! The config hadn’t changed!&lt;/p&gt;
&lt;p&gt;To confirm my findings, I used &lt;code&gt;docker compose cp&lt;/code&gt; again, this time yoinking the config file from inside the container. The inside config file was definitely different to the outside config file. What? My hypothesis at this point was “something something Docker nonsense”. I mount the config as read-only, and was wondering if that meant that Docker was doing something &lt;em&gt;weird&lt;/em&gt; instead of just bind mounting it. A quick trip to &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/mounts&lt;/code&gt; showed that it was, in fact, not doing anything weird, and was just bind mounting it.&lt;/p&gt;
&lt;p&gt;If the file is bind mounted, then surely it’s the same file? I ran &lt;code&gt;stat&lt;/code&gt; on the file on the host, noted the inode number, then pondered how to actually run &lt;code&gt;stat&lt;/code&gt; on the file inside the container, given the aforementioned awkwardly minimal image. The solution was easy: access it via &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/root/&lt;/code&gt;. I could’ve saved myself a bunch of &lt;code&gt;docker compose cp&lt;/code&gt; if I’d thought about that earlier. Oh well. The inode of that file was different. What?&lt;/p&gt;
&lt;p&gt;The answer was DNS. It’s always DNS. Oh, sorry, force of habit. I meant the answer was &lt;em&gt;vim&lt;/em&gt;. I was editing the config in vim, and when it saves files, by default, it writes the new content to a temporary file and does an atomic rename. That’s normally a good thing: it prevents corruption if the write fails midway through&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;. In this case, though, that means the new file has a new inode. Bind mounting a file binds to the inode, so the container just keeps clutching onto the original config from when it was started, blissfully unaware that the party has relocated down the street.&lt;/p&gt;
&lt;p&gt;This problem is likely to happen whenever you bind mount a file into a container. When I mentioned this to a friend, he immediately responded “oh yeah, never do that”, and went on to describe the horrible hacks he’s had to add to Ansible to sidestep the issue. The nicer solution is to just bind mount an entire directory if you can, as then it doesn’t matter what happens to the files within it. I really like having the config files sat alongside the Docker compose files, though; having to create a directory just to work around some bind mount weirdness upsets me.&lt;/p&gt;
&lt;p&gt;Now I knew what the problem was, I found there was an issue &lt;a href=&#34;https://github.com/moby/moby/issues/6011&#34;&gt;raised against Docker&lt;/a&gt; twelve years ago&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;. The first response was “that’s expected”, and I fully agree with the author: “respectfully, that might be expected by you, but it was not expected by me”!&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt; As a result of the issue they &lt;a href=&#34;https://github.com/moby/moby/pull/6854/changes&#34;&gt;added a nice note to the docs&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;
Many tools used to edit files including &lt;code&gt;vi&lt;/code&gt; and &lt;code&gt;sed --in-place&lt;/code&gt; may result
in an inode change. Since Docker v1.1.0, this will produce an error such as
“&lt;em&gt;sed: cannot rename ./sedKdJ9Dy: Device or resource busy&lt;/em&gt;”. In the case where
you want to edit the mounted file, it is often easiest to instead mount the
parent directory.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But in the intervening twelve years, both the note and the functionality described have gone missing. Ho-hum.&lt;/p&gt;
&lt;p&gt;I found it interesting how I’ve only just hit this problem, given how long I’ve used Docker. But I realised that almost everything I run I’m happy to just restart. Cattle, not pets, and so on. My IRC bouncer is one of the few exceptions to that. The only other thing I regularly hot reloaded was &lt;a href=&#34;https://github.com/csmith/centauri&#34;&gt;Centauri&lt;/a&gt;, my reverse proxy, but that had a whole config directory mounted because it was shared between containers, so nicely sidestepped the foot-gun.&lt;/p&gt;
&lt;p&gt;So lesson learnt: check for weird bind mount issues before raising issues about config hot reloading. It’ll join the esteemed company of “maybe the drive is full and causing completely unrelated problems?”, “perhaps everything is dog slow because the kernel ran out of entropy?”, and “did systemd sneakily take over that functionality while you weren’t looking?” in the troubleshooting checklist.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;An IRC bouncer is basically an always-on proxy. It connects to the IRC networks for you, then your clients connect to your bouncer. The bouncer can then send incoming messages to all your different clients, cache them when you’re offline and replay them later, and lots of other nice things people take for granted in their chat apps these days. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;Join/part/quit spam is part of IRC, and clients have ways of handling it, but I still &lt;em&gt;feel&lt;/em&gt; bad about doing it excessively. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;You can disable this in vim by setting &lt;code&gt;backupcopy=yes&lt;/code&gt;. Good luck redoing all the debugging if you ever accidentally remove that from your &lt;code&gt;vimrc&lt;/code&gt;, though! &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;Which is weird, because Docker can’t possibly be that old. That would make me much older than I’m prepared to accept. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;If you read the thread they didn’t actually mean to sound so dismissive, but it’s still pretty funny. “Yes, it’s expected that the foot-gun causes your foot to hurt. Duh. What did you expect?” &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&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.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-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;/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-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-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-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-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.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/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.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;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/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-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.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/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-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;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>An interesting Tailscale + Docker gotcha</title>
        <link href="https://chameth.com/tailscale-docker-gotcha/"/>
        <updated>2026-01-29T00:00:00Z</updated>
        <id>https://chameth.com/tailscale-docker-gotcha/</id>
        <content xml:lang="en" type="html">&lt;p&gt;As I’ve &lt;a href=&#34;https://chameth.com/how-i-use-tailscale/&#34;&gt;written&lt;/a&gt; &lt;a href=&#34;https://chameth.com/avoiding-the-consequences-of-dumb-laws-with-tailscale/&#34;&gt;about&lt;/a&gt; &lt;a href=&#34;https://chameth.com/exposing-game-servers-over-tailscale/&#34;&gt;before&lt;/a&gt;, I use &lt;a href=&#34;https://tailscale.com/&#34;&gt;Tailscale&lt;/a&gt; for a lot of things. I thought I had it set up in a reasonably secure manner, but I recently noticed a problem.&lt;/p&gt;
&lt;p&gt;I use Tailscale’s ACLs to limit what each node can access, based on the tags I apply to it. So an &lt;code&gt;app&lt;/code&gt; node can’t access anything via Tailscale, while an &lt;code&gt;integration&lt;/code&gt; or &lt;code&gt;server&lt;/code&gt; node can access things tagged with either &lt;code&gt;app&lt;/code&gt; or &lt;code&gt;integration&lt;/code&gt;. This is expressed pretty simply in the Tailscale ACL JSON:&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-p&#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-nt&#34;&gt;&amp;#34;tagOwners&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#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;// Servers that can be SSH&amp;#39;d into
&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-nt&#34;&gt;&amp;#34;tag:server&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#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;// Applications that are exposed on tailscale but never connect out
&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-nt&#34;&gt;&amp;#34;tag:app&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#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;// Things which talk to other services over tailscale (connecting to apps etc)
&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-nt&#34;&gt;&amp;#34;tag:integration&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#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&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-nt&#34;&gt;&amp;#34;grants&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#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&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;// Users can access everything
&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-nt&#34;&gt;&amp;#34;src&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;autogroup:member&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-nt&#34;&gt;&amp;#34;dst&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-nt&#34;&gt;&amp;#34;ip&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;  &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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&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&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;// Servers and integrations can access integrations and apps
&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-nt&#34;&gt;&amp;#34;src&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:server&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:integration&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-nt&#34;&gt;&amp;#34;dst&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:integration&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:app&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-nt&#34;&gt;&amp;#34;ip&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;  &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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&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&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Over the past week I’ve been setting up a private &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt; instance behind Tailscale, complete with an actions runner that runs things using a docker-in-docker container. I didn’t want the runner knowing anything about Tailscale, so I had it configured to speak to Forgejo direct over HTTP (&lt;code&gt;forgejo:3000&lt;/code&gt;) instead of using full Tailscale HTTPS URL that I use when accessing it (&lt;code&gt;http://git.example-net.ts.net/&lt;/code&gt;)&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Everything was going fine, until I forgot to do that translation… and it worked. My Forgejo action runners could access anything on my tailnet. I run several things on Tailscale that just have authentication turned off, on the basis that only authorised devices can access them. Things like the admin interface for this website. I definitely didn’t intend for any workflow I run on my git server to have access to edit my website!&lt;/p&gt;
&lt;p&gt;This wasn’t just limited to Forgejo, either. Any docker container I was running could access the tailnet. In hindsight it’s fairly obvious why: the host is running Tailscale, connected as a node tagged with my &lt;code&gt;server&lt;/code&gt; tag. That creates a &lt;code&gt;tailscale0&lt;/code&gt; interface, and automagically sets up iptables rules to route Tailscale traffic over the interface. Docker also automagically sets up iptables rules to bridge traffic, and apparently these two sets of rules interact in such a way that traffic from Docker containers is allowed to route via the &lt;code&gt;tailscale0&lt;/code&gt; interface.&lt;/p&gt;
&lt;p&gt;I say it’s fairly obvious in hindsight — there’s no reason why Docker would special case any particular host interface after all — but it still feels pretty surprising. Because both bits of software inject their own iptables rules, I never really had a good mental model for how they interact. The host Tailscale node was a completely separate building block to Docker. It would be a pain to use either of them if they didn’t do these rules, but it’s also one of the reasons I don’t really like “magical” things&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;There are lots of ways to fix this, but none of them feel particularly great. You can configure both Tailscale and Docker to not automatically fiddle with iptables and handle the rules yourself, but I really hate dealing with iptables&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;. As a stopgap I did hold my nose and add some iptables rules to drop traffic to the &lt;code&gt;tailscale0&lt;/code&gt; interface if it originated from the IP ranges that Docker was configured to use:&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;iptables -I DOCKER-USER -s 192.168.0.0/16 -o tailscale0 -j DROP
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;iptables -I DOCKER-USER -s 172.17.0.0/12 -o tailscale0 -j DROP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;DOCKER-USER&lt;/code&gt; chain is a nice little escape hatch; it comes before the main auto-generated &lt;code&gt;DOCKER&lt;/code&gt; chain, and Docker leaves the rules in it alone.&lt;/p&gt;
&lt;p&gt;So I added these rules, and felt pretty good about myself, and then… everything started breaking in weird ways. After some debugging I realised the problem was DNS&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;. The server’s DNS resolver is &lt;code&gt;100.100.100.100&lt;/code&gt;, a special Tailscale address. This is so that it can resolve tailnet hostnames via &lt;a href=&#34;https://tailscale.com/kb/1081/magicdns&#34;&gt;MagicDNS&lt;/a&gt; (and so I don’t have to configure my custom DNS servers manually on each device; Tailscale does it for me). My new iptables rules inadvertently dropped all the DNS packets coming from docker containers. D’oh.&lt;/p&gt;
&lt;p&gt;Obviously the solution here is to double down and add MORE iptables rules:&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;iptables -I DOCKER-USER -s 192.168.0.0/16 -d 100.100.100.100 -o tailscale0 -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;iptables -I DOCKER-USER -s 172.17.0.0/12 -d 100.100.100.100 -o tailscale0 -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This got DNS working again, but it all feels a bit gross. Not least because to actually make these persist you need to use &lt;code&gt;iptables-save&lt;/code&gt;, and then you also get all the rules that both Docker and Tailscale have inserted. I came across &lt;a href=&#34;https://github.com/giangi/iptables-docker-filter&#34;&gt;a script&lt;/a&gt; to filter out the Docker ones, but… yuck&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I realised a cleaner idea might just be to lock down what &lt;code&gt;server&lt;/code&gt; tagged nodes could access within Tailscale itself. That way I can avoid touching iptables at all. It’s handy to be able to &lt;code&gt;curl&lt;/code&gt; services from the host when debugging, but it’s not really necessary. So I removed that access, and… stuff broke again. I’m using my Forgejo instance as a registry for some of the docker images I run, so the docker daemon needs to be able to reach it. I ended up making a new tag for &lt;code&gt;infrastructure&lt;/code&gt;, which can be accessed from &lt;code&gt;server&lt;/code&gt; devices. This does still allow all the docker containers to reach Forgejo, but I already have it set up with appropriate access controls and public/private repository splits. Forgejo is a service designed to run publicly, so this seems a reasonable trade-off for convenience. I used the &lt;code&gt;tests&lt;/code&gt; feature of Tailscale’s ACL config to make sure I’d got the rules right:&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-p&#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-nt&#34;&gt;&amp;#34;tests&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#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&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;// Servers can only access infrastructure
&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-nt&#34;&gt;&amp;#34;src&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;   &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:server&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-nt&#34;&gt;&amp;#34;proto&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tcp&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-nt&#34;&gt;&amp;#34;allow&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#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-s2&#34;&gt;&amp;#34;tag:infrastructure:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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&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-nt&#34;&gt;&amp;#34;deny&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#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-s2&#34;&gt;&amp;#34;100.84.16.43:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-s2&#34;&gt;&amp;#34;me@example.com:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-s2&#34;&gt;&amp;#34;tag:server:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-s2&#34;&gt;&amp;#34;tag:app:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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-s2&#34;&gt;&amp;#34;tag:integration:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#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&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&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&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The whole situation still feels a bit messy. If I ever get around to switching to nftables I might loop back and manually craft some rules for routing traffic, instead of leaving Tailscale and Docker to do their own thing.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;I discovered later on that logging into the container registry at &lt;code&gt;forgejo:3000&lt;/code&gt; actually issued a redirect to &lt;code&gt;https://git.example-net.ts.net/&lt;/code&gt; so this was all basically for naught… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;I didn’t use Tailscale’s MagicDNS for a long time just because the word “magic” put me off. Only when I eventually got around to learning how it worked, and seeing that it wasn’t really that magical under the hood, did I change my mind. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;nftables seems far better in lots of ways, but I can’t really be bothered migrating. Maybe next time I reimage the server for whatever reason… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;It’s always DNS… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;Again, nftables would almost certainly help here. It actually has (gasp) configuration files. But again, I really didn’t want to spend the time migrating. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Monthly Meanderings: December 2025</title>
        <link href="https://chameth.com/monthly-meanderings-2025-12/"/>
        <updated>2026-01-01T00:00:00Z</updated>
        <id>https://chameth.com/monthly-meanderings-2025-12/</id>
        <content xml:lang="en" type="html">&lt;p&gt;For a while I’ve been idly thinking about a way to get smaller bits of content
onto my website without it being too annoying for me, or too hard to consume. Things
like interesting links, small project updates, and so on.&lt;/p&gt;
&lt;p&gt;I didn’t immediately come up with anything I was happy with, so just sat on it.
Ignoring the problem seems to work surprisingly well for things like this.
I’d been meaning to redesign the &lt;a href=&#34;https://chameth.com/about&#34;&gt;about page&lt;/a&gt; for a while, but wasn’t
sure what to do with all the interests I wanted to list. After ignoring the
problem for a while I came across the &lt;a href=&#34;https://chrisburnell.github.io/interests-directory/&#34;&gt;interests directory&lt;/a&gt;
and realised I could just bundle all those things into their own page.&lt;/p&gt;
&lt;p&gt;This seems amazingly obvious in hindsight, but I think I needed a bit
of a push to accept that a page just about my interests was… well…
interesting. Anyway, as the end of December rolled around, I got a similar
dose of inspiration: a flurry of monthly round-up posts from various blogs
I follow. If I bundle up all the small bits and bobs, it’ll make something
vaguely blog post shaped, and writing and consuming those is already a
solved problem. Again, pretty obvious in hindsight, and again I think I
just needed a nudge to shut up the little voice in my head saying “but those
people all have interesting things to say… do you, really?”&lt;/p&gt;
&lt;p&gt;So without further ado, welcome to the inaugural post in my new monthly
meanderings series. If you have any feedback on the format or contents,
please drop me a note with the contact form at the bottom!&lt;/p&gt;
&lt;h3 id=&#34;website-updates&#34;&gt;Website updates&lt;/h3&gt;
&lt;p&gt;As already mentioned, I reworked the &lt;a href=&#34;https://chameth.com/about&#34;&gt;about page&lt;/a&gt; and split out
a new &lt;a href=&#34;https://chameth.com/interests&#34;&gt;interests page&lt;/a&gt;. The old about page had a few lists
of interests show up in thought bubbles when you hovered over things,
which was a cool effect on desktop, but barely worked at all on mobile.
The new page gives me a lot more room for waffle, and I do like waffling.&lt;/p&gt;
&lt;p&gt;Only a single new blog post this month:
&lt;a href=&#34;https://chameth.com/exposing-game-servers-over-tailscale/&#34;&gt;exposing game servers over Tailscale&lt;/a&gt;.
Just a quick how-to I wrote up, having not found it documented usefully
anywhere before. I have a bunch of ideas for posts I want to write, but
none of them have really sparked joy quite yet, so I’m once again employing
the “sit on them for a bit and maybe inspiration will strike” approach.&lt;/p&gt;
&lt;h3 id=&#34;other-projects&#34;&gt;Other projects&lt;/h3&gt;
&lt;p&gt;I created a small collection of Docker related projects this month:
&lt;a href=&#34;https://github.com/csmith/containuum&#34;&gt;containuum&lt;/a&gt; is a go library that
helps with listing containers, and keeping that list up-to-date. On top
of that I built &lt;a href=&#34;https://github.com/csmith/centauri-docker-confd&#34;&gt;centauri-docker-confd&lt;/a&gt;
which generates a config for &lt;a href=&#34;https://github.com/csmith/centauri&#34;&gt;centauri&lt;/a&gt;
(my reverse proxy) based on running Docker containers. I already had a
way of doing that, but containuum is designed to be a bit more robust,
and I made it so the config can be passed over a network connection
instead of having to write it to disk and then signal the proxy to reload.&lt;/p&gt;
&lt;p&gt;The next project has a bit of backstory: after reading
&lt;a href=&#34;https://blog.jakesaunders.dev/my-server-started-mining-monero-this-morning/&#34;&gt;I got hacked, my server started mining Monero this morning&lt;/a&gt;
[warning: the article is interesting but a bit LLM-sloppy] I realised that I, too, was
running a vulnerable version of Umami. Luckily, as far as I can tell, it wasn’t
exploited. But it got me looking into automated vulnerability scanning. I don’t
put much stock into it normally, as you get so many false alerts, but this is one
case where it would have definitely helped. There didn’t seem to be any
good solution for my “middle-of-the-road” approach to containers (lots of containers,
but not needing Kubernetes), so I wrote &lt;a href=&#34;https://github.com/csmith/purser&#34;&gt;purser&lt;/a&gt;.
It takes every image used by a running container, runs them through
&lt;a href=&#34;https://github.com/aquasecurity/trivy/&#34;&gt;Trivy&lt;/a&gt;, and generates a combined report.
I’ll add some form of active notifications to it in the future.&lt;/p&gt;
&lt;h3 id=&#34;entertainment&#34;&gt;Entertainment&lt;/h3&gt;
&lt;p&gt;This month I’ve been playing a lot of Hogwarts: Legacy, after picking it up
for free on Epic Games. I grew up with the Harry Potter books coming out,
and while I’m not a super-fan, it still has a special appeal to me.
Obviously she-who-must-not-be-named is doing her best to out-evil Voldemort,
but I think you can still enjoy the universe despite that. Especially when
you don’t have to contribute any money that might go towards her.&lt;/p&gt;
&lt;p&gt;Anyway, the game is surprisingly fun. They did a really good job of making
it feel like an open world. You &lt;em&gt;want&lt;/em&gt; to walk around from place to place
to see everything, find all the various collectables, and so on. Compared
to Far Cry or Cyberpunk the world is a lot smaller but that means it’s far
more densely packed with stuff to do. I initially didn’t vibe with the
way combat works: it’s pretty heavily reliant on dodging or shielding against
incoming attacks, which reminded me a lot of Clair Obscur: Expedition 33.
Fortunately, it’s a lot more forgiving in Hogwarts: Legacy and I got used
to it after not too long.&lt;/p&gt;
&lt;p&gt;It’s been a relatively sedate month for other forms of entertainment.
I saw Fackham Hall and Wicked: For Good at the cinema. The former was
a funny romp, vaguely reminiscent of Airplane or The Naked Gun, as you’d
expect from something penned by Jimmy Carr. Wicked was… fine? I wasn’t
overly enamoured with part one, so it’s not a surprise I didn’t vibe with
the follow-up. If you smushed them into a single normal-length feature I think they
would have made a really good film, but two films both over two hours
long is a bit much for what actually happens.&lt;/p&gt;
&lt;h3 id=&#34;around-the-web&#34;&gt;Around the web&lt;/h3&gt;
&lt;h4 id=&#34;wikipedia-list-of-citogenesis-incidentshttpsenwikipediaorgwikiwikipedialist-of-citogenesis-incidents&#34;&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Wikipedia:List_of_citogenesis_incidents&#34;&gt;Wikipedia: List of citogenesis incidents&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;A list of incidents where (often false) information on Wikipedia has ended up
becoming its own source. That led to Mark Dominus’ blog post titled
&lt;a href=&#34;https://blog.plover.com/wikipedia/ysolo.html&#34;&gt;Imaginary Albanian eggplant festivals… IN SPACE&lt;/a&gt;
where he discovers that a mountain on Ceres got named after a made-up festival
(and then got it fixed!)&lt;/p&gt;
&lt;p&gt;Not relying on Wikipedia as a source was drummed into us at school, but
it feels like this sort of thing is only going to get worse and worse now
LLMs are on the scene. I assume we’re already at the point where LLM
hallucinations are prevalent enough that they’ll find their way into
the training data for future iterations.&lt;/p&gt;
&lt;h4 id=&#34;simon-collisons-websitehttpscollycom&#34;&gt;&lt;a href=&#34;https://colly.com/&#34;&gt;Simon Collison’s website&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;I really like the design here. Both the homepage and how well every
journal entry is formatted. It makes me feel bad for the many walls of
text that adorn this site. The &lt;a href=&#34;https://colly.com/v4/&#34;&gt;archived v4 design&lt;/a&gt;
is also wonderful.&lt;/p&gt;
&lt;h4 id=&#34;my-dinner-with-skinnerhttpswwwyoutubecomwatchvpk-oq8iytva&#34;&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=pk-Oq8iYtVA&#34;&gt;My Dinner with Skinner&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;A 43-minute-long feature film that is “just” the Steamed Hams gag from
The Simpsons done in the style of My Dinner with Andre. There’s a (much shorter)
&lt;a href=&#34;https://www.youtube.com/watch?v=1Ecg_5r1L1M&#34;&gt;behind-the-scenes video&lt;/a&gt; that
goes over the extreme amount of effort that was put into it, too.&lt;/p&gt;
&lt;p&gt;I’m still trying to wrap my head around this. It’s… something.&lt;/p&gt;
&lt;h4 id=&#34;delete-spotify-sure-but-dont-just-replace-it-with-another-subscriptionhttpsstephveecablogentertainmentdelete-spotify-sure-but-dont-just-replace-it-with-another-subscription&#34;&gt;&lt;a href=&#34;https://stephvee.ca/blog/entertainment/delete-spotify-sure-but-dont-just-replace-it-with-another-subscription/&#34;&gt;Delete Spotify? Sure, But Don’t Just Replace it With Another Subscription&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;A similar take to my own &lt;a href=&#34;https://chameth.com/escaping-spotify-the-hard-way/&#34;&gt;escaping Spotify the hard way&lt;/a&gt;,
but it avoids getting lost in the technical weeds like I did. (In my defence,
they’re very fun weeds to get lost in.) Stephanie is
also one of the people that does monthly wrap-ups, and I was pleasantly
surprised when &lt;a href=&#34;https://stephvee.ca/blog/rewinds/rewind-dec-2025/&#34;&gt;the latest one&lt;/a&gt;
popped up in my RSS reader and I saw my own site linked.&lt;/p&gt;
</content>
    </entry>
</feed>
