<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://chameth.com/">
    <title>Chameth.com - posts like artisanal-docker-images, debugging-beyond-the-debugger, thinking-more-about-backups, why-you-should-be-using-https but not sense-api</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/artisanal-docker-images,debugging-beyond-the-debugger,thinking-more-about-backups,why-you-should-be-using-https/unlike/sense-api/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2026-04-30T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <entry>
        <title>Migrating from GitHub to Forgejo</title>
        <link href="https://chameth.com/migrating-from-github-to-forgejo/"/>
        <updated>2026-04-30T00:00:00Z</updated>
        <id>https://chameth.com/migrating-from-github-to-forgejo/</id>
        <content xml:lang="en" type="html">&lt;p&gt;When Microsoft bought GitHub in 2018 my kneejerk reaction — like so many others — was to start looking for alternatives. For a while I self hosted a &lt;a href=&#34;https://about.gitea.com/&#34;&gt;Gitea&lt;/a&gt; instance but I never totally bought into it: some repositories I still pushed to GitHub, some I pushed to Gitea and they got mirrored, and I ended up causing myself problems when I got the two confused. Part of the problem was that the GitHub UI was faster and cleaner than Gitea’s at the time; using Gitea felt like a chore compared to GitHub. I ended up not maintaining it and eventually binning it and just going back to GitHub.&lt;/p&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.png&#34; alt=&#34;A screenshot of the GitHub error page, featuring an angry-looking Unicorn.&#34; loading=&#34;lazy&#34; width=&#34;575&#34; height=&#34;477&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;An all-too familiar unicorn&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Fast forward eight years, and GitHub is about what we all imagined when Microsoft bought it. If you take the most pessimistic way of counting, they have &lt;a href=&#34;https://mrshu.github.io/github-statuses/&#34;&gt;zero nines of reliability&lt;/a&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;. If you take the most generous, they have a single nine. That’s around 30 minutes of downtime every day. It feels like it must be more than that, given how many times you see the damned unicorn.&lt;/p&gt;
