<?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, docker-automatic-nginx-proxy but not g15-ram-upgrade, migrating-from-github-to-forgejo, why-you-should-be-using-https</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/artisanal-docker-images,debugging-beyond-the-debugger,docker-automatic-nginx-proxy/unlike/g15-ram-upgrade,migrating-from-github-to-forgejo,why-you-should-be-using-https/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2026-01-29T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <entry>
        <title>An interesting Tailscale + Docker gotcha</title>
        <link href="https://chameth.com/tailscale-docker-gotcha/"/>
        <updated>2026-01-29T00:00:00Z</updated>
        <id>https://chameth.com/tailscale-docker-gotcha/</id>
        <content xml:lang="en" type="html">&lt;p&gt;As I’ve &lt;a href=&#34;https://chameth.com/how-i-use-tailscale/&#34;&gt;written&lt;/a&gt; &lt;a href=&#34;https://chameth.com/avoiding-the-consequences-of-dumb-laws-with-tailscale/&#34;&gt;about&lt;/a&gt; &lt;a href=&#34;https://chameth.com/exposing-game-servers-over-tailscale/&#34;&gt;before&lt;/a&gt;, I use &lt;a href=&#34;https://tailscale.com/&#34;&gt;Tailscale&lt;/a&gt; for a lot of things. I thought I had it set up in a reasonably secure manner, but I recently noticed a problem.&lt;/p&gt;
