<?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 docker-automatic-nginx-proxy, home-automation-without-megacorps, understanding-docker-volume-mounts, why-you-should-be-using-https</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/docker-automatic-nginx-proxy,home-automation-without-megacorps,understanding-docker-volume-mounts,why-you-should-be-using-https/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2026-04-09T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <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>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>Home Automation Without the Megacorps</title>
        <link href="https://chameth.com/home-automation-without-megacorps/"/>
        <updated>2025-05-21T00:00:00Z</updated>
        <id>https://chameth.com/home-automation-without-megacorps/</id>
        <content xml:lang="en" type="html">&lt;p&gt;I first experimented with home automation in 2016, by picking up a Samsung
“SmartThings” hub. It was terrible. The UI to configure things was slow and
clunky, firmware updates were applied whether you wanted them or not, and
everything stopped working if their cloud services stopped. You were also locked
into whatever integrations they deigned to support, of course. After that broke
for the umpteenth time I scaled back and for years the closest I got to home
automation was a couple of Hue bulbs.&lt;/p&gt;
&lt;p&gt;Recently I’ve been building it out again, though. This time using off-the-shelf
components that interop using Zigbee, open-source software, and some code I
wrote myself. It’s great; it runs entirely locally and has had basically zero
downtime. The Zigbee ecosystem lets me integrate all sorts of things without
having to spend lots of money on “smart” alternatives. I think I’ve spent less
on this incarnation than I did on the original SmartThings hub all those years
ago (even without adjusting for inflation!).&lt;/p&gt;
&lt;h3 id=&#34;my-current-setup&#34;&gt;My current setup&lt;/h3&gt;
&lt;p&gt;I run everything on a Raspberry Pi 4, with a Sonoff USB Zigbee adapter based
on the CC2652P chipset&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;. Interfacing with the Zigbee stack is handled by
&lt;a href=&#34;https://www.zigbee2mqtt.io/&#34;&gt;zigbee2mqtt&lt;/a&gt; (z2m for short), an open-source project that
basically bridges your devices to an MQTT broker&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;. When a device reports some
data, it will send a new message over MQTT; when you want to make a device do
something you just post a message back. It’s incredibly lightweight, but
supports a huge array of devices out of the box. And as it’s just using MQTT,
it’s trivial to integrate with other software or build on top of.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;I know a lot of people building out this kind of home automation use
&lt;a href=&#34;https://www.home-assistant.io/&#34;&gt;Home Assistant&lt;/a&gt;, but I don’t get on with it
terribly well. It felt incredibly sluggish, and the entire project is just a
bit… much. I want a binary or package or docker image I can just run, not an
entire operating system. That’s not how computers are meant to work! In contrast,
z2m is simple to set up, super light weight and responsive.&lt;/p&gt;
&lt;p&gt;Anyway. z2m exposes Zigbee devices over MQTT, so I wrote some code in Go to
connect to the MQTT broker, and listen to the messages. It’s grown a bit beyond
this now, and I’m skipping some boring bits like error handling and JSON
parsing, but at first I had something 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;for&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;message&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;topic&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;err&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;c&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;ReadSlices&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&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-k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nb&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;topic&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-o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;&amp;#34;zigbee2mqtt/desk-button&amp;#34;&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-k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;strings&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;Contains&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;message&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-s&#34;&gt;&amp;#34;single&amp;#34;&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;c&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;Publish&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-kc&#34;&gt;nil&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-nb&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;&amp;#34;{\&amp;#34;state\&amp;#34;: \&amp;#34;TOGGLE\&amp;#34;}&amp;#34;&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-s&#34;&gt;&amp;#34;zigbee2mqtt/room-lights/set&amp;#34;&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-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 think I actually made this harder to read by trying to simplify it here, but
hopefully you can follow that it was relatively straight forward to listen
for a particular action to happen (in this case me “single”-pressing on the
device called “desk-button”) and then make another device do something in
response (toggling the state of the “room-lights” device).&lt;/p&gt;
&lt;p&gt;So what actually are these devices? At present in z2m I have the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some generic buttons&lt;/li&gt;
&lt;li&gt;An air quality sensor (USB powered), and a separate temperature sensor (battery powered)&lt;/li&gt;
&lt;li&gt;A light switch&lt;/li&gt;
&lt;li&gt;Some 240V relay modules, that I use to turn non-smart devices on and off&lt;/li&gt;
&lt;li&gt;Some USB relay modules, for the same purpose&lt;/li&gt;
&lt;li&gt;Some “smart” plugs that I mostly use to monitor power usage&lt;/li&gt;
&lt;li&gt;A motorised blind roller&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I think all of these are either Tuya or Moes branded, and were all sourced from
AliExpress. You can get the same things from Amazon or elsewhere, but they tend
to be 3-4x more expensive for the same product.&lt;/p&gt;
&lt;p&gt;One of the cool things about Zigbee devices is that the powered ones work
together to create a mesh network, so you don’t have to worry about network
repeaters or signal strength like you do with Wi-Fi networks. z2m even makes
a map showing the connections:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/zigbee-map.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/zigbee-map.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/home-automation-without-megacorps/zigbee-map.png&#34; alt=&#34;A &amp;#34;map&amp;#34; showing how Zigbee devices connect to one another&#34; loading=&#34;lazy&#34; width=&#34;1093&#34; height=&#34;651&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The network map produced by z2m. You can see how all the powered devices form a mesh that the lower power ones can connect to.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&#34;some-interesting-automations&#34;&gt;Some interesting automations&lt;/h3&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/relay.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/relay.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/home-automation-without-megacorps/relay.jpg&#34; alt=&#34;A Zigbee relay and some wago connectors wired up in a project box&#34; loading=&#34;lazy&#34; width=&#34;325&#34; height=&#34;500&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;A Zigbee relay spliced into the power cable for the fan&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So what do I actually do with all of this? Some of it is just what I’ll call
“laziness automation”: I want to be able to turn on the lights without getting
up and walking to the light switch. So there’s a Zigbee button on my desk that
does it. Then there are some less manual automations: my blinds are
automatically closed at sunset&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;, for example. It feels a bit trivial, but it’s
surprisingly nice just not to have to think about that at all.&lt;/p&gt;
&lt;p&gt;Another nice quality of life automation is the light I have above my monitor.
It’s USB powered, and I now have it going through a Zigbee USB relay that lets
it be toggled on and off. Then I have a small agent running on my desktop that
turns the light on when the computer is unlocked, and off when it locks or
shuts down. I keep meaning to make a “film mode” that detects when I’m watching
a film, and turning all the lights and blinds down, but I haven’t got around
to it yet.&lt;/p&gt;
&lt;p&gt;The most complex automation is probably for a window fan. It’s not smart in
any way, so I cut the power cable and inserted a Zigbee relay. The whole thing
is housed in a little project box to keep it secure. The relay basically acts
as a switch: the live wire leading to the fan runs to the “normally open”&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;
contact, while the upstream power goes to “common”. When the relay is told to
turn the fan on, the two get connected and the fan gets power. Oh, it’s also
not actually a window fan, but I designed and 3D printed some adapters so that
it sits right in the window.&lt;/p&gt;
&lt;p&gt;So now we have a window fan that can be turned off and on automatically. But I don’t
want to have to press a button to do that. I’m lazy, remember? Instead, I made
it so that the Go code constantly monitors the temperature reported by the air
quality sensor, and queries the free &lt;a href=&#34;https://openweathermap.org/&#34;&gt;OpenWeatherMap API&lt;/a&gt;
to get the rough temperature outside. It can then turn the fan on if the room is
too hot, and outside is cool enough to make a difference (there’s no point in
blowing hotter air in!).&lt;/p&gt;
&lt;figure class=&#34;image left&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/fan-graph.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/fan-graph.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/home-automation-without-megacorps/fan-graph.png&#34; alt=&#34;A graph of room temperature, slowly rising to 23.5 degrees, then sharply dropping to 19, rising to 20, dropping to 19 again, etc&#34; loading=&#34;lazy&#34; width=&#34;540&#34; height=&#34;500&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;A graph of room temperature, showing the effect of the fan being turned on and off&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I don’t want it to do that all the time though. The room doesn’t need to be
cooled if I’m not around. There are various Zigbee presence sensors you can get,
but the cheaper ones seem to be of dubious quality. Instead, I wrote some code
to guess whether I’m present. It uses the state of the monitor light
as a proxy for “is the computer in use”, and then makes some guesses based on
the last time the computer was used and the current time (if I turn the computer
off at midnight it probably means I’m going to sleep; if I turn it off at 2pm
it probably means I’m going out somewhere).&lt;/p&gt;
&lt;p&gt;You can see in the graph that the fan has a decent impact on temperature. I
coded it to cool the room to 19 degrees, but then not turn on again until it
was above 20. This prevents it flip-flopping on and off constantly. You can
clearly see the pattern in the graph, as it actively cools and then slowly
the room warms back up. This arrangement is much better than being woken up at
5am because it’s painfully cold. Trust me.&lt;/p&gt;
&lt;h3 id=&#34;bonuses-metrics-and-3d-printer&#34;&gt;Bonuses: metrics and 3D printer&lt;/h3&gt;
&lt;p&gt;One of the things my custom Go code does is collate all the various stats
reported by the Zigbee devices, and inserts them into a &lt;a href=&#34;https://victoriametrics.com/&#34;&gt;VictoriaMetrics&lt;/a&gt;
database. I originally hosted this on the Pi itself, and it performed fine, but
I’ve since moved it onto a server so that I can use it for some other things
as well.&lt;/p&gt;
&lt;p&gt;I set up Grafana to point to VM, and can create dashboards showing power usage,
what devices are turned on when, and a bunch of environmental conditions. This
also makes it easy to spot how good the data coming from the devices are. For
example, the air quality sensor reports a figure for the amount of Carbon Dioxide
in the air, as well as the amount of Volatile Organic Compounds (VOCs). The
graphs are basically identical, but on a different scale. It turns out this
particular device has no actual way of detecting CO₂, so it just “calculates”
it from the VOCs figure. Useful to know if you want to actually use that data.&lt;/p&gt;
&lt;p&gt;Recently I came across &lt;a href=&#34;https://github.com/torbenconto/bambulabs_api&#34;&gt;a go library for interacting with Bambu Lab printers&lt;/a&gt;,
so I’ve also hooked that into my automation. It exports metrics about the
printer, so I now have a way of seeing what’s going on when I’m not physically
present&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;. I even added a HTTP endpoint (exposed over &lt;a href=&#34;https://tailscale.com/&#34;&gt;Tailscale&lt;/a&gt;) that
shows pictures from the built-in camera.&lt;/p&gt;
&lt;p&gt;On the automation front, I’ve made it automatically turn off the printer’s light
when it finishes a print, and also added push notifications via &lt;a href=&#34;https://pushover.net/&#34;&gt;PushOver&lt;/a&gt;
whenever the state changes. No longer will I be sat in another room blissfully
unaware it ran out of filament seconds after I walked out the door!&lt;/p&gt;
&lt;h3 id=&#34;was-it-worth-it&#34;&gt;Was it worth it?&lt;/h3&gt;
&lt;p&gt;These days you could probably just buy some kind of ‘smart home’ hub that works
well enough, and do most of what I’ve done with a lot less effort and no coding.
I’m still of the opinion that for something so essentially &lt;em&gt;local&lt;/em&gt;, it should
itself be managed entirely locally. I don’t trust companies like Google or
Amazon not to kill their products, or change or remove an API I rely on.&lt;/p&gt;
&lt;p&gt;Obviously the “Not Invented Here” approach of coding everything yourself doesn’t
suit everyone, but as someone who enjoys coding and enjoys having things work
&lt;em&gt;just so&lt;/em&gt; it works very well for me.&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;It turns out the chipset is important. I initially used a similar Sonoff
dongle that used a CC2531 chipset, and its performance wasn’t great. It often
failed to pair with new devices, and dropped links to existing ones. The
&lt;a href=&#34;https://www.zigbee2mqtt.io/advanced/zigbee/02_improve_network_range_and_stability.html&#34;&gt;zigbee2mqtt docs&lt;/a&gt;
do explicitly advise against the CC2531 chips for that reason. At the time they
recommended CC2652Ps, so that’s what I went with. If you’re starting new I’d
go with whatever their latest recommendation was. &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;MQTT is basically a network protocol for publishing and subscribing to
arbitrary messages. A broker is the thing that sits in the middle and routes
the messages. I use &lt;a href=&#34;https://mosquitto.org/&#34;&gt;Mosquitto&lt;/a&gt; but any will do. &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;They originally also re-opened at sunrise but that was a terrible
mistake. Who knew the sun rose so early?! &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;“open” meaning “there’s a gap so it doesn’t work” not
“open for business”. This confusion in terminology also extends to drawbridges. &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;I use the printer in LAN mode, which means there’s no way to monitor it
from a phone, even if you’re connected to the same network still. &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>How I Get Things Done</title>
        <link href="https://chameth.com/how-i-get-things-done/"/>
        <updated>2024-09-01T00:00:00Z</updated>
        <id>https://chameth.com/how-i-get-things-done/</id>
        <content xml:lang="en" type="html">&lt;p&gt;I always quite like reading about how other people &lt;em&gt;do things&lt;/em&gt;. What software
or hardware they use, or how they manage reminders, todo lists, and so on.
I’ve never actually written about how I do any of that, though. So here it is!&lt;/p&gt;
&lt;h3 id=&#34;productivity&#34;&gt;Productivity&lt;/h3&gt;
&lt;p&gt;In the past I’ve fallen victim to the idea of there being One True Productivity
System that would solve all my problems and make me amazing at getting things
done. The title of this post is a nod to Getting Things Done, which I’ve read
and tried to religiously follow in the past, but it’s just not for me. One thing
that did actually stick from it, though, was the idea of “open loops”:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Anything that does not belong where it is, the way it is, is an “open loop,”
which will be pulling on your attention if it’s not appropriately managed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If I want to do something and it’s not recorded in a way I trust, it
weighs on me a little. Those little weights all add up, and just make life
uncomfortable. I deal with those in a few ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Inbox Zero-ish: if an e-mail needs me to do something, it sits in my inbox
until it’s done. When it’s done, it gets archived.&lt;/li&gt;
&lt;li&gt;Notes: random things I try to remember I put into my notes, so I can search
for them later.&lt;/li&gt;
&lt;li&gt;Budget: anything monetary I just adjust in my budget or create a category for.&lt;/li&gt;
&lt;li&gt;Todo list: any other kind of task I want/need to do either now or at some
point in the future goes into Todoist.&lt;/li&gt;
&lt;/ul&gt;
&lt;!--more--&gt;
&lt;h4 id=&#34;todoist&#34;&gt;Todoist&lt;/h4&gt;
&lt;p&gt;In the past I’ve gone through a lot of different todo systems, once again
hunting for one that will fix all my problems etc&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;. In the end I always ended
up coming back to &lt;a href=&#34;https://todoist.com/&#34;&gt;Todoist&lt;/a&gt;, and have stuck with it for a
decent period now.&lt;/p&gt;
&lt;p&gt;In Todoist, I have everything split up into projects. Work projects are one
colour and personal projects are another. Anything that needs more than a
handful of individual tasks gets a project, which is archived when it’s done.
Any one-off things that don’t neatly fall into projects go into “Random junk”
project.&lt;/p&gt;
&lt;p&gt;As I’m writing this, I have 116 items in 22 projects. A good deal of those are
things that I might want to do at some point, not things that I actively need
to worry about. If I want to get one of them done, I schedule it so it shows
up in the “today” view. I have the Todoist desktop app open automatically when
I login to my computer, and it gets positioned on the right hand side of my
ultra-wide monitor, so I can see the “today” view and add/remove things
immediately at any point.&lt;/p&gt;
&lt;p&gt;I’m not precious about tasks becoming overdue, or unscheduling things I’d
previously scheduled. Priorities change, ideas that sounded good a week ago
might no longer sound good, and so on.&lt;/p&gt;
&lt;p&gt;I do a few things to help me manage tasks and keep track of things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I have a monthly recurring task to look through everything that’s not
scheduled, just so I keep a rough idea of what’s there, and can update or
get rid of it as needed.&lt;/li&gt;
&lt;li&gt;I wrote &lt;a href=&#34;https://github.com/csmith/todoistager&#34;&gt;todoistager&lt;/a&gt; and have it
running on a server to automatically apply tags to tasks based on their rough
age: &lt;code&gt;weeks&lt;/code&gt;, &lt;code&gt;months&lt;/code&gt; or &lt;code&gt;years&lt;/code&gt;. This lets me identify things that are
lingering and reconsider them&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;/li&gt;
&lt;li&gt;I automate adding tasks for things that demand my attention, like incoming
GitHub pull requests. The Todoist API is nice and straight forward, and
hooking it up to things is trivial.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;other-desktop-software&#34;&gt;Other desktop software&lt;/h3&gt;
&lt;p&gt;For notes I use &lt;a href=&#34;https://logseq.com/&#34;&gt;Logseq&lt;/a&gt;, and I just put whatever I need
to write under today’s note and rely on search or tags to find it again when
I need to. I note basically anything that I might like to use again in the
future. One of my most common tags is &lt;code&gt;#blub&lt;/code&gt;, a term I came across in
the excellent article “&lt;a href=&#34;https://www.benkuhn.net/blub/&#34;&gt;In defense of blub studies&lt;/a&gt;”.
Blub is random ultra-specific knowledge; it might not be eminently useful
to know on its own, but over time it can pay dividends.&lt;/p&gt;
&lt;p&gt;I’ve previously tried a bunch of note taking apps, and methodologies, but like
with todo lists I end up just wanting something that &lt;em&gt;just works&lt;/em&gt; and stays out
of the way. Logseq stores notes in Markdown, and I have a cronjob to commit and
push my changes to a private git repository.&lt;/p&gt;
&lt;p&gt;Like Todoist, Logseq launches when I log into my computer, and sits in the same
500px-or-so region on the right of my monitor. This allows me to switch from
Todoist to take or refer to notes when needed. To do this positioning, I use
&lt;a href=&#34;https://www.nongnu.org/devilspie2/&#34;&gt;devilspie2&lt;/a&gt;. I also use this to position
Discord and my IRC client side-by-side on a small portable monitor that sits
underneath my ultrawide.&lt;/p&gt;
&lt;p&gt;More mundane software: I use &lt;a href=&#34;https://www.jetbrains.com/idea/&#34;&gt;IntejjiJ IDEA&lt;/a&gt;
for almost all development and complex text editing&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;. The only exception is
Android development where &lt;a href=&#34;https://developer.android.com/studio&#34;&gt;Android Studio&lt;/a&gt;
rules the roost. For lighter text editing, &lt;a href=&#34;https://www.sublimetext.com/&#34;&gt;Sublime Text&lt;/a&gt;
is my choice.&lt;/p&gt;
&lt;p&gt;I spend a fair bit of the time at the command line. I use &lt;a href=&#34;https://sw.kovidgoyal.net/kitty/&#34;&gt;kitty&lt;/a&gt;
as a terminal emulator, with the &lt;a href=&#34;https://www.zsh.org/&#34;&gt;zsh&lt;/a&gt; shell and
&lt;a href=&#34;https://ohmyz.sh/&#34;&gt;Oh My Zsh&lt;/a&gt; for some nice plugins and enhancements. One thing
I couldn’t live without is &lt;a href=&#34;https://github.com/wting/autojump&#34;&gt;autojump&lt;/a&gt;, which
allows quickly jumping to any directory you’ve previously visited using a
substring of the name.&lt;/p&gt;
&lt;p&gt;Web browsing is all &lt;a href=&#34;https://www.mozilla.org/en-GB/firefox/new/&#34;&gt;Firefox&lt;/a&gt;,
with &lt;a href=&#34;https://bitwarden.com/&#34;&gt;Bitwarden&lt;/a&gt; for password management and two pinned
&lt;a href=&#34;https://www.fastmail.com/&#34;&gt;Fastmail&lt;/a&gt; tabs, one for personal e-mail and one for
work. I don’t think there’s much else to note there.&lt;/p&gt;
&lt;p&gt;Oh, I use &lt;a href=&#34;https://archlinux.org/&#34;&gt;Arch&lt;/a&gt;, by the way.&lt;/p&gt;
&lt;h3 id=&#34;infrastructure&#34;&gt;Infrastructure&lt;/h3&gt;
&lt;p&gt;This is the hodge-podge of things that support my computer usage but sit outside
the actual computer. In no particular order…&lt;/p&gt;
&lt;p&gt;All my DNS queries go through &lt;a href=&#34;https://nextdns.io/&#34;&gt;NextDNS&lt;/a&gt; using
DNS-over-HTTPS. NextDNS lets you add custom responses, block certain things
from resolving, and subscribe to pre-made anti-tracking or anti-advertising
lists. It gives you far more control over everything than most other services
I’ve looked at.&lt;/p&gt;
&lt;p&gt;Every device I control runs &lt;a href=&#34;https://tailscale.com/&#34;&gt;Tailscale&lt;/a&gt; to enable remote
access. I also use Tailscale’s SSH authentication feature for my servers,
allowing quick and easy access from my phone where I don’t have or want normal
SSH keys.&lt;/p&gt;
&lt;p&gt;One exciting service that combines these two is &lt;a href=&#34;https://github.com/tailscale/golink&#34;&gt;golink&lt;/a&gt;.
It was created by Tailscale, presumably based on the &lt;code&gt;go&lt;/code&gt; service used extensively
inside Google. I run a version on my tailnet, and have NextDNS configured to
resolve &lt;code&gt;go&lt;/code&gt; to the tailscale IP of the service. That lets me type “go/whatever”
on any machine I set up with NextDNS and Tailscale, including mobile devices.
I use go links for all sorts of things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shortcuts to tools I use commonly: &lt;code&gt;go/board&lt;/code&gt; gets updated to my current
client’s kanban board or similar tracker, whether it’s in Jira, GitHub, Asana
or whatever; &lt;code&gt;go/meet&lt;/code&gt; goes to Google Meet with the user parameter set
so I’m logged in with my work account not my personal account.&lt;/li&gt;
&lt;li&gt;In place of bookmarks: &lt;code&gt;go/flex&lt;/code&gt; goes to the
&lt;a href=&#34;https://css-tricks.com/snippets/css/a-guide-to-flexbox/&#34;&gt;CSS-Tricks flexbox guide&lt;/a&gt;&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;,
&lt;code&gt;go/cad&lt;/code&gt; goes to the &lt;a href=&#34;https://www.onshape.com/en/&#34;&gt;OnShape&lt;/a&gt; login page,
&lt;code&gt;go/hmrc&lt;/code&gt; goes to the &lt;a href=&#34;https://www.gov.uk/log-in-register-hmrc-online-services&#34;&gt;HMRC online services&lt;/a&gt;
page, and so on&lt;/li&gt;
&lt;li&gt;A few dynamic things: &lt;code&gt;go/github/chameth.com&lt;/code&gt; will go to the &lt;code&gt;csmith/chameth.com&lt;/code&gt;
project on GitHub; &lt;code&gt;go/ref/pipico&lt;/code&gt; will go to the &lt;a href=&#34;https://ref.c5h.io/view/pipico&#34;&gt;Pi Pico page&lt;/a&gt;
on my electronics reference wiki.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my servers I have a minimal &lt;a href=&#34;https://www.ansible.com/&#34;&gt;Ansible&lt;/a&gt; setup that
does some basic configuration and deploys SSH keys to the right places. Almost
everything runs inside Docker, and I use my own software for HTTP proxying:
&lt;a href=&#34;https://github.com/csmith/dotege&#34;&gt;Dotege&lt;/a&gt; uses tags I apply to containers to
build a config for &lt;a href=&#34;https://github.com/csmith/centauri&#34;&gt;Centauri&lt;/a&gt; which handles
reverse proxying, using &lt;a href=&#34;https://letsencrypt.org/&#34;&gt;Let’s Encrypt&lt;/a&gt; for TLS certs.
At the minute I have two servers: a dedicated server that runs basically all my
services (including this website), and a little VPS for monitoring and backups.
Both are from &lt;a href=&#34;https://www.hetzner.com/&#34;&gt;Hetzner&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wrote my own basic monitoring software called &lt;a href=&#34;https://github.com/csmith/goplum&#34;&gt;Goplum&lt;/a&gt;
to make sure things that should be running on my servers actually are. If it
fails checks it sends a notification to my phone using &lt;a href=&#34;https://pushover.net/&#34;&gt;Pushover&lt;/a&gt;,
and a message to a channel on my private IRC server (running &lt;a href=&#34;https://github.com/ergochat/ergo/&#34;&gt;Ergo&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id=&#34;hardware&#34;&gt;Hardware&lt;/h3&gt;
&lt;p&gt;For the last two years I’ve used a laptop as my main computer&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;. It’s a Dell G15
Special Edition. After some upgrades its specs are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intel i7-12700H processor&lt;/li&gt;
&lt;li&gt;NVIDIA GeForce RTX 3060 graphics card&lt;/li&gt;
&lt;li&gt;2x32GB DDR5 RAM&lt;/li&gt;
&lt;li&gt;2TB Gen4 SSD&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At home it sits behind my &lt;a href=&#34;https://www.dell.com/en-uk/shop/alienware-34-curved-qd-oled-gaming-monitor-aw3423dwf/apd/210-bfrq/monitors-monitor-accessories&#34;&gt;Alienware AW3423DWF&lt;/a&gt;
monitor. This is a Quantum Dot OLED monitor, and I bought it almost immediately
upon seeing someone else use it at a LAN event. As did three other people that
were sat near us. It just looks &lt;strong&gt;so&lt;/strong&gt; good. I wasn’t sure I’d get on with
a widescreen monitor, and made sure it supported picture-by-picture before
buying it, but have never actually used that. I generally run whatever I’m
working on at nearly-fullscreen, then have Todoist or Logseq on the far right.
Games go fullscreen over everything, of course.&lt;/p&gt;
&lt;p&gt;Above the widescreen I have a &lt;a href=&#34;https://www.amazon.co.uk/gp/product/B09FHMPFW1/&#34;&gt;curved lightbar&lt;/a&gt;
that I find helps reduce eyestrain and makes it easier to focus on the monitor.
And on top of &lt;em&gt;that&lt;/em&gt; I have a &lt;a href=&#34;https://www.logitech.com/en-gb/products/webcams/brio-4k-hdr-webcam.960-001106.html&#34;&gt;Logitech Brio webcam&lt;/a&gt;
which gives a great picture with lots of control over pan/zoom/etc for meetings.
I have a little set of systemd scripts that detect when the external webcam is
connected and disable the laptop’s built-in webcam, and vice-versa when it’s
disconnected; that means whenever I jump into a video call in any app there’s
only one input and it’s always the right one, which is nice.&lt;/p&gt;
&lt;p&gt;Below the main monitor and its friends, I have a generic 1080p portable monitor
connected to the laptop via USB-C. This comes with me when I travel, and serves
as a Discord/IRC monitor when at home. It’s handy for throwing other things on
as well, like if I need to reference a design while writing code, and don’t
want to keep alt+tabbing.&lt;/p&gt;
&lt;p&gt;I have two &lt;a href=&#34;https://www.durgod.com/product/k320-space-gray/&#34;&gt;Durgod K320&lt;/a&gt;
keyboards: one with Cherry MX Silent Red switches for when I’m at home and don’t
want to annoy people with the sounds, and one with Cherry MX Blues for when I’m
travelling and can make a bit more noise in exchange for a much nicer typing
experience. They’re both tenkeyless so they can handily fit in a backpack.&lt;/p&gt;
&lt;p&gt;I use a &lt;a href=&#34;https://zowie.benq.eu/en-uk/mouse/ec1-c.html&#34;&gt;Zowie EC1-C&lt;/a&gt; wired mouse,
which does the job well. I got fed up with mice from more mainstream manufacturers
failing: my previous mouse was a Razer Viper that lasted 7 months before it
started losing clicks. All my audio goes through a
&lt;a href=&#34;https://uk.hyperx.com/products/hyperx-cloud-alpha-wireless&#34;&gt;HyperX Cloud Alpha Wireless&lt;/a&gt;,
which has the most insanely good battery life of any wireless headset I’ve ever
owned or seen. Their claim of 300 hours is not just marketing!&lt;/p&gt;
&lt;p&gt;In non-computer hardware, I have a &lt;a href=&#34;https://uk.store.bambulab.com/products/p1s&#34;&gt;Bambu P1S&lt;/a&gt; 3D printer
that I’ve mentioned in my blog before. For printing in fewer dimensions I have an
&lt;a href=&#34;https://www.epson.co.uk/en_GB/products/printers/inkjet/consumer/p/30174&#34;&gt;Epson EcoTank ET-1810&lt;/a&gt;,
which is surprisingly reasonable for an inkjet printer, and the ink costs aren’t as astronomical
as normal cartridges. Plus, you can’t decide to DRM ink when it’s loaded raw into the
printer…&lt;/p&gt;
&lt;p&gt;I have a small Zigbee network set up mainly to control the lights and the old
3D printer I no longer use (the P1S has a sensible standby mode so you don’t
have to yank its power away to make the fans stop spinning!). That’s run through
a Raspberry Pi running &lt;a href=&#34;https://www.zigbee2mqtt.io/&#34;&gt;Zigbee2Mqtt&lt;/a&gt; and a custom
app I wrote to monitor events on MQTT and generate the appropriate responses
(or just log the data).&lt;/p&gt;
&lt;p&gt;My daily-driver phone is an &lt;a href=&#34;https://www.apple.com/uk/iphone-15-pro/&#34;&gt;iPhone 15 Pro&lt;/a&gt;,
but I have a bunch of Android devices kicking around for work purposes. I read
nearly every day on a &lt;a href=&#34;https://www.amazon.co.uk/gp/product/B08N2QK2TG/&#34;&gt;Kindle Paperwhite Signature Edition&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;money-and-work&#34;&gt;Money and work&lt;/h3&gt;
&lt;p&gt;I use &lt;a href=&#34;https://www.ynab.com/&#34;&gt;You Need A Budget&lt;/a&gt; for figuring out how much money
I can spend on what, and making sure I have enough put aside for bills and taxes
and so on. I use it for both business and personal accounts together, but I mark
all the business categories and accounts with a 💼 emoji, and have some scripts
that use the API to make sure the money split makes sense&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:6&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Every recurring monthly and annual payment has a category, as do all upcoming
events I plan to attend, and all the wonderful taxes I have to pay. I also have
categories for hobbies and other expenditure. I think all of that is pretty
typical for a normal YNAB setup.&lt;/p&gt;
&lt;p&gt;I also use YNAB for tracking anything I might like to buy at some point: it
gets a category in a “Wishlist” group, and I’ll occasionally budget to buy
things from there. I try (and often fail) to use that as a way to put off
impulse purchasing. As my income is often very inconsistent, I try to set
aside money so that I can “spend” a bit every week on things in the wishlist
group. That feels a lot nicer to me than just budgeting everything in one go
as soon as I get paid, and gives me more time to consider whether I &lt;em&gt;really&lt;/em&gt;
want those things&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:7&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;For work, I do all the more technical accountancy and tax filing and so on
through &lt;a href=&#34;https://www.freeagent.com/&#34;&gt;FreeAgent&lt;/a&gt;. For my fairly straight-forward tax
situation, I can basically manage everything myself using FreeAgent without
paying for an accountant. That does mean occasionally spending a bunch of time
reading tax manuals, but I’d rather fully understand the rules than just follow
advice anyway.&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;The problem with implementing productivity systems or switching todo apps
or anything of that ilk is that it can &lt;em&gt;feel&lt;/em&gt; like you’re being productive by
doing so. It takes a while to realise that you’re not, and actually getting on
with things is better. &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;Or just accept that I’ll maybe do them at some point. I kept the labels
vague to try and not make it feel judgmental. &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;Including writing this blog post right now. &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;One day I will get all the &lt;code&gt;justify-&lt;/code&gt; and &lt;code&gt;align-&lt;/code&gt; options right without
looking them up, but until then… &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;This is very normal for developers, but much less common for computer
gamers. I’ll probably go back to a desktop next time I upgrade, as I’m
travelling a lot less. &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;li id=&#34;fn:6&#34;&gt;
&lt;p&gt;I can’t just arbitrarily move money from the business to myself, it has
to be given as salary or a divided etc, which has tax implications. If I budget
“business money” for personal things, then it actually has to be properly
transferred and accounted for before being spent (as I’m in effect budgeting
a future paycheck). It would be cleaner to keep the accounts completely
separate, but I find this way works well for me as I can be a bit more flexible. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:6&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:7&#34;&gt;
&lt;p&gt;It feels a little like I’m giving myself pocket money, which is a bit
weird, but it’s a lot better than having no “income” for several months when
client invoices fall weirdly or I’m between contracts. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:7&#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>Further adventures in 3D printing</title>
        <link href="https://chameth.com/further-adventures-in-3d-printing/"/>
        <updated>2024-08-04T00:00:00Z</updated>
        <id>https://chameth.com/further-adventures-in-3d-printing/</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/further-adventures-in-3d-printing/p1s.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-3d-printing/p1s.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/further-adventures-in-3d-printing/p1s.png&#34; alt=&#34;Marketing image of the Bambu Labs P1S&#34; loading=&#34;lazy&#34; width=&#34;404&#34; height=&#34;400&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The Bambu Labs P1S.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Not quite a year ago, I bought a Sovol SV06 3D printer and &lt;a href=&#34;https://chameth.com/adventures-in-3d-printing/&#34;&gt;wrote about&lt;/a&gt;
my initial experiences. At the end I joked:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I’m sure there will be more problems, but hopefully I get at least a week or
so before anything else goes wrong. Maybe I’ll print an “X days since the
last SV06 problem” counter…&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That turned out to be more prophetic than I expected. It became a running joke
that I’d flip the imaginary “days since” sign back to 0 almost every time I
printed. I still really enjoyed having a printer, though: it’s so useful to
be able to be able to go from an idea to a CAD drawing to a physical object
in the space of minutes or hours. So when Bambu Labs reduced the price of the
&lt;a href=&#34;https://uk.store.bambulab.com/products/p1s&#34;&gt;P1S&lt;/a&gt; — a printer generally regarded
as about as trouble free as you can get — to £522 including shipping, I splurged on one.&lt;/p&gt;
&lt;h3 id=&#34;concerns&#34;&gt;Concerns&lt;/h3&gt;
&lt;p&gt;I make it sound like that was an easy decision, but I actually ummed and ahed
a lot before pulling the trigger. I had a lot of objections to a Bambu Labs
printer, and wasn’t quite sure if the price made up for them.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;The Bambu Labs printers are designed to be more consumer friendly than most
previous 3D printers. You can start a model printing directly from their
website or mobile app, which employs a cloud-based slicer that then sends the
model down to your printer. This is one of those things that sounds amazing for
consumers, but should terrify anyone who works with computers. There are so many
ways for a “cloud-connected” printer to go wrong, and with such potentially
disastrous results it gives me anxiety just thinking about it.&lt;/p&gt;
&lt;p&gt;This is not just theoretical, either. In a blog post with the rather under-stated
title of &lt;a href=&#34;https://blog.bambulab.com/cloud-temporary-outage-investigation/&#34;&gt;“Initial Investigation in the Bambu Cloud Temporary Outage”&lt;/a&gt;,
Bambu describe how a server-side error caused printers to start printing unprompted.
Users on Reddit reported printers starting in the middle of the night, trying
to print on top of other objects that were left on the build plate, and so on.
3D printers can easily start fires; having them cloud controlled is frankly
insane.&lt;/p&gt;
&lt;p&gt;As a result of that “temporary outage”, Bambu further improved the “LAN mode”
for their printers, which allows you to sever its connection from their servers
and control it over the network. That’s basically how my old printer worked
anyway, so I was happy enough that I could enable LAN mode and ignore all the
cloud nonsense.&lt;/p&gt;
&lt;p&gt;My other objection to Bambu was more nebulous. Almost all 3D printers are built
upon open source software, and some companies like Prusa go out of their way to
make their products more open to give back to the community. Bambu don’t really
do any of that: I’d be very surprised if they &lt;em&gt;hadn’t&lt;/em&gt; incorporated a bunch of
open source software into their printers and just ignored the licences&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;. I’d
much rather give my money to a company doing good, but the only equivalent
printer was the &lt;a href=&#34;https://www.prusa3d.com/product/original-prusa-mk4-2/&#34;&gt;Prusa MK4&lt;/a&gt;
which was &lt;em&gt;double&lt;/em&gt; the price and didn’t come with an enclosure. To add an
enclosure kit would have made it £800 more expensive than the P1S, and I just
can’t justify that.&lt;/p&gt;
&lt;h3 id=&#34;experience&#34;&gt;Experience&lt;/h3&gt;
&lt;p&gt;The initial setup of the P1S was a breeze. While the SV06 shipped in parts, the
P1S came fully assembled and just needed some packaging and retaining screws
removed. These were all clearly marked with large coloured arrows. It’s very
much designed so anyone can do it.&lt;/p&gt;
&lt;p&gt;I ran through the initial calibration, firmware update, and then put it
in LAN mode. Then I just started printing and it went fine. No messing around
levelling a bed, or using a sheet of paper to try and set the Z-offset properly;
it just worked. It was &lt;em&gt;really&lt;/em&gt; loud though. After talking a bit with a friend
who’d also bought a P1S, I realised that most of his noise was from the fans,
while most of mine was from the servos. A bit of research later and I found out
they added servo noise compensation in a firmware update, but I hadn’t run the
calibration for it because the initial calibration happened before the update.
After doing that, the printer went from sounding like it was impersonating a
continental police siren to just being a bit fan-y.&lt;/p&gt;
&lt;p&gt;The biggest difference over the SV06 is the speed. I’d swapped the nozzle on the
SV06 to a 0.6mm one, and generally printed at a layer height of 0.4mm. The P1S
still has its 0.4mm nozzle, and I mostly print at a layer height of 0.2mm. So
it’s laying down twice the number of layers, with a nozzle that can extrude
half the amount of plastic… And it’s at least twice as fast! Prints that I
previously wouldn’t have even attempted on the SV06 are now 8-10 hours on the
P1S, without even adjusting the speed or lowering the quality. Here’s a
side-by-side comparison:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-3d-printing/comparison.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-3d-printing/comparison.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/further-adventures-in-3d-printing/comparison.png&#34; alt=&#34;A side-by-side comparison of printing time on the P1S and SV06. The P1S prints 8 objects at higher quality than the SV06 prints 3.&#34; loading=&#34;lazy&#34; width=&#34;1856&#34; height=&#34;944&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;P1S vs SV06.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The P1S can print eight objects because of its slightly larger bed, while the
SV06 can only manage three. The P1S is also printing at a much higher quality.
Yet somehow it still ends up being faster. It’s mind blowing.&lt;/p&gt;
&lt;p&gt;The other big difference I’ve not mentioned yet is that the P1S is a “core XY”
printer rather than a “bed slinger”. That means in order to move along the
Y-axis (from the front of the bed to the back), the P1S moves the extruder
while the SV06 moves the bed. This makes a big difference when printing tall
objects, as they’re far more likely to wobble and come loose when the bed is
moving back and forth. It also presumably makes the kinematics a lot easier
as the mass of the moving parts is constant, instead of gradually becoming
heavier as more and more plastic gets added.&lt;/p&gt;
&lt;h3 id=&#34;the-only-failures&#34;&gt;The only failures&lt;/h3&gt;
&lt;p&gt;I’ve only had a handful of failures since getting the P1S. I had one weird
issue where the slicer decided to print some parts extremely slowly, and the
print just didn’t work out in those parts; that’s more of a slicer issue,
though: after tweaking the model to stop the weird behaviour it printed fine.&lt;/p&gt;
&lt;p&gt;The only failure I can directly attribute to the printer was when I tried
ironing. This is a process where after printing the top layer, the printer
slowly goes back over it at the same height while extruding a small amount of
plastic. This is meant to fill in the small gaps between extrusions and give
a smooth finish. In my case, though, it just clogged the nozzle and extruder.&lt;/p&gt;
&lt;p&gt;At first I thought it was because I was printing too hot: the profile I was
using was designed for fast printing, so had to melt the filament rapidly, but
there was no way to turn that temperature down just for the ironing pass. I
tried another run at a lower temperature and everything clogged again. I then
swore off ironing, did a normal print, and everything clogged once more.&lt;/p&gt;
&lt;p&gt;At this point I was getting exceedingly good at disassembling the hot end, but
also fairly annoyed. It felt like I was using an SV06 again&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;! I’d previously
read some recommendations that you should leave the door or top of the enclosure
open when printing PLA, but had never really had any issues so hadn’t bothered.
I tried a print with the top propped open and everything worked smoothly again.
In hindsight all these clogging issues happened when it was particularly hot
and humid outside, which was probably a factor.&lt;/p&gt;
&lt;h3 id=&#34;summary&#34;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Upgrading to the P1S has made the 3D printer feel more like a tool and less like
an additional ongoing project. The speed increase continues to amaze me, as
does the increase in quality.&lt;/p&gt;
&lt;p&gt;I still think it was valuable to have the SV06 to learn how printers work — you
definitely don’t get as much of that from using something so consumer-focused
as the P1S — and figure out if I was actually going to use it long-term or
whether it was just a fad for me. So it served its purpose, but I’m rather
glad to see the back of it in favour of something better in pretty much every
way.&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;And if they did there’s nothing anyone can do about it because copyright
doesn’t exist in the same way in China. &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;Although taking apart the P1S (which is a proprietary machine you might
not expect to be very user-servicable) was orders of magnitude easier than taking
apart the SV06 (which was an open source design). Everything is just much nicer:
the connectors are easier to attach and detach, the screws are all a consistent
size instead of needing a whole suite of allen keys, and so on. &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;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
</feed>