&lt;p&gt;Uptime aside, they’ve crowbarred LLMs in all over the place; they &lt;a href=&#34;https://github.com/actions/create-release/issues/119&#34;&gt;don’t have the time&lt;/a&gt; to maintain official GitHub actions; the ones they do maintain have &lt;a href=&#34;https://github.com/actions/toolkit/compare/09cb71a033743b7545c8a9181facc06d7d6012ba...7ae5c2f423367fd11aa625ddcc0bbb0a8e5de5fa&#34;&gt;Copilot running roughshod all over them&lt;/a&gt;&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;; actions themselves are slow to run&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;, often have weird transient failures, and there are &lt;em&gt;so many&lt;/em&gt; footguns that I’m pretty sure GitHub actions should be an entire category in the CWE top 10.&lt;/p&gt;
&lt;p&gt;Speaking of security, while I was drafting this post a remote code execution &lt;a href=&#34;https://www.wiz.io/blog/github-rce-vulnerability-cve-2026-3854&#34;&gt;was reported&lt;/a&gt; and the details are… impressive. The actual security issue was a pretty stupid oversight. Those happen. But they end up being able to execute code as a globally shared &lt;code&gt;git&lt;/code&gt; user with access to all the other repositories on the node. What? I can’t quite get my head around it. No sandboxing, no containers, it just runs as a &lt;code&gt;git&lt;/code&gt; user?&lt;/p&gt;
&lt;p&gt;So, yeah, I’m not a fan of GitHub in its current state. A few months back I took the plunge and set up &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt;, a fork of Gitea where they did radical things like add tests and ensure that it’s community operated not twisted for commercial use. Forgejo also potentially &lt;a href=&#34;https://dustri.org/b/carrot-disclosure-forgejo.html&#34;&gt;has some fun security problems&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;, but the way I host it mitigates more-or-less all problems. It’s also slightly less shocking when an open source project has lapses in basic security versus a $2 trillion corporation.&lt;/p&gt;
&lt;p&gt;It’s taken a bit of doing, but I’m very happy with the setup I have.&lt;/p&gt;
&lt;h3 id=&#34;the-basic-setup&#34;&gt;The basic setup&lt;/h3&gt;
&lt;p&gt;I run all my server software using Docker, and Forgejo is no exception. They have &lt;a href=&#34;https://forgejo.org/docs/latest/admin/installation/docker/&#34;&gt;decent documentation&lt;/a&gt; on how to get it running, and publish rootless images which is nice (and diminishes my urge to make an &lt;a href=&#34;https://chameth.com/artisanal-docker-images/&#34;&gt;artisanal version myself&lt;/a&gt;). You can set configuration options using environment variables, which is perfect for containers, although the names can end up a bit unwieldy. Like this thing: &lt;code&gt;FORGEJO__repository.signing__SIGNING_NAME=Chris Smith&lt;/code&gt;. It gets the job done, but the environment-to-ini mapping is pretty ugly.&lt;/p&gt;
&lt;p&gt;I didn’t want to expose Forgejo publicly, as I wanted to avoid having to try and secure it&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;, or dealing with bots scraping it, or users signing up, and so on. I wanted somewhere to store my git repositories, handle CI for me, and mirror them somewhere public for other people to see and interact with. If you’ve read some of my other posts you’ll probably guess where this is heading: &lt;a href=&#34;https://tailscale.com/&#34;&gt;Tailscale&lt;/a&gt;. Alongside the Forgejo container, I run a Tailscale instance with a &lt;code&gt;serve.json&lt;/code&gt; that covers both the web frontend and the SSH listener used for git operations:&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;TCP&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-nt&#34;&gt;&amp;#34;22&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-nt&#34;&gt;&amp;#34;TCPForward&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;forgejo:2222&amp;#34;&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;443&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-nt&#34;&gt;&amp;#34;HTTPS&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;true&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-nt&#34;&gt;&amp;#34;Web&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-nt&#34;&gt;&amp;#34;${TS_CERT_DOMAIN}:443&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-nt&#34;&gt;&amp;#34;Handlers&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-nt&#34;&gt;&amp;#34;/&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-nt&#34;&gt;&amp;#34;Proxy&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;http://forgejo:3000&amp;#34;&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;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;AllowFunnel&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-nt&#34;&gt;&amp;#34;${TS_CERT_DOMAIN}:443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;false&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;This allows me to interact normally with Forgejo from any of my devices or servers (which all run Tailscale), while not exposing it to the Internet at all. It also handles the TLS certificates automatically, which saves me setting up something else to do that.&lt;/p&gt;
&lt;p&gt;For CI, I run the Forgejo runner image on the same server, and gave it a &lt;code&gt;dind&lt;/code&gt; container to use to actually run the workloads. &lt;code&gt;dind&lt;/code&gt; is the cutesy name for Docker-in-Docker, basically a Docker daemon running inside a Docker container. That keeps the CI workload separated nicely from the “production” workload running on the server, which is nice from both a security and a monitoring point of view.&lt;/p&gt;
&lt;h3 id=&#34;the-uncanny-valley-of-actions&#34;&gt;The uncanny valley of actions&lt;/h3&gt;
&lt;p&gt;Forgejo actions are basically like GitHub actions: they run a series of steps using one or more container images. You define those steps in the exact same clunky YAML format as with GitHub. The big difference between them is that GitHub defaults to using an absolutely massive base image filled with &lt;a href=&#34;https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md&#34;&gt;out-of-date preinstalled software&lt;/a&gt;, and Forgejo leaves that for the administrator to configure. You could, of course, just use the same image as GitHub, but lugging around a multi-gigabyte image that doesn’t even have recent versions of the software I want isn’t really my style. Instead, I set about using a plain &lt;code&gt;debian&lt;/code&gt; image as the base, and then did a face-palm when I tried to run a “normal” action and remembered they’re all written in JavaScript, so expect node to exist.&lt;/p&gt;
&lt;p&gt;Accommodating JavaScript isn’t really my style, either, so I did the very sensible thing of writing &lt;a href=&#34;https://github.com/csmith/actions&#34;&gt;my own suite of actions in Go&lt;/a&gt;. Being written in Go means they can be compiled statically, published as a container, and manage their own very limited dependencies. For example, the &lt;code&gt;checkout&lt;/code&gt; action is built into an &lt;code&gt;alpine&lt;/code&gt; container with &lt;code&gt;git&lt;/code&gt; added, while the &lt;code&gt;dockerbuild&lt;/code&gt; action uses the &lt;code&gt;buildah&lt;/code&gt; image as a base. Each image has the tools it needs, pinned to a recent version, and nothing more. This is very much not a sensible approach to take for most people, but it made me happy. With six basic actions, I could replace almost all the ad-hoc workflows I had in GitHub&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;Other than the base images and my self-inflicted JavaScript machinations, everything works the same as GitHub. But faster. So much faster. Most of my CI workflows finish on Forgejo before GitHub would even have allocated a runner to the job&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;. And I can actually view the logs without them glitching out.&lt;/p&gt;
&lt;h3 id=&#34;step-aside-dependabot-here-comes-renovate&#34;&gt;Step aside Dependabot, here comes Renovate&lt;/h3&gt;
&lt;p&gt;One of the boons and/or banes of hosting code on GitHub is access to Dependabot&lt;sup id=&#34;fnref:8&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:8&#34; role=&#34;doc-noteref&#34;&gt;8&lt;/a&gt;&lt;/sup&gt;. It’s a tool that monitors your dependencies, and automatically submits PRs when new versions are available. It also does security alerts, but they’re comically bad for Go at least&lt;sup id=&#34;fnref:9&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:9&#34; role=&#34;doc-noteref&#34;&gt;9&lt;/a&gt;&lt;/sup&gt;. I do like having dependencies automatically handled, especially with &lt;a href=&#34;https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns&#34;&gt;a cooldown&lt;/a&gt;, which is how I had Dependabot configured before moving.&lt;/p&gt;
&lt;p&gt;To replace it, I’ve configured &lt;a href=&#34;https://github.com/renovatebot/renovate&#34;&gt;Renovate&lt;/a&gt;, an open source alternative. I’d seen it used a bunch before: both by projects on GitHub who prefer it over Dependabot, and by people using GitLab and other platforms where Dependabot isn’t. It mostly does the same things, but Renovate has a few very nice extras.&lt;/p&gt;
&lt;p&gt;Firstly, it supports transcluding config from another repo. So in each of my many, many projects, I just have this stub of a renovate config:&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;$schema&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;https://docs.renovatebot.com/renovate-schema.json&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;extends&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;local&amp;gt;meta/renovate&amp;#34;&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;Then the actual config lives in a &lt;code&gt;default.json&lt;/code&gt; in the &lt;code&gt;meta/renovate&lt;/code&gt; repository. I &lt;a href=&#34;https://github.com/csmith/forgejo-renovate&#34;&gt;mirror that repository&lt;/a&gt; to GitHub, if you want to have a look. This is an amazing feature. When I enabled cooldowns in my Dependabot config it was a &lt;em&gt;slog&lt;/em&gt; to go through all my active repositories and make the same change over and over again. With Renovate I can configure that centrally. As far as I know Dependabot can’t do anything like that, even for enterprises.&lt;/p&gt;
&lt;p&gt;Within that central config I use some of the other nice features. It has the ability to auto merge changes. I use this to automatically accept changes from trusted projects, or my own libraries:&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;matchManagers&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;gomod&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;matchPackagePatterns&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;^github\\.com\\/csmith\\/&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;^chameth\\.com\\/&amp;#34;&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;matchUpdateTypes&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;minor&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;patch&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;automerge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;true&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;minimumReleaseAge&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;0 days&amp;#34;&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-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-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;matchManagers&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;gomod&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;matchPackagePatterns&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;^github\\.com\\/csmith\\/&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;^chameth\\.com\\/&amp;#34;&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;matchUpdateTypes&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;major&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;minimumReleaseAge&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;0 days&amp;#34;&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-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This takes any minor or patch release of my libraries, and automerges it with no cooldown. For major version changes it overrides the cooldown but doesn’t automerge. To automerge Dependabot PRs you have to write an action, implement that logic yourself, and hope you’ve successfully avoided all the footguns inherent in that.&lt;/p&gt;
&lt;p&gt;Speaking of major version updates, Renovate also has an option to automatically update the imports in Go packages when it’s offering a major update. In the config I have:&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-s2&#34;&gt;&amp;#34;postUpdateOptions&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#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;gomodTidy&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;gomodUpdateImportPaths&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which means that every time it does an update it’ll run &lt;code&gt;go mod tidy&lt;/code&gt;, and it’ll update the import paths. In Go, &lt;code&gt;v2&lt;/code&gt; of a module has a different import path to &lt;code&gt;v1&lt;/code&gt; (so they can coexist if transient dependencies need different versions), so usually you have to go through and find and replace those paths. Renovate handles that for me now! I still have to address whatever breaking changes are in the new version, but there’s a lot less grunt work.&lt;/p&gt;
&lt;p&gt;It can also run custom commands&lt;sup id=&#34;fnref:10&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:10&#34; role=&#34;doc-noteref&#34;&gt;10&lt;/a&gt;&lt;/sup&gt;, so for example in my &lt;a href=&#34;https://github.com/csmith/legotapas&#34;&gt;legotapas&lt;/a&gt; project I have this extra snippet in the &lt;code&gt;renovate.json&lt;/code&gt;:&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-s2&#34;&gt;&amp;#34;postUpgradeTasks&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#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-nt&#34;&gt;&amp;#34;commands&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;sh -c &amp;#39;rm plate_*.go&amp;#39;&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;go run ./cmd/generate&amp;#34;&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;executionMode&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;branch&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;fileFilters&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;*.go&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;/code&gt;&lt;/pre&gt;&lt;p&gt;I won’t go into the gory details, but this handles automatically regenerating a bunch of files that change every time a certain dependency is updated. Previously I had a separate action to do that on GitHub; just being able to drop commands in the config file is much nicer.&lt;/p&gt;
&lt;p&gt;To actually run Renovate, I simply have a workflow in Forgejo on a schedule that uses their official Docker image. It also triggers if I update the config, which is nice when I add a new rule, as it’ll immediately update the open PRs to comply. Overall, Renovate has felt like a pretty big quality-of-life upgrade.&lt;/p&gt;
&lt;h3 id=&#34;odd-problems&#34;&gt;Odd problems&lt;/h3&gt;
&lt;p&gt;Not everything has been smooth sailing. Sometimes when Renovate automerged a change, it would just… disappear. The PR was closed as merged, but the commit never landed on master. It wasn’t a huge issue as the next Renovate run would just recreate it, but it would then &lt;em&gt;disable&lt;/em&gt; the automerge and I’d get confused as to why. It turns out there was a major problem with how I had everything set up. Renovate was telling Forgejo to automerge when the required checks pass, but I had no branch protection rules set up in Forgejo, so as soon as the PR was created it was eligible to merge. As the CI was so fast I hadn’t noticed that the automerges were happening &lt;em&gt;before&lt;/em&gt; the CI results were posted.&lt;/p&gt;
&lt;p&gt;The disappearing commits were because the repository was getting into a bad state when Renovate tried to automerge multiple changes instantly in the same repo. Forgejo has a bunch of maintenance commands that helped to fix that problem, and I then wrote a little script to enable branch protection rules so the CI would actually run. The automerging has been smooth since.&lt;/p&gt;
&lt;p&gt;Another issue I had early on was that merging would fail because the GPG keyring was locked. I had Forgejo configured to sign all the automatic commits, so you can verify what commits came from my infrastructure. Forgejo is the only process using the keyring, so I wasn’t sure why it was becoming locked. It turns out that GnuPG uses the hostname as part of the lockfile, and because Forgejo was running in Docker it was getting random hostnames based on the container ID. Explicitly setting a hostname made GnuPG and by extension Forgejo much happier.&lt;/p&gt;
&lt;h3 id=&#34;webhooks-and-updating&#34;&gt;Webhooks and updating&lt;/h3&gt;
&lt;p&gt;Forgejo has this amazing feature: you can add a webhook that’s called for all your repos. GitHub, for some reason, supports this for organisations but not people. Even though the two are treated the same in a lot of places. It doesn’t seem like it would be hard, and it’s such an obvious thing to want, but… no. I wrote a &lt;a href=&#34;https://github.com/csmith/webhooked&#34;&gt;whole tool&lt;/a&gt; to add webhooks for me to get around this. Now I don’t need it, Forgejo does the right thing!&lt;/p&gt;
&lt;p&gt;One particularly interesting use of these webhooks has been making my Docker containers auto-update when a new image is pushed. I previously had a webhook receiver for a few projects that would update them automatically, but GitHub package notifications were extremely laggy. Every other webhook fired within a minute or so of the event happening, but package webhooks would sometimes take 30-60+ minutes to fire. I opened a support case with GitHub and it got escalated to engineering and fixed about 10 weeks later&lt;sup id=&#34;fnref:11&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:11&#34; role=&#34;doc-noteref&#34;&gt;11&lt;/a&gt;&lt;/sup&gt;. It was better for a while, and then got worse again. Trying to do continuous deployment with a 30 minute random lag was frustrating.&lt;/p&gt;
&lt;p&gt;So now I had reliable webhooks, but the update process was a bit gross. I have a hangup about internet-facing services not having access to the Docker socket. It just feels like too big a security risk. So I had a process that received hooks, and then another that figured out if containers needed to be updated and restarted them. Adding a new container to the system meant recompiling the second process, which was a massive faff.&lt;/p&gt;
&lt;p&gt;Fortunately, I was saved from this mess by my friend &lt;a href=&#34;https://greboid.com/&#34;&gt;Greg&lt;/a&gt;. He wrote &lt;a href=&#34;https://github.com/greboid/adze&#34;&gt;adze&lt;/a&gt;, a tool that receives webhooks and uses Docker Compose to update matching containers. He wrote &lt;a href=&#34;https://greboid.com/ramblings/2026/04/Keeping-containers-up-to-date&#34;&gt;a blog post about it&lt;/a&gt;. It’s all one process, but that’s no longer a blocker for me because the webhooks can be delivered privately from my Forgejo instance without exposing adze to the internet. It finds matching containers automatically based on the images they’re using&lt;sup id=&#34;fnref:12&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:12&#34; role=&#34;doc-noteref&#34;&gt;12&lt;/a&gt;&lt;/sup&gt;, and combined with Forgejo not forcing me to add a webhook to every single repository makes my life so much easier. I write some code, push it to Forgejo, and within a minute or so it’s running on my dev instance.&lt;/p&gt;
&lt;p&gt;I have IRC notifications set up for when adze does updates, and I still have some old ones that announce GitHub package webhooks. I keep seeing things like this and chuckling:&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;15:44:42 &amp;lt;@ircjag&amp;gt; [ADZE] chameth.com: pending updating git.yak-wall.ts.net/public/chameth.com
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;15:44:45 &amp;lt;@ircjag&amp;gt; [ADZE] chameth.com: success updating git.yak-wall.ts.net/public/chameth.com
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;17:11:54 &amp;lt;@ircjag&amp;gt; [GHCR] Container pushed to csmith/chameth.com:dev.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I pushed a change at 15:43, Forgejo built it and pushed the image (to both its own registry and to GitHub), then at 15:44 adze updated the container to the new version. An hour and a half later GitHub gets around to delivering its own webhook.&lt;/p&gt;
&lt;h3 id=&#34;mirroring-and-prs&#34;&gt;Mirroring and PRs&lt;/h3&gt;
&lt;p&gt;The final piece I had to figure out was how to actually interact with people. Most of my projects are small and don’t get a massive amount of public contributions, but the option should still be there. And ideally not involve anyone having to figure out how to e-mail patches&lt;sup id=&#34;fnref:13&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:13&#34; role=&#34;doc-noteref&#34;&gt;13&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Mirroring outwards is simple: Forgejo supports it natively. Whenever I push to Forgejo, it pushes out to the corresponding repo on GitHub. I’ve also started mirroring some projects to &lt;a href=&#34;https://codeberg.org/&#34;&gt;Codeberg&lt;/a&gt;, to avoid having GitHub as my single “public” presence. But what happens when I get a pull request? If I merge it in the GitHub web UI, Forgejo will push its own branch over the top and get rid of it&lt;sup id=&#34;fnref:14&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:14&#34; role=&#34;doc-noteref&#34;&gt;14&lt;/a&gt;&lt;/sup&gt;. I could pull the changes, merge them, and push the result to Forgejo but then I don’t get the benefit of any CI.&lt;/p&gt;
&lt;p&gt;I ended up writing a small private tool that deals with this for me. I give it a GitHub PR, and it effectively imports it into Forgejo for me. I created a &lt;code&gt;prs&lt;/code&gt; organisation, and it’ll create a fork of the project in that org if it doesn’t exist. Then it pushes the PR contents to a branch, and creates a PR from the &lt;code&gt;prs&lt;/code&gt; repo into the main one. It runs as a user which can write to the PRs org, but only submit pull requests to public repositories outside of it.&lt;/p&gt;
&lt;p&gt;When the PR is imported, I can review it in the Forgejo UI, and then I can approve the CI workflows for that particular PR. This gives me some extra defence-in-depth in case anyone is trying to abuse one of those action footguns. Once the CI passes, I can merge it as normal in the Forgejo UI, it gets mirrored out to GitHub, and GitHub marks the PR as merged automatically. It’s not the most elegant solution, but the actual workflow is pretty smooth, especially for the low volumes of PRs I get.&lt;/p&gt;
&lt;p&gt;With that, I have Forgejo set up to do everything I need from a git host, and I’m no longer hamstrung every time GitHub goes down.&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;This way of counting isn’t &lt;a href=&#34;https://evanhahn.com/in-defense-of-githubs-poor-uptime/&#34;&gt;particularly fair&lt;/a&gt;, but it’s somewhat realistic. GitHub break out their uptime for things like git operations, pull requests, actions, webhooks and packages. Realistically if any of those independent things are down then my overall workflow doesn’t, well, work. &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;To be clear, I’m not objecting to the use of Copilot here. It’s a tool. But if you use a tool to do something fairly simple and it spews out 35 commits, including at least 5 reverts, then either it’s a terrible tool or you’re using it very wrong. I didn’t hunt down this example, I came across it while trying to debug something and wasted a bunch of time trying to figure out what on earth was going 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;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;You could self-host runners, which may help, except they’ve already threatened to &lt;a href=&#34;https://github.blog/changelog/2025-12-16-coming-soon-simpler-pricing-and-a-better-experience-for-github-actions/&#34;&gt;charge you for the privilege of bringing your own runners&lt;/a&gt;. &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;If people could stop finding RCEs in git services for a few days so I can stop redrafting this intro, that’d be great. &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’m feeling pretty smug about this decision after reading about the security issues. &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;There’s one holdout: my &lt;a href=&#34;https://github.com/csmith/dockerfiles/&#34;&gt;dockerfiles repo&lt;/a&gt; which is a very special snowflake with a very complex workflow and I’m still deciding how to handle it. &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;Which is to be expected, given I’ve got a server dedicated to one person, and they’re trying to serve however-many million users at once. That doesn’t make it any less nice, though! &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;li id=&#34;fn:8&#34;&gt;
&lt;p&gt;I’d link to it but there’s nowhere really good to link to. It’s just been subsumed into the ‘security’ functions of GitHub, along with a bunch of copilot nonsense. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:8&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:9&#34;&gt;
&lt;p&gt;The go project maintain a &lt;a href=&#34;https://pkg.go.dev/vuln/&#34;&gt;vulnerability database&lt;/a&gt; that includes details about which exact symbols are affected, and the &lt;a href=&#34;https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck&#34;&gt;govulncheck&lt;/a&gt; tool uses this to only report security issues in code that’s actually used. Dependabot doesn’t do that: it constantly alerts about code that’s not imported, so won’t ever be shipped. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:9&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:10&#34;&gt;
&lt;p&gt;In the default config it doesn’t allow anything of the sort, of course. You can whitelist individual commands, but because this is a private server I can merrily whitelist &lt;code&gt;.*&lt;/code&gt; and do whatever I like. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:10&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:11&#34;&gt;
&lt;p&gt;The turnaround time isn’t particularly impressive, but the fact that they kept the support case open, and the (human!) support agent updated me several times is. I don’t have a lot of good things to say about GitHub, but I don’t think I’ve ever had a support experience that good from a company of a similar size. Especially as I wasn’t a paying customer at the time. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:11&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:12&#34;&gt;
&lt;p&gt;Why didn’t I do that in the first place? It seems so obvious now… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:12&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:13&#34;&gt;
&lt;p&gt;I really like the &lt;em&gt;idea&lt;/em&gt; of an e-mail based workflow, but I absolutely hate actually doing it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:13&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:14&#34;&gt;
&lt;p&gt;This is what happened to me a lot when I was half using Gitea. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:14&#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>Thinking more about backups</title>
        <link href="https://chameth.com/thinking-more-about-backups/"/>
        <updated>2025-11-01T00:00:00Z</updated>
        <id>https://chameth.com/thinking-more-about-backups/</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/thinking-more-about-backups/backblaze.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/thinking-more-about-backups/backblaze.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/thinking-more-about-backups/backblaze.png&#34; alt=&#34;The Backblaze logo: a stylised flame above the word Backblaze&#34; loading=&#34;lazy&#34; width=&#34;500&#34; height=&#34;320&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The Backblaze logo&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Almost a year ago I wrote about &lt;a href=&#34;https://chameth.com/simple-backups-restic-hetzner/&#34;&gt;how I do backups with Restic and Hetzner&lt;/a&gt;.