&lt;p&gt;I use Tailscale’s ACLs to limit what each node can access, based on the tags I apply to it. So an &lt;code&gt;app&lt;/code&gt; node can’t access anything via Tailscale, while an &lt;code&gt;integration&lt;/code&gt; or &lt;code&gt;server&lt;/code&gt; node can access things tagged with either &lt;code&gt;app&lt;/code&gt; or &lt;code&gt;integration&lt;/code&gt;. This is expressed pretty simply in the Tailscale ACL JSON:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;tagOwners&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-c1&#34;&gt;// Servers that can be SSH&amp;#39;d into
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;tag:server&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-c1&#34;&gt;// Applications that are exposed on tailscale but never connect out
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;tag:app&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-c1&#34;&gt;// Things which talk to other services over tailscale (connecting to apps etc)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;tag:integration&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;grants&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-c1&#34;&gt;// Users can access everything
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;src&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;autogroup:member&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;dst&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;ip&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;  &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-c1&#34;&gt;// Servers and integrations can access integrations and apps
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;src&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:server&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:integration&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;dst&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:integration&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:app&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;ip&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;  &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Over the past week I’ve been setting up a private &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt; instance behind Tailscale, complete with an actions runner that runs things using a docker-in-docker container. I didn’t want the runner knowing anything about Tailscale, so I had it configured to speak to Forgejo direct over HTTP (&lt;code&gt;forgejo:3000&lt;/code&gt;) instead of using full Tailscale HTTPS URL that I use when accessing it (&lt;code&gt;http://git.example-net.ts.net/&lt;/code&gt;)&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Everything was going fine, until I forgot to do that translation… and it worked. My Forgejo action runners could access anything on my tailnet. I run several things on Tailscale that just have authentication turned off, on the basis that only authorised devices can access them. Things like the admin interface for this website. I definitely didn’t intend for any workflow I run on my git server to have access to edit my website!&lt;/p&gt;
&lt;p&gt;This wasn’t just limited to Forgejo, either. Any docker container I was running could access the tailnet. In hindsight it’s fairly obvious why: the host is running Tailscale, connected as a node tagged with my &lt;code&gt;server&lt;/code&gt; tag. That creates a &lt;code&gt;tailscale0&lt;/code&gt; interface, and automagically sets up iptables rules to route Tailscale traffic over the interface. Docker also automagically sets up iptables rules to bridge traffic, and apparently these two sets of rules interact in such a way that traffic from Docker containers is allowed to route via the &lt;code&gt;tailscale0&lt;/code&gt; interface.&lt;/p&gt;
&lt;p&gt;I say it’s fairly obvious in hindsight — there’s no reason why Docker would special case any particular host interface after all — but it still feels pretty surprising. Because both bits of software inject their own iptables rules, I never really had a good mental model for how they interact. The host Tailscale node was a completely separate building block to Docker. It would be a pain to use either of them if they didn’t do these rules, but it’s also one of the reasons I don’t really like “magical” things&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;There are lots of ways to fix this, but none of them feel particularly great. You can configure both Tailscale and Docker to not automatically fiddle with iptables and handle the rules yourself, but I really hate dealing with iptables&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;. As a stopgap I did hold my nose and add some iptables rules to drop traffic to the &lt;code&gt;tailscale0&lt;/code&gt; interface if it originated from the IP ranges that Docker was configured to use:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;iptables -I DOCKER-USER -s 192.168.0.0/16 -o tailscale0 -j DROP
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;iptables -I DOCKER-USER -s 172.17.0.0/12 -o tailscale0 -j DROP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;DOCKER-USER&lt;/code&gt; chain is a nice little escape hatch; it comes before the main auto-generated &lt;code&gt;DOCKER&lt;/code&gt; chain, and Docker leaves the rules in it alone.&lt;/p&gt;
&lt;p&gt;So I added these rules, and felt pretty good about myself, and then… everything started breaking in weird ways. After some debugging I realised the problem was DNS&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;. The server’s DNS resolver is &lt;code&gt;100.100.100.100&lt;/code&gt;, a special Tailscale address. This is so that it can resolve tailnet hostnames via &lt;a href=&#34;https://tailscale.com/kb/1081/magicdns&#34;&gt;MagicDNS&lt;/a&gt; (and so I don’t have to configure my custom DNS servers manually on each device; Tailscale does it for me). My new iptables rules inadvertently dropped all the DNS packets coming from docker containers. D’oh.&lt;/p&gt;
&lt;p&gt;Obviously the solution here is to double down and add MORE iptables rules:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;iptables -I DOCKER-USER -s 192.168.0.0/16 -d 100.100.100.100 -o tailscale0 -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;iptables -I DOCKER-USER -s 172.17.0.0/12 -d 100.100.100.100 -o tailscale0 -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This got DNS working again, but it all feels a bit gross. Not least because to actually make these persist you need to use &lt;code&gt;iptables-save&lt;/code&gt;, and then you also get all the rules that both Docker and Tailscale have inserted. I came across &lt;a href=&#34;https://github.com/giangi/iptables-docker-filter&#34;&gt;a script&lt;/a&gt; to filter out the Docker ones, but… yuck&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I realised a cleaner idea might just be to lock down what &lt;code&gt;server&lt;/code&gt; tagged nodes could access within Tailscale itself. That way I can avoid touching iptables at all. It’s handy to be able to &lt;code&gt;curl&lt;/code&gt; services from the host when debugging, but it’s not really necessary. So I removed that access, and… stuff broke again. I’m using my Forgejo instance as a registry for some of the docker images I run, so the docker daemon needs to be able to reach it. I ended up making a new tag for &lt;code&gt;infrastructure&lt;/code&gt;, which can be accessed from &lt;code&gt;server&lt;/code&gt; devices. This does still allow all the docker containers to reach Forgejo, but I already have it set up with appropriate access controls and public/private repository splits. Forgejo is a service designed to run publicly, so this seems a reasonable trade-off for convenience. I used the &lt;code&gt;tests&lt;/code&gt; feature of Tailscale’s ACL config to make sure I’d got the rules right:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;tests&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-c1&#34;&gt;// Servers can only access infrastructure
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;src&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt;   &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:server&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;proto&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tcp&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;allow&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;				&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:infrastructure:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;deny&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;				&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;100.84.16.43:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;				&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;me@example.com:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;				&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:server:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;				&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:app:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;				&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;tag:integration:8080&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;			&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;		&lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;	&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The whole situation still feels a bit messy. If I ever get around to switching to nftables I might loop back and manually craft some rules for routing traffic, instead of leaving Tailscale and Docker to do their own thing.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;I discovered later on that logging into the container registry at &lt;code&gt;forgejo:3000&lt;/code&gt; actually issued a redirect to &lt;code&gt;https://git.example-net.ts.net/&lt;/code&gt; so this was all basically for naught… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;I didn’t use Tailscale’s MagicDNS for a long time just because the word “magic” put me off. Only when I eventually got around to learning how it worked, and seeing that it wasn’t really that magical under the hood, did I change my mind. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;nftables seems far better in lots of ways, but I can’t really be bothered migrating. Maybe next time I reimage the server for whatever reason… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;It’s always DNS… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;Again, nftables would almost certainly help here. It actually has (gasp) configuration files. But again, I really didn’t want to spend the time migrating. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Docker reverse proxying, redux</title>
        <link href="https://chameth.com/docker-proxying-redux/"/>
        <updated>2022-12-06T00:00:00Z</updated>
        <id>https://chameth.com/docker-proxying-redux/</id>
        <content xml:lang="en" type="html">&lt;p&gt;Six years ago, &lt;a href=&#34;https://chameth.com/docker-automatic-nginx-proxy/&#34;&gt;I described&lt;/a&gt;
my system for configuring a reverse proxy for docker containers.
It involved six containers including a key-value store and a webserver.
Nothing in that system has persisted to this day. Don’t get me wrong – it
worked – but there were a lot of rough edges and areas for improvement.&lt;/p&gt;
&lt;h3 id=&#34;microservices-and-their-limitations&#34;&gt;Microservices and their limitations&lt;/h3&gt;
&lt;p&gt;My goal was to follow the UNIX philosophy of “do one thing and do it well”.
Unfortunately, that doesn’t really work when applied to network services that
have to interact with one and other. UNIX tools are built upon a common file
system and simple data passed over STDIN. Microservices don’t have that
shared foundation. You could make one:
companies that use microservices in anger often have a team that deals with
the “developer experience” of creating and using
microservices. But as a solo developer that’s not something I wanted to
spend my time doing.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;This became very apparent when trying to debug issues. In the UNIX world,
if your series of commands piped together does something strange you can simply
echo the data at various stages. Not so much when that data is flying around
on a network, going into and out of things like etcd. Trying to figure out why
a certificate hadn’t been acquired was a case of searching through logs from
four containers, none of which had particularly good logging.
There are many ways to get insight into what’s
happening with microservices but, again, that’s not something I wanted to do myself.&lt;/p&gt;
&lt;p&gt;Over time, and with experience in other projects, I came to realise that
microservices only really make sense if you’re unable to deploy a monolith.
For tech companies this naturally happens when different teams
contribute to the same service: splitting it out into smaller services that
are wholly owned by individual teams makes sense. For solo developers,
that never happens. You can still gain the other benefits
of microservices – such as code separation and having clearly defined APIs –
by sticking to certain coding standards.&lt;/p&gt;
&lt;h3 id=&#34;proxy-inconveniences&#34;&gt;Proxy inconveniences&lt;/h3&gt;
&lt;p&gt;As well as being unhappy with the microservice nature of the solution,
I wasn’t pleased with nginx. If you requested an unknown domain, nginx
would use the first server block in its config to serve a response, instead
of sending an “unrecognised name” alert as I wanted. It was a minor issue, but
it irked me.&lt;/p&gt;
&lt;p&gt;So from nginx I switched to haproxy. It has a &lt;code&gt;strict-sni&lt;/code&gt; option when configuring
TLS connections which makes it behave properly. It also performs a lot better for
this type of workload than nginx. All was well for a while, but then I started getting alerts
that requests were occasionally failing. I couldn’t reproduce the issue, but
my nightly jobs to build and push containers managed to hit it nearly every
night, causing them to fail.&lt;/p&gt;
&lt;p&gt;After some investigation, I found that the haproxy developers had refactored
the header parsing code, and
neglected to properly reset flags when multiple requests were sent over the same
connection. There was a patch, but it wasn’t released. No problem, I thought,
I’ll just cherry-pick it onto the last release… Except that haproxy use
Git in the most convoluted manner I’ve ever seen – they have one
repository per release. This makes it harder to patch, but it also made me question
whether I trusted them to ship stable software: there were no tests for
the header parsing code (which is both fundamental and finicky,
the perfect target for tests), the source code management was weird, and they didn’t
seem in any rush to patch this bug.&lt;/p&gt;
&lt;p&gt;Not long after that issue, &lt;a href=&#34;https://greg.holmes.name/&#34;&gt;Greg&lt;/a&gt; managed to
encounter another bug where haproxy returned a 500 error whenever the
upstream server replied with a particular, perfectly valid, header.
The die was cast – it was time to move to something else.&lt;/p&gt;
&lt;h3 id=&#34;not-invented-here-syndrome&#34;&gt;Not Invented Here syndrome&lt;/h3&gt;
&lt;p&gt;Looking for a new solution, there were many more options than
back in 2016. I’m still convinced, however, that anything
exposed to the Internet should not have access to run docker containers.
It’s the modern equivalent of running a CGI script as root. That
single requirement eliminates most off-the-shelf solutions.
What do you do when nothing quite meets
your specific requirements? You make something yourself! My new solution has two
components: &lt;a href=&#34;https://github.com/csmith/dotege&#34;&gt;Dotege&lt;/a&gt; and
&lt;a href=&#34;https://github.com/csmith/centauri&#34;&gt;Centauri&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Dotege is a replacement for the
microservices that monitored containers and obtained certificates. It’s fundamentally
a templating engine - whenever the containers change, it evaluates a
template and saves the result to disk. The template has access to details about
the containers, their labels, ports, and so on. Dotege can also obtain certificates
from Let’s Encrypt, and raise a signal against another
process whenever the template or certificates change. I used this to generate
the configuration and certificates used by haproxy for a while, and more recently
changed the template so that it works for Centauri.&lt;/p&gt;
&lt;p&gt;Centauri is my own reverse proxy. It’s configured using a simple text file
and can also obtain certificates from an ACME provider. It doesn’t serve static
content, has no knowledge about docker, and avoids the other bells and
whistles that adorn most reverse proxies. It also has good test
coverage to ensure that I don’t, say, accidentally break header parsing.&lt;/p&gt;
&lt;p&gt;As a software engineer I enjoy writing software, but I also enjoy running
simple, easy to understand software. That’s what I’ve achieved here: it’s
very easy to identify where the problem is if anything goes wrong, both are small
Go programs rather than vast sprawling C
monstrosities, and their interaction is primarily through a file written to disk
that can be inspected or edited as needed.&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Reproducible Builds and Docker Images</title>
        <link href="https://chameth.com/reproducible-builds-docker-images/"/>
        <updated>2022-02-18T00:00:00Z</updated>
        <id>https://chameth.com/reproducible-builds-docker-images/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image left&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/reproducible-builds-docker-images/dependency.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/reproducible-builds-docker-images/dependency.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/reproducible-builds-docker-images/dependency.png&#34; alt=&#34;Comic showing all modern digital infrastructure is built upon one project by a random person in Nebraska&#34; loading=&#34;lazy&#34; width=&#34;385&#34; height=&#34;489&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;XKCD 2347: Dependency&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;a href=&#34;https://reproducible-builds.org/&#34;&gt;Reproducible builds&lt;/a&gt; are builds which you are able to reproduce byte-for-byte,
given the same source input. Your initial reaction to that statement might be “Aren’t nearly all builds
‘reproducible builds’, then? If I give my compiler a source file it will always give me the same binary, won’t it?”
It &lt;em&gt;sounds&lt;/em&gt; simple, like it’s something that should just be fundamentally true unless we go out of our way to break it,
but in reality it’s actually quite a challenge. A group of Debian developers have been working on reproducible packages
for the best part of a decade and while they’ve made fantastic progress,
&lt;a href=&#34;https://isdebianreproducibleyet.com/&#34;&gt;Debian still isn’t reproducible&lt;/a&gt;. Before we talk about why it’s a hard problem,
let’s take a minute to ponder why it’s worth that much effort.&lt;/p&gt;
&lt;h3 id=&#34;on-supply-chain-attacks&#34;&gt;On supply chain attacks&lt;/h3&gt;
&lt;p&gt;Suppose you want to run some open-source software. One of the many benefits of open-source software is that anyone
can look at the source and, in theory, spot bugs or malicious code. Some projects even have sponsored audits or
penetration tests to affirm that the software is safe. But how do you actually deploy that software? You’re probably
not building from source - more likely you’re using a package manager to install a pre-built version, or downloading
a binary archive, or running a docker image. How do you know whoever prepared those binary artifacts did so from
an un-doctored copy of the source? How do you know a
&lt;a href=&#34;https://en.wikipedia.org/wiki/SourceForge#Controversies&#34;&gt;middle-man hasn’t decided to add malware to the binaries to make money&lt;/a&gt;?&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;Even worse: if the software you’re trying to use includes any dependencies, you have the same issue of trust
with them. Maybe &lt;em&gt;your&lt;/em&gt; supplier isn’t compromising the software, but that doesn’t mean &lt;em&gt;their&lt;/em&gt; supplier isn’t. The
beauty-cum-horror of a supply chain attack is that it can target the weakest link anywhere along the supply chain.
Even if there aren’t any binary files involved, dependencies can still be attacked: what if &lt;code&gt;npmjs.com&lt;/code&gt; or
&lt;code&gt;proxy.golang.org&lt;/code&gt; or &lt;code&gt;github.com&lt;/code&gt; return a different version of a dependency-of-a-dependency when the request
comes from your IP address? It doesn’t even need to be a modified dependency, it could be a perfectly un-tampered,
properly signed copy of the source, just from an older version with a known vulnerability.&lt;/p&gt;
&lt;p&gt;Enter stage left: reproducible builds, here to save the day! If the build process is reproducible then you - or anyone
else on the internet - can perform the same build on the same source and validate the output has the same checksum or
hash. If Debian publish a binary package and an independent re-builder comes up with the exact same build artifact,
there’s a reasonably good chance that the build is good. An attacker would have to compromise both the build machine
and the re-build machine to do anything nefarious. The more re-builders there are, the less feasible a supply chain
attack is.&lt;/p&gt;
&lt;h3 id=&#34;so-why-isnt-software-just-reproducible&#34;&gt;So why isn’t software just reproducible?&lt;/h3&gt;
&lt;h4 id=&#34;compilers&#34;&gt;Compilers&lt;/h4&gt;
&lt;p&gt;As a bit of an experiment, I asked some friends to run the following for me and report the answer:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-nb&#34;&gt;echo&lt;/span&gt; -e &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;#include &amp;lt;stdio.h&amp;gt;\nint main() { printf(\&amp;#34;Hello\&amp;#34;); return 0; }&amp;#34;&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  gcc -x c -o hello.out - &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  sha256sum hello.out
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This compiles a super-simple hello world program and then prints the SHA-256 hash of the resulting binary. Here are
the results:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hash&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;GCC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1f62feab5a06861dc575201d807781926d1ae49fb113da018fde8b670a1346f7&lt;/td&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;11.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b8e6f2c7082be69f65ffa5e7a3d749eb47866a1b2e1ec19efb63cc59a8b160cd&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;8.3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cbad2e47a22c234b5e7fa55e029a8db4d64ac7a962e2176bd2e1373d78954088&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;8.3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e0f6bbc13b29fea8cfa2a975ba4661e781323298aec166c8311d342e6f93c4a6&lt;/td&gt;
&lt;td&gt;Alpine&lt;/td&gt;
&lt;td&gt;10.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e379156895e06c7a0bf18ac4d648860edcb2655576b0ab9fab172bd6c8b92075&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;10.2.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7ffdaee4eb64e016b89dc5e54d2c8eebab3cebafe2c7aa97de627b5972ecea46&lt;/td&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;11.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8ae52cc166743b6ae1eb3e14179ef33de5061a04237f8f97088c896c41a2f698&lt;/td&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;11.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8ae52cc166743b6ae1eb3e14179ef33de5061a04237f8f97088c896c41a2f698&lt;/td&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;11.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As you can see, there are barely any duplicates. Even the same version of GCC on the same OS sometimes produces
different results. And this is the most basic program I could write! Differences arise from the compiler version,
the build flags, the libraries installed, and a whole host of other factors. If you compile a Go application instead of
a C one, then by default the compiler will include debug information in the binary. This includes the full path to the
source file on disk, so building a project in &lt;code&gt;/home/chris/&lt;/code&gt; will produce a different binary to building the same
source in &lt;code&gt;/tmp&lt;/code&gt;. Future versions of Go are also going to stamp in other meta-data such as VCS info, so building inside
and outside a Git repository will produce different binaries.&lt;/p&gt;
&lt;h4 id=&#34;archives&#34;&gt;Archives&lt;/h4&gt;
&lt;p&gt;Compilers are only half the problem. Build processes are usually multistep, involving compiling, moving, compressing,
and so on. Consider creating an archive of a file:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;repeat &lt;span class=&#34;chroma-m&#34;&gt;4&lt;/span&gt; touch hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; tar zcf hello.tgz hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum hello.tgz &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sleep 0.5
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;f3d5c56f6b8089de95d62d060e6ffcbbad26875807ae7bc253f07cd097ea61be  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;ab67f2e865b5afa87d9b2434d92b0c271b3cf730fa85988f84852551749ba6ed  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;ab67f2e865b5afa87d9b2434d92b0c271b3cf730fa85988f84852551749ba6ed  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;738678c9650b10fd83636997dd1aba4016bbf0ec5ebf3dfd4ef75d770b56e23b  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Any file added to a tar takes with it a timestamp, so the build is only reproducible if it happens at the exact same
time! We can make this reproducible by forcing &lt;code&gt;tar&lt;/code&gt; (and the same goes for &lt;code&gt;zip&lt;/code&gt; and most other archive formats) to
set a certain timestamp on the files:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;repeat &lt;span class=&#34;chroma-m&#34;&gt;4&lt;/span&gt; touch hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; tar --mtime 2022-02-18T01:00 -zcf hello.tgz hello &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum hello.tgz &lt;span class=&#34;chroma-o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; sleep 0.5 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;081060a900beff2a6aad9957a8cbb8792f8db7904f86b318dbf26b682a2d3f0a  hello.tgz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In a real build there are basically two approaches here: you can set it to a pre-defined value (like the unix epoch),
or you can set it to match the modification time of the source files. The former is easiest, but the latter is more
cosmetically and semantically appealing.&lt;/p&gt;
&lt;h4 id=&#34;iteration-order&#34;&gt;Iteration order&lt;/h4&gt;
&lt;p&gt;So we’ve pinned our build environment, we’re manipulating timestamps when adding files to archives, now what? Imagine
part of the build process involves looping through all the files in a directory and doing &lt;em&gt;something&lt;/em&gt;. What order do
these files get iterated in? Well, that very much depends on the filesystem and perhaps when the files themselves
were created. To ensure this is reproducible we need to explicitly sort any such operation so that it’s always
consistent. This iteration could be happening in a tool that’s called by another tool that’s called by a build script,
so the fix isn’t necessarily straight-forward.&lt;/p&gt;
&lt;aside class=&#34;sidenote raised-box&#34;&gt;
  &lt;h5 class=&#34;plain-header&#34;&gt;Side note: a bug war story&lt;/h5&gt;
  &lt;p&gt;I’ve personally been victim to this kind of non-determinism. I was working on an Android app, and committed a new
test that worked fine on my machine, and worked fine on the CI server. But it failed consistently for a colleague.&lt;/p&gt;
&lt;p&gt;We both did fresh checkouts of the source, and ran the tests. Mine passed, his failed. He sent me an archive of
his checkout in case there was something weird going on there, and the tests passed on my machine. We compared
hashes of our checkouts, and they were the same. It was obviously environmental somehow, but everything else worked
fine, and the build system went to great pains to ensure things were the same.&lt;/p&gt;
&lt;p&gt;After a &lt;em&gt;lot&lt;/em&gt; of debugging, I worked out that his test was running with a different version of a library to me,
despite the libraries being defined in the build files and the build files being identical. After &lt;em&gt;even more&lt;/em&gt;
debugging it turned out there were two versions of the library on the classpath, and the ordering of them was
different between my machine and his.&lt;/p&gt;
&lt;p&gt;The actual issue turned out to be that the build tool generated the classpath by iterating over the library
files, and that iteration was done in order of file creation time. The two libraries were added at different points
in the project history, so the creation time in your local cache depended on which versions of the app you’d built
in the past. With no cache everything worked as expected but there was a slim range of commits where only one
library was in use, and if you had run the tests during that period your cache was effectively poisoned.&lt;/p&gt;
&lt;p&gt;We fixed the issue by excluding the older version of the library (which was being pulled in as a transient dependency),
and filed a bug against the build tool to make the classpath properly deterministic. I think that stands as the most
difficult to diagnose bug I’ve ever dealt with.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;Interestingly, if you iterate over a map in Go, the iteration is &lt;em&gt;deliberately&lt;/em&gt; non-deterministic. That’s an attempt
to defeat &lt;a href=&#34;https://www.hyrumslaw.com/&#34;&gt;Hyrum’s Law&lt;/a&gt; and prevent developers from relying on whatever the current
behaviour happens to be. This actually makes it easier to make things reproducible as the problem is loud and
in-your-face, rather than subtle and hard to spot.&lt;/p&gt;
&lt;h4 id=&#34;other-sources&#34;&gt;Other sources&lt;/h4&gt;
&lt;p&gt;There’s an awful lot of other places that non-determinism can come from. If the app pulls in dependencies, their
versions have to be pinned, otherwise your build changes depending on the latest release of that dependency. If
the build process pulls any information from a website, it’s liable to change. Hopefully the website is under your
control so that you can version the resource and pin that version. Obviously, anything to do with dates or the
current user will probably cause problems. Timezones and locales can cause subtle differences.&lt;/p&gt;
&lt;h3 id=&#34;what-about-docker&#34;&gt;What about Docker?&lt;/h3&gt;
&lt;p&gt;Docker comes with some good and some bad points for reproducibility. The biggest advantage is that it inherently
completely describes the build environment; it should work exactly the same from one system to another, even across
different OS families. The biggest drawback is it sprays timestamps around like no-one’s business. Each layer in
a container image is a &lt;code&gt;.tar.gz&lt;/code&gt; file, meaning each file within it is timestamped as discussed above. Making an image
involves a lot of copying of files around, so these timestamps invariably end up causing reproducibility issues.&lt;/p&gt;
&lt;p&gt;Even worse than timestamps in the filesystem, the image format also contains some meta-data that includes the
timestamp at which each layer was built. That means even if you go out of your way to set the timestamp of every
single file in your image, the image itself will be different every time you rebuild it. There is no way to deal
with this in Docker, which is a very sad state of affairs. Fortunately, &lt;a href=&#34;https://buildah.io/&#34;&gt;Buildah&lt;/a&gt; provides
a &lt;code&gt;--timestamp&lt;/code&gt; flag for &lt;em&gt;its&lt;/em&gt; build commands; this not only sets the layer timestamp but also the creation
timestamp of any file within the layer.&lt;/p&gt;
&lt;p&gt;The other major issue that affects Docker images is the pinning of packages pulled in by package managers. An awful
lot of images are based on Alpine or Debian derivatives, and use &lt;code&gt;apk&lt;/code&gt; or &lt;code&gt;apt&lt;/code&gt; to install dependencies. These need
to have a version specified as otherwise the package manager will just pull in the latest at the time of the build.
But this isn’t quite enough: you also need to pin the version of any packages that they depend on, recursively.
This means flattening the entire package hierarchy and installing all the packages explicitly and with pinned
versions.&lt;/p&gt;
&lt;p&gt;One more wrinkle in the package management space is that Alpine don’t keep old packages in their main repositories.
If you have a Docker image with pinned alpine packages in, it will stop building if the package is updated. This
isn’t necessarily fatal to making a reproducible build — as long as it’s reproducible for its useful lifetime,
I don’t really see an issue.&lt;/p&gt;
&lt;p&gt;Honestly, though, the biggest issue with making Docker images reproducible is getting people to care. Dockerfiles
are a relatively new way of packaging software, and there’s no centralised organisation like you find with Linux
distributions. There are enough challenges that most casual packagers aren’t going to bother, and no real
incentive for them to. That won’t stop me trying, though!&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Artisanal Docker images</title>
        <link href="https://chameth.com/artisanal-docker-images/"/>
        <updated>2022-02-05T00:00:00Z</updated>
        <id>https://chameth.com/artisanal-docker-images/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/artisanal-containers.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/artisanal-containers.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/artisanal-docker-images/artisanal-containers.jpg&#34; alt=&#34;Shelf showing a variety of artisanal containers&#34; loading=&#34;lazy&#34; width=&#34;300&#34; height=&#34;432&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;Artisanal containers…&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I run a fair number of services as docker containers. Recently, I’ve been moving away from pre-built images
pulled from Docker Hub in favour of those I’ve hand-crafted myself. If you’re thinking “that sounds like a
lot of effort”, you’re right. It also comes with a number of advantages, though, and has been a fairly fun
journey.&lt;/p&gt;
&lt;h3 id=&#34;the-problems-with-docker-hub-and-its-images&#34;&gt;The problems with Docker Hub and its images&lt;/h3&gt;
&lt;h4 id=&#34;rate-limits&#34;&gt;Rate limits&lt;/h4&gt;
&lt;p&gt;For the last few years, I’ve been getting increasingly unhappy with Docker Hub itself. Docker-the-technology
is wonderful, but Docker-the-company has been making some rather large missteps. The biggest and most impactful
of these has been introducing “pull rate” limits. At the time of writing, if you want to just pull a public image
without logging in then you are limited to 100 pulls every 6 hours. If you log in then you’re limited to 200 pulls
per 6 hours, but it’s account wide. This might seem like a big enough number, but I repeatedly hit it and there
is no way to actually audit what is causing it. I have various containers that may all pull images at arbitrary
times (e.g. continuous integration build agents), and the only information you get back from Docker Hub is the
number of pulls remaining.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;Obviously, I could start paying Docker Hub for a “Pro” plan. That gets you 5,000 pulls per day for $7/month.
The downside is that every docker client would have to be authenticated, which presents a fair annoyance in
terms of credential management. I also don’t really like how they positioned the service as a public utility
with special treatment in the docker software, and then start tightening the ratchet to make money.&lt;/p&gt;
&lt;h4 id=&#34;bad-images&#34;&gt;“Bad” images&lt;/h4&gt;
&lt;p&gt;I’m fairly opinionated about what a container image should look like: most importantly it should run just a
single process, and only include the bare minimum dependencies required for that. Other people think differently,
and it’s very hard to tell at a glance whether an image on Docker Hub contains just the application you want,
or whether it also bundles MySQL, Redis, Elasticsearch, and a partridge in a pear tree. Some people want that
kind of thing, but I really don’t. It’s also very hard to tell whether an image is officially endorsed by the
upstream project, and where the source Dockerfile is. This used to be better because most projects used Docker Hub’s
automatic builds, but they’re now a “pro” feature.&lt;/p&gt;
&lt;p&gt;I quite often found that I’d be looking for an image for X, and there would be 5-10 images from different users.
None of them looked official, some of them were out-of-date, some bundled the kitchen sink. Even when one looked
good, it’s a bit of a gamble whether the author is going to keep it updated or not.&lt;/p&gt;
&lt;h4 id=&#34;doijanky&#34;&gt;Doijanky&lt;/h4&gt;
&lt;p&gt;The rate limits and other problems were annoying, but they weren’t really annoying enough to force me to do
anything about it. The straw that broke the camel’s back came later: I was looking at the
&lt;a href=&#34;https://hub.docker.com/_/golang&#34;&gt;official golang images&lt;/a&gt;, and noticed that all the tags were pushed by a
random user account called “doijanky”:&lt;/p&gt;
&lt;figure class=&#34;image center&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/doijanky.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/artisanal-docker-images/doijanky.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/artisanal-docker-images/doijanky.png&#34; alt=&#34;An &amp;#39;official&amp;#39; Docker Hub image pushed by user &amp;#39;doijanky&amp;#39;&#34; loading=&#34;lazy&#34; width=&#34;786&#34; height=&#34;249&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;An ‘official’ Docker Hub image pushed by user ‘doijanky’&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I, perhaps naively, assumed that official images were built on Docker Hub’s own infrastructure. Why would
all the Golang images be attributed to this user? Checking out their profile, they’re simply identified as
a “Community User” like everyone else, with no repositories of their own. The only thing in the profile is
their homepage, which is a link to a Jenkins dashboard: &lt;a href=&#34;https://doi-janky.infosiftr.net/&#34;&gt;https://doi-janky.infosiftr.net/&lt;/a&gt;. It appears
legitimate: “Infosiftr” are a container consultancy and the dashboard is linked to from the README in the
official images git repository, but I find it baffling that they’re using third-party infrastructure and
a normal user account (with a dubious name) to push these images. There doesn’t seem to be a good way to
verify what you pull corresponds to the Dockerfile it came from; if infosiftr wanted to inject something
into the build they could happily do so, and who knows how good their infosec posture is? If someone got
access to the “doijanky” account, how long could they upload malicious images before someone noticed?&lt;/p&gt;
&lt;p&gt;This little roller-coaster ride from “are all the official images compromised?!” to “oh, no, they’re not,
it’s all just awful” finally convinced me to look at building my own images from scratch.&lt;/p&gt;
&lt;h3 id=&#34;the-implementation-templating-with-contempt&#34;&gt;The implementation: templating with contempt&lt;/h3&gt;
&lt;p&gt;One of the big issues I needed to tackle was how to deal with updates. I didn’t want to have to go and
edit a file every time some minor release was made of some software, or every time there was a security
vulnerability in a common library. The official images use a shell-scripting based system to check for
updates and generate Dockerfiles, I decided to do something similar but with Go templates. The result is
a tool called &lt;a href=&#34;https://github.com/csmith/contempt&#34;&gt;contempt&lt;/a&gt;. It takes a template like:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;{{image&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;golang&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt; AS build&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;ARG&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;TAG&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;{{github_tag &amp;#34;&lt;/span&gt;example/project&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;}}&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;RUN&lt;/span&gt; apk add --no-cache &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-o&#34;&gt;{{&lt;/span&gt;range &lt;span class=&#34;chroma-nv&#34;&gt;$key&lt;/span&gt;, &lt;span class=&#34;chroma-nv&#34;&gt;$value&lt;/span&gt; :&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt; alpine_packages &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;git&amp;#34;&lt;/span&gt; -&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-o&#34;&gt;{{&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;$key&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;}}={{&lt;/span&gt;&lt;span class=&#34;chroma-nv&#34;&gt;$value&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-o&#34;&gt;{{&lt;/span&gt;end&lt;span class=&#34;chroma-o&#34;&gt;}}&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Contempt has support for getting information from a variety of sources. In this case, it’s getting
the latest digest of another Docker image, the latest tag from a Git repository, and the latest version
of an alpine package and all its dependencies. The resulting Dockerfile looks something like this:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c&#34;&gt;# Generated from https://github.com/csmith/dockerfiles/blob/master/miniflux/Dockerfile.gotpl&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c&#34;&gt;# BOM: {&amp;#34;apk:brotli-libs&amp;#34;:&amp;#34;1.0.9-r5&amp;#34;,&amp;#34;apk:busybox&amp;#34;:&amp;#34;1.34.1-r4&amp;#34;, &amp;lt;snip&amp;gt; }&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;reg.c5h.io/golang@sha256:ac8fa5f4078b0a697796b5d741&lt;/span&gt;... AS build&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;ARG&lt;/span&gt; &lt;span class=&#34;chroma-nv&#34;&gt;TAG&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;2.0.35&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;RUN&lt;/span&gt; apk add --no-cache &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        brotli-libs&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;1.0.9-r5&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nv&#34;&gt;busybox&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;1.34.1-r4&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-c1&#34;&gt;# &amp;lt;snip&amp;gt;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nv&#34;&gt;pcre2&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;10.39-r0&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nv&#34;&gt;zlib&lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;=&lt;/span&gt;1.2.11-r3&lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;chroma-se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-c1&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I’ve cut out the longer parts for readability. You can see that it pins all versions of the alpine packages in use,
as well as the base image. This ensures that if you build from the same Dockerfile at a later time it will build the
same image (or will fail entirely, as Alpine doesn’t keep their old packages around indefinitely). It also produces
a “bill of materials” as a really long JSON-encoded comment. If you let contempt commit the Dockerfile it uses the
BOM to generate useful commit messages like: &lt;code&gt;[project] apk:busybox: 1.34.1-r3-&amp;gt;1.34.1-r4&lt;/code&gt;, so you can see exactly
what changed.&lt;/p&gt;
&lt;p&gt;Contempt also has support for building and pushing images whenever it changes the Dockerfile. I use it in a
GitHub action that runs daily to check all my images are up-to-date and push those that aren’t. It understands
the dependencies between images (by pre-analysing the templates) so it will always check and build base images
before ones that require them. This means an update to, say, the “alpine” base image will cause anything that
depends on it to get updated at the same time, ensuring security updates are rolled out promptly.&lt;/p&gt;
&lt;h3 id=&#34;the-result&#34;&gt;The result&lt;/h3&gt;
&lt;p&gt;You can see my collection of lovingly hand-crafted Dockerfiles in my &lt;a href=&#34;https://github.com/csmith/dockerfiles&#34;&gt;dockerfiles&lt;/a&gt;
repository.&lt;/p&gt;
&lt;p&gt;There are a number of advantages to handwriting all the images I use. The obvious one is that they’re all built
how I want: there are no extraneous dependencies, they’re all based on the same small set of base images (rather
than pulling around 10 different versions of debian), nothing tries to also run a DBMS in its container, etc.&lt;/p&gt;
&lt;p&gt;This level of customisation goes further, though. Because I’m packaging the software myself, I can tweak how it’s
built to fit my needs. A couple of things I run need their own TLS certificates separate from my normal HTTPS
setup, so I bake my &lt;a href=&#34;https://github.com/csmith/certwrapper/&#34;&gt;certwrapper&lt;/a&gt; tool in to manage those; I can even set the
build flags on certwrapper to only enable the particular DNS provider I personally need (thus avoiding dragging in
clients for AWS, GCP, etc). Some software like Hashicorp Vault has an optional web interface that I don’t need,
so I simply don’t enable it in the build. These changes save build time, reduce image sizes, in some cases improve
runtime performance, and generally reduce the attack surface of what’s running in the container.&lt;/p&gt;
&lt;p&gt;It’s also been a great way to learn more about how software is distributed. Writing a Dockerfile is not that distant
from writing a PKGBUILD file for an Arch Linux package, or the equivalent for other distributions. In a couple of
instances I’ve googled how to solve a particular issue, and found an Arch or Void linux maintainer asking the upstream
project about the exact same issue.&lt;/p&gt;
&lt;p&gt;Finally, all the images I build I push to my own registry so there are obviously no rate limiting issues.
Standing up a service (assuming the Dockerfile has been written!) is amazingly quick because the base layers are all
shared and cached, and the registry is a lot physically closer than Docker Hub. Bootstrapping this whole thing becomes
an interesting problem because the image for the registry is stored on the registry, but I’ll leave that discussion for
another post…&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>An introduction to containers</title>
        <link href="https://chameth.com/intro-to-containers/"/>
        <updated>2020-03-01T00:00:00Z</updated>
        <id>https://chameth.com/intro-to-containers/</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/intro-to-containers/containers.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/intro-to-containers/containers.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/intro-to-containers/containers.jpg&#34; alt=&#34;Containers in port&#34; loading=&#34;lazy&#34; width=&#34;300&#34; height=&#34;396&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;So. Many. Containers.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I’m a huge fan of (software) containers. Most people I know fall in to one of two camps:
either they also use, and are fans of, containers, or they haven’t yet really figured them
out and view them as some kind of voodoo that they don’t really want or need.&lt;/p&gt;
&lt;p&gt;I’m writing this short guide to explain a little how containers work - and how running
something in a container isn’t really that much different to running it normally -
to hopefully enable more people in that second group to give them a try. It’s aimed at
people who have a fairly good grasp of how Linux works.&lt;/p&gt;
&lt;p&gt;Containers are often mentioned in the same breath as VMs, which is not a helpful comparison
or analogy. Think of containers as standard units of software, much like how
&lt;a href=&#34;https://en.wikipedia.org/wiki/Intermodal_container&#34;&gt;Intermodal containers&lt;/a&gt; are standard
units of freight transport across the world. When a company internationally ships goods in
volume there isn’t a question about how they’re packaged - they go in an intermodal container.
The same container can be deployed on a freight train, a lorry, or a ship. The haulage company
doesn’t need to care what’s in the container because they’re completely standardised.
Likewise, with software containers you don’t really need to care about what’s inside: the
software you’re deploying could be written in Go, Python2, Python3, Bash, PHP, LOLCODE, or
anything&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h3 id=&#34;what-does-a-running-container-look-like&#34;&gt;What does a running container look like?&lt;/h3&gt;
&lt;p&gt;When you run a container, you’re just running a process. In a lot of ways it’s not
very different to what happens if you were to start the same process on the host
computer.&lt;/p&gt;
&lt;p&gt;For example I have a container that just runs &lt;code&gt;cat(1)&lt;/code&gt;. With no arguments, &lt;code&gt;cat&lt;/code&gt;
will read from stdin until it receives an EOF, so it’s handy to test with.
If I run &lt;code&gt;ps a&lt;/code&gt; on my computer, I can see the cat process in amongst everything
else I’m currently running:&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;    PID TTY      STAT   TIME COMMAND
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   7199 pts/1    Ss     0:01 /usr/bin/zsh
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt; 323806 pts/0    Ss+    0:00 /bin/cat
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt; 324120 pts/4    R+     0:00 ps a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;/bin/cat&lt;/code&gt; process is in a container, and the &lt;code&gt;ps a&lt;/code&gt; underneath it is just running
like normal on my desktop. They look very similar, right? If I look under &lt;code&gt;/proc/323806&lt;/code&gt;
I can see all the usual attributes, the same as any other process running.&lt;/p&gt;
&lt;p&gt;If I run &lt;code&gt;ps&lt;/code&gt; in a container, though, it’s a different story:&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;    PID TTY      STAT   TIME COMMAND
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      1 pts/0    Rs+    0:00 ps a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So inside the container it looks like there’s only one process running. It can’t see
anything running “outside” on my desktop. The secret here is that this isn’t a special
container trick: this is just a feature of the Linux kernel called &lt;em&gt;namespacing&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;If we go back to procfs and look at the &lt;code&gt;ns/pid&lt;/code&gt; node we can see the process in the
container is in a separate PID (process ID) namespace to the one on my desktop:&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;# readlink /proc/323806/ns/pid  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;pid:[4026534564]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;# readlink /proc/7199/ns/pid
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;pid:[4026531836]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Almost all&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; processes running ’normally’ on my desktop have the same PID namespace,
whereas each container gets their own by default. PID namespaces are hierarchical:
a new process is assigned a PID in its own namespace, and the parent namespace, and
the grandparent namespace, and so on. That’s why I can see the process running in
the container from my normal shell - the container’s namespace is a child of the
main namespace all of my desktop software is running in.&lt;/p&gt;
&lt;p&gt;Linux supports - and container software makes use of - a bunch of other namespaces too:
mount points, network, UTS&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;, cgroups, and more. These all play a part in isolating
a container from the system it is running on.&lt;/p&gt;
&lt;p&gt;You can manually run a process with &lt;code&gt;unshare(1)&lt;/code&gt; to “unshare” some namespaces from
the parent process. For example if I run &lt;code&gt;unshare -fp --mount-proc ps a&lt;/code&gt;, it
looks very similar to running ps instead the container:&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;    PID TTY      STAT   TIME COMMAND
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      1 pts/4    R+     0:00 ps a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So: a process running inside a container is just a heavily namespaced process running
otherwise normally in the operating system. No voodoo magic here!&lt;/p&gt;
&lt;h3 id=&#34;what-about-the-filesystem-what-are-images&#34;&gt;What about the filesystem? What are ‘images’?&lt;/h3&gt;
&lt;p&gt;Containers run in their own mount namespace meaning mount points can be different
inside the container to those on the host. This means the container can have a different
&lt;code&gt;/&lt;/code&gt; mounted to the host, effectively giving it its own filesystem.&lt;/p&gt;
&lt;p&gt;The root filesystem of the container is defined in the container’s image. If I
use Docker to run a container using the Ubuntu image (&lt;code&gt;docker run ubuntu&lt;/code&gt;), the root
filesystem inside that container will resemble a minimal ubuntu install. Note
that this is just the filesystem: the container doesn’t have its own kernel.&lt;/p&gt;
&lt;p&gt;You might be thinking that sounds pretty inefficient. Downloading Ubuntu is definitely
not instant, and doing it for every application you run would be insane! Quite.
Containers solve this by using filesystem &lt;strong&gt;layers&lt;/strong&gt;. These are stacked on top of
one another to create the final filesystem. Each layer can be retrieved and
cached independently of all others.&lt;/p&gt;
&lt;p&gt;Say (for simplicity) that the Ubuntu image is a single layer. If I run one container
with that image, then the layer will be downloaded and cached once. If I run three
hundred containers with that image then the layer will be downloaded and cached once.
Even better, if I use another image that’s based on Ubuntu but adds some software
on top, only the “on top” layer will be downloaded if I already have the relevant
“Ubuntu” layer cached.&lt;/p&gt;
&lt;p&gt;If all the layers are cached, what happens when you change a file? This is dealt
with using the copy-on-write technique: when you modify a file it is copied from
the source layer and the changes are only made in a new layer. This is handled
by the OverlayFS filesystem which is part of the mainline kernel.&lt;/p&gt;
&lt;p&gt;When a container is running, changes made to its filesystem are temporary, and
do not persist across container restarts. To persist data - or introduce new
data to a container - you can mount volumes. How this works varies depending
on how you’re running your container, but at the basic level it is pretty much
the same as bind-mounting (&lt;code&gt;mount -o bind&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;You may be familiar with using &lt;code&gt;chroots&lt;/code&gt; to change the apparent root directory
of processes, perhaps with full-blown “jails” built on top. Containers offer
much better isolation thanks to the use of namespaces. Instead of being
constrained to a portion of the host’s filesystem, they don’t even have
it mounted! Containers also get to specify their environment - if they
expect in certain places, for example - instead of the sysadmin having to
manually set up the chroot. Finally, containers offer much more fine-grained
control over what processes can do (if you want it), and allow much more
advanced use-cases such as inter-container networking.&lt;/p&gt;
&lt;p&gt;Images and filesystems employ a little magic to ensure that layers are reusable
and cacheable, but again there’s nothing terribly special about them: a container
has a filesystem that appears to it to work the same way as a filesystem on
the host, and it’s using a standard filesystem shipped with the kernel.&lt;/p&gt;
&lt;h3 id=&#34;how-about-networking&#34;&gt;How about networking?&lt;/h3&gt;
&lt;p&gt;Again, networking is namespaced, so a container has its own network stack,
its own virtual network interface, its own IP address and so on. How that network
interacts with your real network depends on how you’re running the container.
Docker, for example, can add iptables rules to NAT traffic between containers&amp;#39;
networks and the outside world.&lt;/p&gt;
&lt;p&gt;Containers can generally be connected into networks, and can communicate
amongst themselves without the traffic actually leaving the host machine.
This allows you to, for example, run a SQL database and connect it to
a web application without ever exposing the database to the outside world.
Moreover, as well as being isolated from the outside world, it’s isolated
from other containers in other networks. If one of your applications has a crazy bug
or is compromised, this significantly limits the damage it can do.&lt;/p&gt;
&lt;p&gt;You have to explicitly opt in to “publishing” ports from a container, which
exposes them to the outside world (either directly, or via a load balancer
or some other middle-man, depending on how you’re running the container).
This means you can pick and chose how the outside world sees the app you’re
deploying: if it’s a web service that listens on both port 443 and port 80,
you can chose to only expose the encrypted port.&lt;/p&gt;
&lt;p&gt;If you run some containers and create some networks, you can see the
interfaces and bridges on the host:&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;$ ip l
&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;6: br-2405a8cc0445: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UP mode DEFAULT group default 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    link/ether 02:42:3e:fa:23:62 brd ff:ff:ff:ff:ff:ff
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;8: veth8ed0735@if7: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue master br-2405a8cc0445 state UP mode DEFAULT group default 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    link/ether b2:c1:5d:55:26:9b brd ff:ff:ff:ff:ff:ff link-netnsid 2
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;10: veth541d84b@if9: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue master br-9d7bc4024c1a state UP mode DEFAULT group default 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    link/ether 86:aa:5f:ee:da:1a brd ff:ff:ff:ff:ff:ff link-netnsid 1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This shows a bridge and two virtual NICs, just the same as if you’d manually created
them (albeit with seemingly random names). So: as before, nothing special here.&lt;/p&gt;
&lt;h3 id=&#34;docker-compose-k8s-floccinaucinihilipilification&#34;&gt;Docker? Compose? K8s? Floccinaucinihilipilification?&lt;/h3&gt;
&lt;p&gt;(OK, Floccinaucinihilipilification isn’t actually a container technology, as far
as I’m aware.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker&lt;/strong&gt; is the most popular &lt;em&gt;container engine&lt;/em&gt; - that is, the bit of software
that actually runs containers. It’s responsible for setting up all those namespaces
we found, downloading and caching image layers, and actually starting and stopping
the processes. Docker runs as a system-wide daemon - when you run a command like
&lt;code&gt;docker run ubuntu&lt;/code&gt; it actually just instructs the daemon to do the work.&lt;/p&gt;
&lt;p&gt;There are several alternatives to Docker for running containers; one interesting one
is &lt;a href=&#34;https://podman.io/&#34;&gt;Podman&lt;/a&gt; which runs containers without a daemon. Container
engines have all standardised around the same image format looked after by the
&lt;a href=&#34;https://www.opencontainers.org/&#34;&gt;Open Container Initiative&lt;/a&gt;, so an image you build
in Docker can be used in Podman, or pretty much any other engine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker compose&lt;/strong&gt; is a tool for defining and running multiple-container applications.
I mentioned earlier running a database alongside a webapp - in practice to do this
you’re going to have to configure a network for them, configure a mount point for
the database to persist its data on, pass credentials in to both the database and
the application, and so on. Doing all that by hand is tedious and error prone.&lt;/p&gt;
&lt;p&gt;Docker compose lets you write “compose files”, which are simple yaml descriptions
of the containers you wish to run, their properties, and details about any volumes
or networks you may want. Out of the box, docker-compose will create a default
network for each compose file you run so the containers within it can communicate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt;  is a container &lt;em&gt;orchestrator&lt;/em&gt;, designed to automate deployment
and management of large numbers of containers. It works with Docker under the
hood, but provides a huge amount of tooling on top to allow you to deploy
applications and manage their dependencies. It runs across multiple physical
(or virtual) machines (while still allowing containers to communicate privately),
and can support massive workloads by scaling out services (running multiple copies
of a container on different hosts) and load balancing. Kubernetes is sometimes
shorted to &lt;strong&gt;k8s&lt;/strong&gt; (as in &lt;code&gt;K&lt;/code&gt; - 8 elided letters - &lt;code&gt;s&lt;/code&gt;) because computer people
don’t like long words.&lt;/p&gt;
&lt;h3 id=&#34;ok-they-make-sense-now-but-why-bother&#34;&gt;OK, they make sense now. But why bother?&lt;/h3&gt;
&lt;p&gt;Hopefully if you’ve read this far you’ve already picked up on some of the potential
benefits, but this is my personal list:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Isolation&lt;/strong&gt;. If I run some software in a container, there is very little it
can do to upset me. It’s not going to leave bits of itself all over my filesystem,
it can’t steal all of the secrets in my home directory, I can even limit its CPU
and memory resources if I want. If I decide to stop running it, I just delete
the container and it is completely gone: no trace remains.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ease of use&lt;/strong&gt;. If you give me a container image I have a very good idea of how
to run it already. I might need to do some minor configuration to expose ports
or mount volumes, but there’s no question about how to run it, how to make it
automatically start, and there’s no “installation” procedure. If I want to
then swap it with an alternative (say, move from MySQL to MariaDB), it’s
potentially just a case of changing the name of the image I pull.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dependencies included&lt;/strong&gt;. Containers just &lt;em&gt;run&lt;/em&gt;. Python 2 software includes
Python 2 and just work. Python 3 software includes Python 3 and just work.
I don’t have a massive headache trying to run both at the same time, because
they take care of their own messes. Similarly I’m not going to have to install
&lt;code&gt;npm&lt;/code&gt; or &lt;code&gt;cargo&lt;/code&gt; or &lt;code&gt;composer&lt;/code&gt; to pull in dependencies for an application:
that’s going to have been done in the build process.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt;. As a fallout from having dependencies included and being
isolated from everything else, containers give you amazing reproducibility.
If it “works on your machine” in a container, it’ll almost certainly work in
production because it’s the exact same environment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standardisation&lt;/strong&gt;. At the start of this article I called containers standard
units of software. One of my favourite advantages of containers is that you
basically get an API to list all the software you’re running. Most container
engines let you supply labels attached to containers as well, so you can add
your own annotations. I use this to annotate services which expose HTTP
endpoints, and I have a tool that automatically generates SSL certificates
for them and configures haproxy to route traffic to them. I can’t imagine
how I’d do this without containers - I imagine it’d involve a lot of
manual work.&lt;/p&gt;
&lt;p&gt;Hopefully this has helped demystify containers a little. If you feel like
I’ve missed something important out, or I’ve left you more confused than
when you started, feel free to drop me a note using the feedback form below.&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;OK, maybe you should care if you’re deploying something written in crazy languages like PHP. &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;Some multi-process apps, such as web browsers, are starting to use namespaces to enhance security,
as do certain package systems like Flatpak &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;“Unix timesharing system”; in practice this means having a separate hostname &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>
</feed>