That system has been ticking along well ever since, but recently I had some… thoughts. These backups are
all well and good if I accidentally delete a file, or a database gets corrupted, or something, but there are
two glaring issues:&lt;/p&gt;
&lt;p&gt;Firstly, I’m backing up my Hetzner server to Hetzner cloud storage. If something happens to Hetzner — or
my Hetzner account — then all my eggs go down with that basket. Obviously Hetzner are a big organisation
and aren’t likely to just vanish overnight, but I’m less confident about my account. Could a false abuse
report get it suspended? What if the UK passes
&lt;a href=&#34;https://www.legislation.gov.uk/ukpga/2023/50/contents&#34;&gt;even more dumb laws&lt;/a&gt; and Hetzner decide it’s easier
just to not do business with people here? This is the same sort of concern I have about Google accounts:
if you have half of your life in Google Drive and Google Mail, what happens if you comment on a YouTube
video, get flagged by an AI moderation process, and your account gets suspended? It’s probably not very
likely, but these are things my brain likes to dwell on.&lt;/p&gt;
&lt;p&gt;Secondly, the credentials to access the backups sit on each machine that is backed up. If someone malicious
gained access to the machine, they’d also have access to delete or tamper with all the backups. It feels
a little silly that the same attack could take down both the originals and the backups. There’s no way to
avoid that with Hetzner’s S3 implementation, as far as I can tell.&lt;/p&gt;
&lt;h3 id=&#34;exploring-options&#34;&gt;Exploring options&lt;/h3&gt;
&lt;p&gt;I toyed with the idea of making local copies of the backups, but the only way to avoid the same problems
would be to keep them offline and do a manual copy every now and then. I didn’t really want to do that,
and was concerned that if I did a monthly offline backup then I stood to lose up to a month of data in
the worst case.&lt;/p&gt;
&lt;p&gt;I then looked around at other S3 providers. &lt;a href=&#34;https://aws.amazon.com/s3/storage-classes/glacier/&#34;&gt;Amazon’s glacier offering&lt;/a&gt;
is tempting due to its very low storage costs, but you pay for that if you ever want to restore anything.
There are also lots of weird pricing edge cases around moving data between storage classes, minimum file
sizes, and so on. A much better option is &lt;a href=&#34;https://www.backblaze.com/cloud-storage&#34;&gt;Backblaze’s B2&lt;/a&gt; product.
Their pricing is much more straight-forward, and they have an interesting feature that’s particularly useful
in this case: &lt;a href=&#34;https://www.backblaze.com/blog/backblaze-b2-lifecycle-rules/&#34;&gt;lifecycle rules&lt;/a&gt;. Coupled with
the ability to create API keys that don’t have access to delete files (just “hide” them), this allows for
what’s effectively an append-only store.&lt;/p&gt;
&lt;p&gt;This works more-or-less out of the box with Restic. &lt;a href=&#34;https://pricey.uk/blog/restic-backups-without-delete/&#34;&gt;Joseph Price has a guide&lt;/a&gt;
that goes into the setup in a bit more depth. Basically, whenever Restic would delete a file (e.g. during
a “forget” or “prune” operation), it instead gets hidden and is only deleted when the B2 lifecycle rules
decide it should be. I’ve kept the existing Hetzner S3 backups for now, and just added an extra step to
the end of my script: a simple &lt;code&gt;restic copy&lt;/code&gt; and a &lt;code&gt;restic forget&lt;/code&gt;. B2 actually works out cheaper than the
Hetzner storage, as they don’t bill you for a minimum of 1TB storage; my current usage is around $3/month.
Not a bad price for some extra peace of mind!&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Simple backups with Restic and Hetzner Cloud</title>
        <link href="https://chameth.com/simple-backups-restic-hetzner/"/>
        <updated>2024-12-06T00:00:00Z</updated>
        <id>https://chameth.com/simple-backups-restic-hetzner/</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/simple-backups-restic-hetzner/restic.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/simple-backups-restic-hetzner/restic.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/simple-backups-restic-hetzner/restic.png&#34; alt=&#34;The Restic logo — a gopher with two umbrellas.&#34; loading=&#34;lazy&#34; width=&#34;400&#34; height=&#34;400&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Restic’s mascot, who’s dual-wielding umbrellas to save you from a rainy day.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I have a confession: for the past few years I’ve not been backing up any of my
computers. Everyone knows that you &lt;em&gt;should&lt;/em&gt; do backups, but actually getting
around to doing it is another story.&lt;/p&gt;
&lt;p&gt;Don’t get me wrong: most of my important things are “backed up” by virtue of
being committed to remote git repositories, or attached to e-mails, or
re-obtainable from the original source, and so on. I don’t think any machine
failing completely would be a disaster for me, but it would certainly be a pain.&lt;/p&gt;
&lt;p&gt;This week I finally got around to doing something, and it ended up being a lot
more straight forward than my previous forays into backup-land.&lt;/p&gt;
&lt;h3 id=&#34;restic&#34;&gt;Restic&lt;/h3&gt;
&lt;p&gt;After soliciting a few opinions, the choice of backup software came down to
either &lt;a href=&#34;https://www.borgbackup.org/&#34;&gt;Borg&lt;/a&gt; or &lt;a href=&#34;https://restic.net/&#34;&gt;Restic&lt;/a&gt;.
I’m pretty sure either would have done what I want, but I leaned towards Restic
for a few reasons: it has a more informative website, it’s written in Go
rather than Python&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;, and Borg seems to be transitioning between major
releases at the moment&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;!--more--&gt;
&lt;p&gt;The way Restic works is pretty simple: you initialise a ‘repository’, and can
then call &lt;code&gt;restic backup /some/path&lt;/code&gt; and it’ll get backed up to the repository.
Restic handles keeping different backups separate, and only sending data that’s
changed, and deduplicating, and so on. You basically point it at a thing you
don’t want to lose, and it sorts it out for you. Perfect.&lt;/p&gt;
&lt;p&gt;There are equally straight-forward commands for removing old snapshots
(&lt;code&gt;restic forget&lt;/code&gt;) and verifying backups (&lt;code&gt;restic check&lt;/code&gt;). One of the nice things
about modern backup solutions is they support a whole range of backends. I was
originally going to spin up a small VPS to host my backups, but noticed that
Restic supported S3-compatible stores…&lt;/p&gt;
&lt;h3 id=&#34;hetzner-cloud-object-storage&#34;&gt;Hetzner Cloud Object Storage&lt;/h3&gt;
&lt;p&gt;I host my servers with Hetzner, and was going to use them to spin up a VPS as
well. Despite the “Cloud” branding on a bunch of products, they offer reasonable
prices and good service. A couple of months ago, they started offering
&lt;a href=&#34;https://docs.hetzner.com/storage/object-storage/overview&#34;&gt;S3-compatible object storage&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The pricing isn’t totally straight forward, but for continuous use the “free
quota” amounts to 1TB of storage and 1TB of egress a month. Ingress is free,
as is traffic within the &lt;code&gt;eu-central&lt;/code&gt; region (where all my servers are). That
quota is only awarded when you pay the “base price”, though, which is €4.99 a
month. So basically it’s €5 a month for 1TB of storage and enough egress to
fully restore every single byte. That’s better value than any VPS I can find,
much cheaper than Amazon S3, and about the same as
&lt;a href=&#34;https://www.backblaze.com/cloud-storage&#34;&gt;Backblaze B2&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;getting-them-to-work-together&#34;&gt;Getting them to work together&lt;/h3&gt;
&lt;p&gt;Now you’d think making the tool that supports S3-compatible object storage
work with your S3-compatible object storage would be easy, right? Well not
quite. The library Restic uses to deal with S3 backends has some
strange logic for figuring out the bucket name given a URL. It doesn’t quite
seem to work right, though…&lt;/p&gt;
&lt;p&gt;Hetzner buckets have URLs like &lt;code&gt;s3://bucketname.hel1.your-objectstorage.com&lt;/code&gt;&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;,
but just passing that to Restic gives an error that the bucket is not specified.
The docs mention there’s an advanced option to make it use the virtual host for
the bucket name: &lt;code&gt;-o s3.bucket-lookup=dns&lt;/code&gt;. But that… also doesn’t work.
What I ended up doing was specifying the URL as
&lt;code&gt;s3://hel1.your-objectstorage.com/bucketname&lt;/code&gt;, and also passing in the &lt;code&gt;dns&lt;/code&gt;
option. The library then seems to muddle its way back to a real, working URL.
I’m not sure why it works like this: maybe it’s just a weird aspect of S3 that
I’m oblivious to?&lt;/p&gt;
&lt;p&gt;The next fun part is that you can configure Restic entirely by using environment
variables, except for that &lt;code&gt;-o s3.bucket-lookup=dns&lt;/code&gt; argument. That has to go
on the command line. I ended up making a little wrapper script to invoke Restic
correctly:&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-cp&#34;&gt;#!/bin/sh
&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&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;# The password restic will use to encrypt your data. You should generate&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;# something nice and secure.&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-nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;RESTIC_PASSWORD&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;repo-password
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&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;# The path to the repository where restic will save the backup. In our case&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;# this takes the form `s3:&amp;lt;endpoint&amp;gt;/&amp;lt;bucket&amp;gt;`. Your endpoint might be different&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;# to mine depending on the region your bucket is in.&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-nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;s3:hel1.your-objectstorage.com/bucket-name
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&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;# The access and secret key generated in the &amp;#39;S3 credentials&amp;#39; section of the&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;# Hetzner Cloud Console&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-nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;hetzner-access-key-id
&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-nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;hetzner-access-key
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&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;# Pass any arguments on to the restic command, along with the magic&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;# `s3.bucket-lookup` option we need to resolve the S3 URL properly.&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-nb&#34;&gt;exec&lt;/span&gt; restic -o s3.bucket-lookup&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;dns &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;$@&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in the backup script I just alias &lt;code&gt;restic&lt;/code&gt; to use the script:&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-cp&#34;&gt;#!/bin/bash
&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&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-nb&#34;&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&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;# This means any time we use `restic` below, we&amp;#39;ll actually execute our special&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;# script which supplies all the env vars and arguments needed to find the&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;# repository. Make sure the path matches where you saved the script!&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-nb&#34;&gt;alias&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;restic&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;chroma-s1&#34;&gt;&amp;#39;~/.bin/restic&amp;#39;&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&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;# Actually do the backup. Each directory in the list below will be backed up&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;# separately.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nv&#34;&gt;dirs&lt;/span&gt;&lt;span class=&#34;chroma-o&#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;/some/path/to/backup/&amp;#34;&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;/some/other/path/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;for&lt;/span&gt; i in &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;dirs&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;[@]&lt;/span&gt;&lt;span class=&#34;chroma-si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;do&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-nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;$i&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nb&#34;&gt;cd&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;$i&lt;/span&gt; &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; restic --verbose backup .&lt;span class=&#34;chroma-o&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;done&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&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;# Prune our snapshots. You can tweak the numbers here. Run with `--dry-run` to&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;# see what effect any changes would have before actually committing to them. &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;restic forget --keep-daily &lt;span class=&#34;chroma-m&#34;&gt;7&lt;/span&gt; --keep-weekly &lt;span class=&#34;chroma-m&#34;&gt;10&lt;/span&gt; --keep-monthly &lt;span class=&#34;chroma-m&#34;&gt;24&lt;/span&gt; --keep-yearly &lt;span class=&#34;chroma-m&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The only other notable thing here is that the script changes into the directory
to be backed up. If you give Restic an absolute path, it will create a new
snapshot if the metadata of any folder in the path changes, which is not what
I want.&lt;/p&gt;
&lt;p&gt;If it wasn’t for the S3 URL issues, the whole thing would’ve probably taken
me about half an hour. That’s including setting up the object storage,
installing Restic, and so on. It’s painfully easy. Why didn’t I do this three
years ago?!&lt;/p&gt;
&lt;h3 id=&#34;addendum-a-step-by-step-guide&#34;&gt;Addendum: a step-by-step guide&lt;/h3&gt;
&lt;aside class=&#34;update raised-box&#34;&gt;
  &lt;h5 class=&#34;plain-header&#34;&gt;Update 2025-03-01:&lt;/h5&gt;
  &lt;p&gt;This section was added after the original article was published, following some
helpful feedback. Let me know if you have any problem with these instructions!&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;If you want to do this yourself, here’s a quick step-by-step guide:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Log in to the &lt;a href=&#34;https://console.hetzner.cloud&#34;&gt;Hetzner Cloud Console&lt;/a&gt;, and
create a project.&lt;/li&gt;
&lt;li&gt;On the “Object Storage” tab, create a new bucket. Note the name and the
endpoint.&lt;/li&gt;
&lt;li&gt;On the “Security” tab, go to “S3 Credentials” and generate new credentials.
Note down the access key and the secret key.&lt;/li&gt;
&lt;li&gt;Copy the first shell script above, fill in the password (you can pick!),
endpoint, bucket name, access key and secret key.&lt;/li&gt;
&lt;li&gt;Run the script with the &lt;code&gt;init&lt;/code&gt; argument (e.g. &lt;code&gt;~/.bin/restic init&lt;/code&gt;). This
will create a new repository, and only needs to be done once even if you
backup multiple machines.&lt;/li&gt;
&lt;li&gt;Copy the second shell script above, making sure the &lt;code&gt;restic&lt;/code&gt; alias points
at the script you saved in step 4. Change the list of directories to
whatever you want to backup.&lt;/li&gt;
&lt;li&gt;Schedule the script to be run automatically, using crontab or systemd timers
or however you prefer.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To check everything is working, you can use the &lt;code&gt;snapshots&lt;/code&gt; subcommand to see
a list of saved snapshots. You might also want to try to restore a snapshot
using the &lt;code&gt;restore&lt;/code&gt; subcommand.&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’m not trying to be a language snob, but given the choice between two
otherwise equal projects one in Go and one in Python, I’ll take the Go one.
I know I’m not going to have weird library issues down the line, and I’m much
more comfortable rummaging around the source. &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;With a “Don’t use this in production!” notice on the shiny new version. &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;Aside: I really hate URLs like that. What is with the trend for completely
generic domains divorced from the service they’re a part of? &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;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Reproducible Builds and Docker Images</title>
        <link href="https://chameth.com/reproducible-builds-docker-images/"/>
        <updated>2022-02-18T00:00:00Z</updated>
        <id>https://chameth.com/reproducible-builds-docker-images/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image left&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/reproducible-builds-docker-images/dependency.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/reproducible-builds-docker-images/dependency.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/reproducible-builds-docker-images/dependency.png&#34; alt=&#34;Comic showing all modern digital infrastructure is built upon one project by a random person in Nebraska&#34; loading=&#34;lazy&#34; width=&#34;385&#34; height=&#34;489&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;XKCD 2347: Dependency&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;a href=&#34;https://reproducible-builds.org/&#34;&gt;Reproducible builds&lt;/a&gt; are builds which you are able to reproduce byte-for-byte,
given the same source input. Your initial reaction to that statement might be “Aren’t nearly all builds
‘reproducible builds’, then? If I give my compiler a source file it will always give me the same binary, won’t it?”
It &lt;em&gt;sounds&lt;/em&gt; simple, like it’s something that should just be fundamentally true unless we go out of our way to break it,
but in reality it’s actually quite a challenge. A group of Debian developers have been working on reproducible packages
for the best part of a decade and while they’ve made fantastic progress,
&lt;a href=&#34;https://isdebianreproducibleyet.com/&#34;&gt;Debian still isn’t reproducible&lt;/a&gt;. Before we talk about why it’s a hard problem,
let’s take a minute to ponder why it’s worth that much effort.&lt;/p&gt;
&lt;h3 id=&#34;on-supply-chain-attacks&#34;&gt;On supply chain attacks&lt;/h3&gt;
&lt;p&gt;Suppose you want to run some open-source software. One of the many benefits of open-source software is that anyone
can look at the source and, in theory, spot bugs or malicious code. Some projects even have sponsored audits or
penetration tests to affirm that the software is safe. But how do you actually deploy that software? You’re probably
not building from source - more likely you’re using a package manager to install a pre-built version, or downloading
a binary archive, or running a docker image. How do you know whoever prepared those binary artifacts did so from
an un-doctored copy of the source? How do you know a
&lt;a href=&#34;https://en.wikipedia.org/wiki/SourceForge#Controversies&#34;&gt;middle-man hasn’t decided to add malware to the binaries to make money&lt;/a&gt;?&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;Even worse: if the software you’re trying to use includes any dependencies, you have the same issue of trust
with them. Maybe &lt;em&gt;your&lt;/em&gt; supplier isn’t compromising the software, but that doesn’t mean &lt;em&gt;their&lt;/em&gt; supplier isn’t. The
beauty-cum-horror of a supply chain attack is that it can target the weakest link anywhere along the supply chain.
Even if there aren’t any binary files involved, dependencies can still be attacked: what if &lt;code&gt;npmjs.com&lt;/code&gt; or
&lt;code&gt;proxy.golang.org&lt;/code&gt; or &lt;code&gt;github.com&lt;/code&gt; return a different version of a dependency-of-a-dependency when the request
comes from your IP address? It doesn’t even need to be a modified dependency, it could be a perfectly un-tampered,
properly signed copy of the source, just from an older version with a known vulnerability.&lt;/p&gt;
&lt;p&gt;Enter stage left: reproducible builds, here to save the day! If the build process is reproducible then you - or anyone
else on the internet - can perform the same build on the same source and validate the output has the same checksum or
hash. If Debian publish a binary package and an independent re-builder comes up with the exact same build artifact,
there’s a reasonably good chance that the build is good. An attacker would have to compromise both the build machine
and the re-build machine to do anything nefarious. The more re-builders there are, the less feasible a supply chain
attack is.&lt;/p&gt;
&lt;h3 id=&#34;so-why-isnt-software-just-reproducible&#34;&gt;So why isn’t software just reproducible?&lt;/h3&gt;
&lt;h4 id=&#34;compilers&#34;&gt;Compilers&lt;/h4&gt;
&lt;p&gt;As a bit of an experiment, I asked some friends to run the following for me and report the answer:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;echo&lt;/span&gt; -e &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;#include &amp;lt;stdio.h&amp;gt;\nint main() { printf(\&amp;#34;Hello\&amp;#34;); return 0; }&amp;#34;&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  gcc -x c -o hello.out - &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  sha256sum hello.out
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This compiles a super-simple hello world program and then prints the SHA-256 hash of the resulting binary. Here are
the results:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hash&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;GCC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1f62feab5a06861dc575201d807781926d1ae49fb113da018fde8b670a1346f7&lt;/td&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;11.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b8e6f2c7082be69f65ffa5e7a3d749eb47866a1b2e1ec19efb63cc59a8b160cd&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;8.3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cbad2e47a22c234b5e7fa55e029a8db4d64ac7a962e2176bd2e1373d78954088&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;8.3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e0f6bbc13b29fea8cfa2a975ba4661e781323298aec166c8311d342e6f93c4a6&lt;/td&gt;
&lt;td&gt;Alpine&lt;/td&gt;
&lt;td&gt;10.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e379156895e06c7a0bf18ac4d648860edcb2655576b0ab9fab172bd6c8b92075&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;10.2.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7ffdaee4eb64e016b89dc5e54d2c8eebab3cebafe2c7aa97de627b5972ecea46&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;11.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8ae52cc166743b6ae1eb3e14179ef33de5061a04237f8f97088c896c41a2f698&lt;/td&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;11.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8ae52cc166743b6ae1eb3e14179ef33de5061a04237f8f97088c896c41a2f698&lt;/td&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;11.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As you can see, there are barely any duplicates. Even the same version of GCC on the same OS sometimes produces
different results. And this is the most basic program I could write! Differences arise from the compiler version,
the build flags, the libraries installed, and a whole host of other factors. If you compile a Go application instead of
a C one, then by default the compiler will include debug information in the binary. This includes the full path to the
source file on disk, so building a project in &lt;code&gt;/home/chris/&lt;/code&gt; will produce a different binary to building the same
source in &lt;code&gt;/tmp&lt;/code&gt;. Future versions of Go are also going to stamp in other meta-data such as VCS info, so building inside
and outside a Git repository will produce different binaries.&lt;/p&gt;
&lt;h4 id=&#34;archives&#34;&gt;Archives&lt;/h4&gt;
&lt;p&gt;Compilers are only half the problem. Build processes are usually multistep, involving compiling, moving, compressing,
and so on. Consider creating an archive of a file:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;repeat &lt;span class=&#34;chroma-m&#34;&gt;4&lt;/span&gt; touch hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; tar zcf hello.tgz hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum hello.tgz &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sleep 0.5
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;f3d5c56f6b8089de95d62d060e6ffcbbad26875807ae7bc253f07cd097ea61be  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;ab67f2e865b5afa87d9b2434d92b0c271b3cf730fa85988f84852551749ba6ed  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;ab67f2e865b5afa87d9b2434d92b0c271b3cf730fa85988f84852551749ba6ed  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;738678c9650b10fd83636997dd1aba4016bbf0ec5ebf3dfd4ef75d770b56e23b  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Any file added to a tar takes with it a timestamp, so the build is only reproducible if it happens at the exact same
time! We can make this reproducible by forcing &lt;code&gt;tar&lt;/code&gt; (and the same goes for &lt;code&gt;zip&lt;/code&gt; and most other archive formats) to
set a certain timestamp on the files:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;repeat &lt;span class=&#34;chroma-m&#34;&gt;4&lt;/span&gt; touch hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; tar --mtime 2022-02-18T01:00 -zcf hello.tgz hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum hello.tgz &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sleep 0.5 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In a real build there are basically two approaches here: you can set it to a pre-defined value (like the unix epoch),
or you can set it to match the modification time of the source files. The former is easiest, but the latter is more
cosmetically and semantically appealing.&lt;/p&gt;
&lt;h4 id=&#34;iteration-order&#34;&gt;Iteration order&lt;/h4&gt;
&lt;p&gt;So we’ve pinned our build environment, we’re manipulating timestamps when adding files to archives, now what? Imagine
part of the build process involves looping through all the files in a directory and doing &lt;em&gt;something&lt;/em&gt;. What order do
these files get iterated in? Well, that very much depends on the filesystem and perhaps when the files themselves
were created. To ensure this is reproducible we need to explicitly sort any such operation so that it’s always
consistent. This iteration could be happening in a tool that’s called by another tool that’s called by a build script,
so the fix isn’t necessarily straight-forward.&lt;/p&gt;
&lt;aside class=&#34;sidenote raised-box&#34;&gt;
  &lt;h5 class=&#34;plain-header&#34;&gt;Side note: a bug war story&lt;/h5&gt;
  &lt;p&gt;I’ve personally been victim to this kind of non-determinism. I was working on an Android app, and committed a new
test that worked fine on my machine, and worked fine on the CI server. But it failed consistently for a colleague.&lt;/p&gt;
&lt;p&gt;We both did fresh checkouts of the source, and ran the tests. Mine passed, his failed. He sent me an archive of
his checkout in case there was something weird going on there, and the tests passed on my machine. We compared
hashes of our checkouts, and they were the same. It was obviously environmental somehow, but everything else worked
fine, and the build system went to great pains to ensure things were the same.&lt;/p&gt;
&lt;p&gt;After a &lt;em&gt;lot&lt;/em&gt; of debugging, I worked out that his test was running with a different version of a library to me,
despite the libraries being defined in the build files and the build files being identical. After &lt;em&gt;even more&lt;/em&gt;
debugging it turned out there were two versions of the library on the classpath, and the ordering of them was
different between my machine and his.&lt;/p&gt;
&lt;p&gt;The actual issue turned out to be that the build tool generated the classpath by iterating over the library
files, and that iteration was done in order of file creation time. The two libraries were added at different points
in the project history, so the creation time in your local cache depended on which versions of the app you’d built
in the past. With no cache everything worked as expected but there was a slim range of commits where only one
library was in use, and if you had run the tests during that period your cache was effectively poisoned.&lt;/p&gt;
&lt;p&gt;We fixed the issue by excluding the older version of the library (which was being pulled in as a transient dependency),
and filed a bug against the build tool to make the classpath properly deterministic. I think that stands as the most
difficult to diagnose bug I’ve ever dealt with.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;Interestingly, if you iterate over a map in Go, the iteration is &lt;em&gt;deliberately&lt;/em&gt; non-deterministic. That’s an attempt
to defeat &lt;a href=&#34;https://www.hyrumslaw.com/&#34;&gt;Hyrum’s Law&lt;/a&gt; and prevent developers from relying on whatever the current
behaviour happens to be. This actually makes it easier to make things reproducible as the problem is loud and
in-your-face, rather than subtle and hard to spot.&lt;/p&gt;
&lt;h4 id=&#34;other-sources&#34;&gt;Other sources&lt;/h4&gt;
&lt;p&gt;There’s an awful lot of other places that non-determinism can come from. If the app pulls in dependencies, their
versions have to be pinned, otherwise your build changes depending on the latest release of that dependency. If
the build process pulls any information from a website, it’s liable to change. Hopefully the website is under your
control so that you can version the resource and pin that version. Obviously, anything to do with dates or the
current user will probably cause problems. Timezones and locales can cause subtle differences.&lt;/p&gt;
&lt;h3 id=&#34;what-about-docker&#34;&gt;What about Docker?&lt;/h3&gt;
&lt;p&gt;Docker comes with some good and some bad points for reproducibility. The biggest advantage is that it inherently
completely describes the build environment; it should work exactly the same from one system to another, even across
different OS families. The biggest drawback is it sprays timestamps around like no-one’s business. Each layer in
a container image is a &lt;code&gt;.tar.gz&lt;/code&gt; file, meaning each file within it is timestamped as discussed above. Making an image
involves a lot of copying of files around, so these timestamps invariably end up causing reproducibility issues.&lt;/p&gt;
&lt;p&gt;Even worse than timestamps in the filesystem, the image format also contains some meta-data that includes the
timestamp at which each layer was built. That means even if you go out of your way to set the timestamp of every
single file in your image, the image itself will be different every time you rebuild it. There is no way to deal
with this in Docker, which is a very sad state of affairs. Fortunately, &lt;a href=&#34;https://buildah.io/&#34;&gt;Buildah&lt;/a&gt; provides
a &lt;code&gt;--timestamp&lt;/code&gt; flag for &lt;em&gt;its&lt;/em&gt; build commands; this not only sets the layer timestamp but also the creation
timestamp of any file within the layer.&lt;/p&gt;
&lt;p&gt;The other major issue that affects Docker images is the pinning of packages pulled in by package managers. An awful
lot of images are based on Alpine or Debian derivatives, and use &lt;code&gt;apk&lt;/code&gt; or &lt;code&gt;apt&lt;/code&gt; to install dependencies. These need
to have a version specified as otherwise the package manager will just pull in the latest at the time of the build.
But this isn’t quite enough: you also need to pin the version of any packages that they depend on, recursively.
This means flattening the entire package hierarchy and installing all the packages explicitly and with pinned
versions.&lt;/p&gt;
&lt;p&gt;One more wrinkle in the package management space is that Alpine don’t keep old packages in their main repositories.
If you have a Docker image with pinned alpine packages in, it will stop building if the package is updated. This
isn’t necessarily fatal to making a reproducible build — as long as it’s reproducible for its useful lifetime,
I don’t really see an issue.&lt;/p&gt;
&lt;p&gt;Honestly, though, the biggest issue with making Docker images reproducible is getting people to care. Dockerfiles
are a relatively new way of packaging software, and there’s no centralised organisation like you find with Linux
distributions. There are enough challenges that most casual packagers aren’t going to bother, and no real
incentive for them to. That won’t stop me trying, though!&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Artisanal Docker images</title>
        <link href="https://chameth.com/artisanal-docker-images/"/>
        <updated>2022-02-05T00:00:00Z</updated>
        <id>https://chameth.com/artisanal-docker-images/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/artisanal-containers.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/artisanal-containers.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/artisanal-docker-images/artisanal-containers.jpg&#34; alt=&#34;Shelf showing a variety of artisanal containers&#34; loading=&#34;lazy&#34; width=&#34;300&#34; height=&#34;432&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Artisanal containers…&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I run a fair number of services as docker containers. Recently, I’ve been moving away from pre-built images
pulled from Docker Hub in favour of those I’ve hand-crafted myself. If you’re thinking “that sounds like a
lot of effort”, you’re right. It also comes with a number of advantages, though, and has been a fairly fun
journey.&lt;/p&gt;
&lt;h3 id=&#34;the-problems-with-docker-hub-and-its-images&#34;&gt;The problems with Docker Hub and its images&lt;/h3&gt;
&lt;h4 id=&#34;rate-limits&#34;&gt;Rate limits&lt;/h4&gt;
&lt;p&gt;For the last few years, I’ve been getting increasingly unhappy with Docker Hub itself. Docker-the-technology
is wonderful, but Docker-the-company has been making some rather large missteps. The biggest and most impactful
of these has been introducing “pull rate” limits. At the time of writing, if you want to just pull a public image
without logging in then you are limited to 100 pulls every 6 hours. If you log in then you’re limited to 200 pulls
per 6 hours, but it’s account wide. This might seem like a big enough number, but I repeatedly hit it and there
is no way to actually audit what is causing it. I have various containers that may all pull images at arbitrary
times (e.g. continuous integration build agents), and the only information you get back from Docker Hub is the
number of pulls remaining.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;Obviously, I could start paying Docker Hub for a “Pro” plan. That gets you 5,000 pulls per day for $7/month.
The downside is that every docker client would have to be authenticated, which presents a fair annoyance in
terms of credential management. I also don’t really like how they positioned the service as a public utility
with special treatment in the docker software, and then start tightening the ratchet to make money.&lt;/p&gt;
&lt;h4 id=&#34;bad-images&#34;&gt;“Bad” images&lt;/h4&gt;
&lt;p&gt;I’m fairly opinionated about what a container image should look like: most importantly it should run just a
single process, and only include the bare minimum dependencies required for that. Other people think differently,
and it’s very hard to tell at a glance whether an image on Docker Hub contains just the application you want,
or whether it also bundles MySQL, Redis, Elasticsearch, and a partridge in a pear tree. Some people want that
kind of thing, but I really don’t. It’s also very hard to tell whether an image is officially endorsed by the
upstream project, and where the source Dockerfile is. This used to be better because most projects used Docker Hub’s
automatic builds, but they’re now a “pro” feature.&lt;/p&gt;
&lt;p&gt;I quite often found that I’d be looking for an image for X, and there would be 5-10 images from different users.
None of them looked official, some of them were out-of-date, some bundled the kitchen sink. Even when one looked
good, it’s a bit of a gamble whether the author is going to keep it updated or not.&lt;/p&gt;
&lt;h4 id=&#34;doijanky&#34;&gt;Doijanky&lt;/h4&gt;
&lt;p&gt;The rate limits and other problems were annoying, but they weren’t really annoying enough to force me to do
anything about it. The straw that broke the camel’s back came later: I was looking at the
&lt;a href=&#34;https://hub.docker.com/_/golang&#34;&gt;official golang images&lt;/a&gt;, and noticed that all the tags were pushed by a
random user account called “doijanky”:&lt;/p&gt;
&lt;figure class=&#34;image center&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/doijanky.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/doijanky.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/artisanal-docker-images/doijanky.png&#34; alt=&#34;An &amp;#39;official&amp;#39; Docker Hub image pushed by user &amp;#39;doijanky&amp;#39;&#34; loading=&#34;lazy&#34; width=&#34;786&#34; height=&#34;249&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;An ‘official’ Docker Hub image pushed by user ‘doijanky’&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I, perhaps naively, assumed that official images were built on Docker Hub’s own infrastructure. Why would
all the Golang images be attributed to this user? Checking out their profile, they’re simply identified as
a “Community User” like everyone else, with no repositories of their own. The only thing in the profile is
their homepage, which is a link to a Jenkins dashboard: &lt;a href=&#34;https://doi-janky.infosiftr.net/&#34;&gt;https://doi-janky.infosiftr.net/&lt;/a&gt;. It appears
legitimate: “Infosiftr” are a container consultancy and the dashboard is linked to from the README in the
official images git repository, but I find it baffling that they’re using third-party infrastructure and
a normal user account (with a dubious name) to push these images. There doesn’t seem to be a good way to
verify what you pull corresponds to the Dockerfile it came from; if infosiftr wanted to inject something
into the build they could happily do so, and who knows how good their infosec posture is? If someone got
access to the “doijanky” account, how long could they upload malicious images before someone noticed?&lt;/p&gt;
&lt;p&gt;This little roller-coaster ride from “are all the official images compromised?!” to “oh, no, they’re not,
it’s all just awful” finally convinced me to look at building my own images from scratch.&lt;/p&gt;
&lt;h3 id=&#34;the-implementation-templating-with-contempt&#34;&gt;The implementation: templating with contempt&lt;/h3&gt;
&lt;p&gt;One of the big issues I needed to tackle was how to deal with updates. I didn’t want to have to go and
edit a file every time some minor release was made of some software, or every time there was a security
vulnerability in a common library. The official images use a shell-scripting based system to check for
updates and generate Dockerfiles, I decided to do something similar but with Go templates. The result is
a tool called &lt;a href=&#34;https://github.com/csmith/contempt&#34;&gt;contempt&lt;/a&gt;. It takes a template like:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;{{image&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;golang&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt; AS build&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;ARG&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;TAG&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;{{github_tag &amp;#34;&lt;/span&gt;example/project&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;}}&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;RUN&lt;/span&gt; apk add --no-cache &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-o&#34;&gt;{{&lt;/span&gt;range &lt;span class=&#34;chroma-nv&#34;&gt;$key&lt;/span&gt;, &lt;span class=&#34;chroma-nv&#34;&gt;$value&lt;/span&gt; :&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt; alpine_packages &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;git&amp;#34;&lt;/span&gt; -&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-o&#34;&gt;{{&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;$key&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;}}={{&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;$value&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-o&#34;&gt;{{&lt;/span&gt;end&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Contempt has support for getting information from a variety of sources. In this case, it’s getting
the latest digest of another Docker image, the latest tag from a Git repository, and the latest version
of an alpine package and all its dependencies. The resulting Dockerfile looks something like this:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c&#34;&gt;# Generated from https://github.com/csmith/dockerfiles/blob/master/miniflux/Dockerfile.gotpl&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c&#34;&gt;# BOM: {&amp;#34;apk:brotli-libs&amp;#34;:&amp;#34;1.0.9-r5&amp;#34;,&amp;#34;apk:busybox&amp;#34;:&amp;#34;1.34.1-r4&amp;#34;, &amp;lt;snip&amp;gt; }&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;reg.c5h.io/golang@sha256:ac8fa5f4078b0a697796b5d741&lt;/span&gt;... AS build&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;ARG&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;TAG&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;2.0.35&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;RUN&lt;/span&gt; apk add --no-cache &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        brotli-libs&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;1.0.9-r5&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nv&#34;&gt;busybox&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;1.34.1-r4&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-c1&#34;&gt;# &amp;lt;snip&amp;gt;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nv&#34;&gt;pcre2&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;10.39-r0&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nv&#34;&gt;zlib&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;1.2.11-r3&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I’ve cut out the longer parts for readability. You can see that it pins all versions of the alpine packages in use,
as well as the base image. This ensures that if you build from the same Dockerfile at a later time it will build the
same image (or will fail entirely, as Alpine doesn’t keep their old packages around indefinitely). It also produces
a “bill of materials” as a really long JSON-encoded comment. If you let contempt commit the Dockerfile it uses the
BOM to generate useful commit messages like: &lt;code&gt;[project] apk:busybox: 1.34.1-r3-&amp;gt;1.34.1-r4&lt;/code&gt;, so you can see exactly
what changed.&lt;/p&gt;
&lt;p&gt;Contempt also has support for building and pushing images whenever it changes the Dockerfile. I use it in a
GitHub action that runs daily to check all my images are up-to-date and push those that aren’t. It understands
the dependencies between images (by pre-analysing the templates) so it will always check and build base images
before ones that require them. This means an update to, say, the “alpine” base image will cause anything that
depends on it to get updated at the same time, ensuring security updates are rolled out promptly.&lt;/p&gt;
&lt;h3 id=&#34;the-result&#34;&gt;The result&lt;/h3&gt;
&lt;p&gt;You can see my collection of lovingly hand-crafted Dockerfiles in my &lt;a href=&#34;https://github.com/csmith/dockerfiles&#34;&gt;dockerfiles&lt;/a&gt;
repository.&lt;/p&gt;
&lt;p&gt;There are a number of advantages to handwriting all the images I use. The obvious one is that they’re all built
how I want: there are no extraneous dependencies, they’re all based on the same small set of base images (rather
than pulling around 10 different versions of debian), nothing tries to also run a DBMS in its container, etc.&lt;/p&gt;
&lt;p&gt;This level of customisation goes further, though. Because I’m packaging the software myself, I can tweak how it’s
built to fit my needs. A couple of things I run need their own TLS certificates separate from my normal HTTPS
setup, so I bake my &lt;a href=&#34;https://github.com/csmith/certwrapper/&#34;&gt;certwrapper&lt;/a&gt; tool in to manage those; I can even set the
build flags on certwrapper to only enable the particular DNS provider I personally need (thus avoiding dragging in
clients for AWS, GCP, etc). Some software like Hashicorp Vault has an optional web interface that I don’t need,
so I simply don’t enable it in the build. These changes save build time, reduce image sizes, in some cases improve
runtime performance, and generally reduce the attack surface of what’s running in the container.&lt;/p&gt;
&lt;p&gt;It’s also been a great way to learn more about how software is distributed. Writing a Dockerfile is not that distant
from writing a PKGBUILD file for an Arch Linux package, or the equivalent for other distributions. In a couple of
instances I’ve googled how to solve a particular issue, and found an Arch or Void linux maintainer asking the upstream
project about the exact same issue.&lt;/p&gt;
&lt;p&gt;Finally, all the images I build I push to my own registry so there are obviously no rate limiting issues.
Standing up a service (assuming the Dockerfile has been written!) is amazingly quick because the base layers are all
shared and cached, and the registry is a lot physically closer than Docker Hub. Bootstrapping this whole thing becomes
an interesting problem because the image for the registry is stored on the registry, but I’ll leave that discussion for
another post…&lt;/p&gt;
</content>
    </entry>
</feed>
