<?xml version="1.0" encoding="utf-8" standalone="yes"?>

<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Thomas Letan's Blog - docker</title>
    <link>https://soap.coffee/~lthms/tags/docker.html</link>
    <description>Posts tagged "docker"</description>
    <atom:link href="https://soap.coffee/~lthms/tags/docker.xml" rel="self"
               type="application/rss+xml" />
    
    
    <item>
      <title>I Cannot SSH Into My Server Anymore (And That’s Fine)</title>
      <link>https://soap.coffee/~lthms/posts/i-cannot-ssh-into-my-server-anymore.html</link>
      <guid>https://soap.coffee/~lthms/posts/i-cannot-ssh-into-my-server-anymore.html</guid>
      <pubDate>January 5, 2026</pubDate>
      <description>
        
        &lt;h1&gt;I Cannot SSH Into My Server Anymore (And That’s Fine)&lt;/h1&gt;&lt;div id=&quot;tags-list&quot;&gt;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/coreos.html&quot; class=&quot;tag hover-coral&quot; marked=&quot;&quot;&gt;coreos&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/docker.html&quot; class=&quot;tag hover-lavender&quot; marked=&quot;&quot;&gt;docker&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/meta.html&quot; class=&quot;tag hover-mint&quot; marked=&quot;&quot;&gt;meta&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/self-hosting.html&quot; class=&quot;tag hover-lavender&quot; marked=&quot;&quot;&gt;self-hosting&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/terraform.html&quot; class=&quot;tag hover-lemon&quot; marked=&quot;&quot;&gt;terraform&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/vultr.html&quot; class=&quot;tag hover-sky&quot; marked=&quot;&quot;&gt;vultr&lt;/a&gt; &lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I would like to thank Yann Régis-Gianas, Sylvain Ribstein and Paul Laforgue
for their feedback and careful review.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To kick off 2026, I had clear objectives in mind: decommissioning &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt;, my
trusty $100+/month VPS, and setting up &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;, its far less costly
successor.&lt;/p&gt;
&lt;p&gt;On the one hand, I have been using &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt; to self-host a number of services,
and it was very handy to know that I had always a go-to place to experiment
with whatever caught my interest. On the other hand, $100/month is obviously a
lot of money, and looking back at how I used it in 2025, it was not
particularly well spent. It was time to downsize.&lt;/p&gt;
&lt;p&gt;Now that &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; is up and running, I cannot even SSH into it. In fact,
&lt;em&gt;nothing&lt;/em&gt; can.&lt;/p&gt;
&lt;p&gt;There is no need. To update one of the services it hosts, I push a new container
image to the appropriate registry with the correct tag. &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; will fetch
and deploy it. All on its own.&lt;/p&gt;
&lt;p&gt;In this article, I walk through the journey that led me to the smoke and
mirrors behind this magic trick: &lt;a href=&quot;https://fedoraproject.org/coreos/&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;Fedora CoreOS&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;, &lt;a href=&quot;https://coreos.github.io/ignition/&quot; class=&quot;hover-mint&quot; marked=&quot;&quot;&gt;Ignition&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; and &lt;a href=&quot;https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html&quot; class=&quot;hover-sky&quot; marked=&quot;&quot;&gt;Podman
Quadlets&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; in the main roles, with &lt;a href=&quot;https://developer.hashicorp.com/terraform&quot; class=&quot;hover-coral&quot; marked=&quot;&quot;&gt;Terraform&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; as an essential supporting
character. This stack checks all the boxes I care about.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewbox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;For interested readers, I have published &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;’s &lt;a href=&quot;https://github.com/lthms/tinkerbell&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;full setup&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; on
GitHub. This article reads as an experiment log, and if you are only
interested in the final result, you should definitely have a look.&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;Container-Centric, Declarative, and Low-Maintenance&lt;/h2&gt;
&lt;p&gt;Going into this, I knew I didn’t want to reproduce &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt;’s setup—it was fully
manual&lt;label for=&quot;fn1&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn1&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;In the end, I never took the time to publish a write-up about it, so
in a nutshell: everything relied on &lt;a href=&quot;https://github.com/lthms/nspawn&quot; class=&quot;hover-periwinkle&quot; marked=&quot;&quot;&gt;a small script&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; that enabled
me to create interconnected &lt;a href=&quot;https://man7.org/linux/man-pages/man5/systemd.nspawn.5.html&quot; class=&quot;hover-coral&quot; marked=&quot;&quot;&gt;nspawn containers&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; on the spot. &lt;/span&gt;
&lt;/span&gt; and I no longer have the time or the motivation to fiddle with
the internals of a server. Instead, I wanted to embrace the principles my
DevOps colleagues had taught me over the past two years.&lt;/p&gt;
&lt;p&gt;My initial idea was to start with this very website, since it was the only
service deployed on &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt; that I really wanted to keep. Since &lt;a href=&quot;./DreamWebsite.html&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;I had written
a container image for this website&lt;/a&gt;, I just had to look
for the most straightforward and future-proof way to deploy it in production™—
something I could later extend to deploy more cool projects, if I ever wanted
to&lt;label for=&quot;fn2&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn2&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;If my goal was limited to host a static website, this whole setup
would arguably be both overengineered and counterproductive. &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;
was to become my little foothold in the Internet, though. My “cloud
homelabs,” of sorts. &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/compose/&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;Docker Compose&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; alone wasn’t a good fit. I like compose files, but one needs
to provision and manage a VM to host them. Ansible can provision VMs, but that
road comes with its own set of struggles. Writing good playbooks has always
felt surprisingly difficult to me. In particular, a good playbook is supposed
to handle two very different scenarios—provisioning a brand new machine, and
updating a pre-existing deployment—and I have found it particularly challenging
to ensure that both paths reliably produce the same result.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kubernetes.io/&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;Kubernetes&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; was &lt;em&gt;very&lt;/em&gt; appealing on paper. I have seen engineers turn compose
files into &lt;a href=&quot;https://helm.sh/docs/topics/charts/&quot; class=&quot;hover-rose&quot; marked=&quot;&quot;&gt;Helm charts&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; and be done with it. If I could do the same thing,
wouldn’t that be bliss? Unfortunately, Kubernetes is a notoriously complex
stack, resulting from compromises made to address challenges I simply don’t
face. Managed clusters could make things easier, but they aren’t cheap. That
would defeat the initial motivation behind retiring &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://fedoraproject.org/coreos/&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;CoreOS&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;, being an operating system specifically built to &lt;em&gt;run containers&lt;/em&gt;,
obviously stood out. That said, I had very little intuition on how it could
work in practice. So I started digging. I learned about Ignition first. Its
purpose is to provision a VM exactly once, at first boot. If you need to change
something afterwards, you throw away your VM and create a new one. This may
seem counter-intuitive, but since it eliminates the main reason I was looking
for an alternative to Ansible, I was hooked&lt;label for=&quot;fn3&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn3&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;CoreOS and Ignition enable me to think about virtual machines the same
way OCaml or Haskell trained me to think about data: as immutable values. &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;I found out how to use systemd unit files to start containers via &lt;code class=&quot;hljs&quot;&gt;podman&lt;/code&gt; CLI
commands. That was way too cumbersome, so I pushed on for a way to orchestrate
containers &lt;em&gt;à la&lt;/em&gt; Docker Compose. That’s when I discovered Podman Quadlets and
&lt;a href=&quot;https://docs.podman.io/en/stable/markdown/podman-auto-update.1.html&quot; class=&quot;hover-rose&quot; marked=&quot;&quot;&gt;auto-updates&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With that, everything clicked. I knew what I wanted to do, and I was very
excited about it.&lt;/p&gt;
&lt;h2&gt;Assembling &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;For more than a year now, my website has been &lt;a href=&quot;./DreamWebsite.html&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;served from RAM by a standalone,
static binary built in OCaml&lt;/a&gt;, with TLS termination handled by Nginx and
&lt;code class=&quot;hljs&quot;&gt;certbot&lt;/code&gt;’s certificates renewal performed by yours truly&lt;label for=&quot;fn4&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn4&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;I didn’t lie when I said &lt;code class=&quot;hljs&quot;&gt;moana&lt;/code&gt;’s setup was indeed &lt;em&gt;manual&lt;/em&gt;. &lt;/span&gt;
&lt;/span&gt;. I didn’t
have any reason to fundamentally change this architecture. I was simply looking
for a way to automate their deployment.&lt;/p&gt;
&lt;h3&gt;Container-Centric, …&lt;/h3&gt;
&lt;p&gt;The logical thing to do was to have &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; run two containers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The reverse proxy:&lt;/strong&gt; I had been firmly on Team Nginx for years now, but
when I heard &lt;a href=&quot;https://caddyserver.com/&quot; class=&quot;hover-coral&quot; marked=&quot;&quot;&gt;Caddy&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; could “&lt;em&gt;automatically obtain&lt;/em&gt; and &lt;em&gt;renew&lt;/em&gt; TLS
certificates,” I was sold on giving it a try.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The website itself:&lt;/strong&gt; Static binaries can be wrapped inside a container
with close to zero overhead using the &lt;a href=&quot;https://hub.docker.com/_/scratch&quot; class=&quot;hover-lemon&quot; marked=&quot;&quot;&gt;&lt;code class=&quot;hljs&quot;&gt;scratch&lt;/code&gt;&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; base image, so I
did just that. I published it to a free-plan, public registry hosted on Vultr
that I created for the occasion&lt;label for=&quot;fn5&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn5&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Which means getting an offline copy of this website is now as
simple as calling &lt;code class=&quot;hljs&quot;&gt;docker pull ams.vultrcr.com/lthms/www/soap.coffee:live&lt;/code&gt;. &lt;/span&gt;
&lt;/span&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://mermaid.ink/img/pako:eNo9UMtOwzAQ_BVrTyCllZs4JPEBiYZjuQASEnUPTrxJLBK7ch1BafPvuC_2tDM7M7vaA9RWIXBoevtdd9J5snoVhoR6Wpe9RuM3ZDZ7PPKcEs5YciTLtYBSKrUnd--rt3sBm4t-N1atk9uOeG2-0FXY95fB8hZwJOX6A6ud9nj1oFEQQeu0Au7diBEM6AZ5gnA4SQT4DgcUwENrcPRO9gKEmYJtK82ntcPN6ezYdsAb2e8CGrdKenzWMtw0_LMuLERX2tF44AnLziHAD_ATIM3mMY3zlCZskRUPRQR74DGL58WCXrk8S6cIfs9b6TxlWZ4EcsFYkdOgR6W9dS-Xj9bWNLqF6Q97DWuS?type=png&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Nothing beats a straightforward architecture&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Nothing fancy or unexpected here, which made it a good target for a first
deployment. It was time to open Neovim to write some YAML.&lt;/p&gt;
&lt;h3&gt;Declarative, …&lt;/h3&gt;
&lt;p&gt;At this point, the architecture was clear. The next step was to turn it into
something a machine could execute. To that end, I needed two things: first an
Ignition configuration, then a CoreOS VM to run it.&lt;/p&gt;
&lt;h4&gt;The Proof of Concept&lt;/h4&gt;
&lt;p&gt;Ignition configurations (&lt;code class=&quot;hljs&quot;&gt;.ign&lt;/code&gt;) are JSON files primarily intended to be
consumed by machines. They are produced from YAML files using a tool called
&lt;a href=&quot;https://coreos.github.io/butane/&quot; class=&quot;hover-sky&quot; marked=&quot;&quot;&gt;Butane&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. For instance, here is the first Butane configuration file I ended up
writing. It provisions a CoreOS VM by creating a new user (&lt;code class=&quot;hljs&quot;&gt;lthms&lt;/code&gt;), along with
a &lt;code class=&quot;hljs&quot;&gt;.ssh/authorized_keys&lt;/code&gt; file allowing me to SSH into the VM&lt;label for=&quot;fn6&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn6&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;I didn’t know at the time that I would &lt;em&gt;deliberately&lt;/em&gt; remove these
lines from the final Butane file. &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;variant:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;fcos&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;version:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1.5&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;.0&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;passwd:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;users:&lt;/span&gt;
    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;lthms&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;ssh_authorized_keys:&lt;/span&gt;
        &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;ssh-ed25519&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;AAAAC3NzaC1lZDI1NTE5AAAAIKajIx3VWRjhqIrza4ZnVnnI1g2q6NfMfMOcnSciP1Ws&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;lthms@vanellope&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What’s important to keep in mind is that Ignition runs exactly once, at first
boot. Then it is never used again. This single fact has far-reaching
consequences, and is the reason why any meaningful change implies replacing the
machine, not modifying it.&lt;/p&gt;
&lt;p&gt;Before going any further, I wanted to understand how the actual deployment was
going to work. I generated the Ignition configuration file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;butane main.bu &amp;gt; main.ign
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, I decided to investigate how to define the Vultr VM in Terraform. The
resulting configuration is twofold. First, we need to configure Terraform to be
able to interact with the Vultr API, using the &lt;a href=&quot;https://registry.terraform.io/providers/vultr/vultr/latest/docs&quot; class=&quot;hover-periwinkle&quot; marked=&quot;&quot;&gt;Vultr provider&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. Second, I
needed to &lt;a href=&quot;https://registry.terraform.io/providers/vultr/vultr/latest/docs/resources/instance&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;create the VM&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;&lt;label for=&quot;fn7&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn7&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;For discovering what values to put in most fields, &lt;code class=&quot;hljs&quot;&gt;vultr-cli&lt;/code&gt; is
pretty convenient. Kudos to the Vultr team for making it in the first
place. &lt;/span&gt;
&lt;/span&gt; and feed it the Ignition
configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-hcl&quot;&gt;resource &quot;vultr_instance&quot; &quot;tinkerbell&quot; {
  region = &quot;cdg&quot;
  plan = &quot;vc2-1c-1gb&quot;
  os_id = &quot;391&quot;

  label = &quot;tinkerbell&quot;
  hostname = &quot;tinkerbell&quot;

  user_data = file(&quot;main.ign&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that was it. I invoked &lt;code class=&quot;hljs&quot;&gt;terraform apply&lt;/code&gt;, waited for a little while, then
SSHed into the newly created VM with my &lt;code class=&quot;hljs&quot;&gt;lthms&lt;/code&gt; user. Sure enough, the
&lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; VM was now listed in the Vultr web interface. I explored for a
little while, then called &lt;code class=&quot;hljs&quot;&gt;terraform destroy&lt;/code&gt; and rejoiced when everything
worked as expected.&lt;/p&gt;
&lt;h4&gt;The MVP&lt;/h4&gt;
&lt;p&gt;At this point, I was basically done with Terraform, and I just needed to write
the Butane configuration that would bring my containers to life. As I mentioned
earlier, the first approach I tried was to define a systemd service responsible
for invoking &lt;code class=&quot;hljs&quot;&gt;podman&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;systemd:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;units:&lt;/span&gt;
    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;soap.coffee.service&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;enabled:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;contents:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;|
        [Unit]
        Description=Web Service
        After=network-online.target
        Wants=network-online.target
&lt;/span&gt;
        [&lt;span class=&quot;hljs-string&quot;&gt;Service&lt;/span&gt;]
        &lt;span class=&quot;hljs-string&quot;&gt;ExecStart=/usr/bin/podman&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;hljs-string&quot;&gt;--name&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;soap.coffee&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;hljs-string&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;8901&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:8901&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;hljs-string&quot;&gt;--restart=always&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;hljs-string&quot;&gt;ams.vultrcr.com/lthms/www/soap.coffee:latest&lt;/span&gt;
        &lt;span class=&quot;hljs-string&quot;&gt;ExecStop=/usr/bin/podman&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;stop&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;soap.coffee&lt;/span&gt;

        [&lt;span class=&quot;hljs-string&quot;&gt;Install&lt;/span&gt;]
        &lt;span class=&quot;hljs-string&quot;&gt;WantedBy=multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding this entry in my Butane configuration and redeploying &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; got
me exactly what I wanted. My website was up and running. For the sake of
getting something working first, I added the necessary configuration for Caddy
(the container and the provisioning of its configuration file), redeployed
&lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; again, only to realize I also needed to create a network so that
the two containers could talk together. After half an hour or so, I got
everything working, but was left with a sour taste in my mouth.&lt;/p&gt;
&lt;p&gt;This would simply not do. I wasn’t defining anything, I was writing a shell
script in the most cumbersome way possible.&lt;/p&gt;
&lt;p&gt;Then, I remembered my initial train of thought and started to search for a way
to have Docker Compose work on CoreOS&lt;label for=&quot;fn8&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn8&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Well, Podman Compose, I guess. &lt;/span&gt;
&lt;/span&gt;. That is when I discovered
Quadlet, whose &lt;a href=&quot;https://github.com/containers/quadlet&quot; class=&quot;hover-mint&quot; marked=&quot;&quot;&gt;initial repository does a good job justifying its
existence&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;&lt;label for=&quot;fn9&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn9&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;This repository is now archived, since Quadlet has got merged
upstream. &lt;/span&gt;
&lt;/span&gt;. In particular,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With quadlet, you describe how to run a container in a format that is very
similar to regular systemd config files. From these actual systemd
configurations are automatically generated (using &lt;a href=&quot;https://github.com/containers/quadlet&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;systemd generators&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To give a concrete example, here is the &lt;code class=&quot;hljs&quot;&gt;.container&lt;/code&gt; file I wrote for my
website server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ini&quot;&gt;&lt;span class=&quot;hljs-section&quot;&gt;[Container]&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;ContainerName&lt;/span&gt;=soap.c&lt;span class=&quot;hljs-literal&quot;&gt;off&lt;/span&gt;ee
&lt;span class=&quot;hljs-attr&quot;&gt;Image&lt;/span&gt;=ams.vultrcr.com/lthms/www/soap.c&lt;span class=&quot;hljs-literal&quot;&gt;off&lt;/span&gt;ee:live

&lt;span class=&quot;hljs-section&quot;&gt;[Service]&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;Restart&lt;/span&gt;=always

&lt;span class=&quot;hljs-section&quot;&gt;[Install]&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;WantedBy&lt;/span&gt;=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wasn’t wasting my time teaching systemd how to start containers anymore. I
was now declaring what should exist, so that systemd—repurposed for the
occasion as a container orchestrator—could take care of the rest.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-tip&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-light-bulb mr-2&quot; viewbox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Tip&lt;/p&gt;&lt;p&gt;If your containers are basically ignored by systemd, be smarter than me. Do
not try to blindly change your &lt;code class=&quot;hljs&quot;&gt;.container&lt;/code&gt; files and redeploy your VM in a
very painful and frustrating loop. Simply ask systemd for the generator logs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs&quot;&gt;sudo journalctl -b | grep -i quadlet 
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;I excitedly turned &lt;code class=&quot;hljs&quot;&gt;caddy.service&lt;/code&gt; into &lt;code class=&quot;hljs&quot;&gt;caddy.container&lt;/code&gt;, redeployed
&lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt;, ran into the exact same issue I had encountered before and
discovered the easiest way for two Quadlet-defined containers to talk to each
other was to introduce a &lt;a href=&quot;https://docs.podman.io/en/latest/_static/api.html?version=latest#tag/pods&quot; class=&quot;hover-lemon&quot; marked=&quot;&quot;&gt;&lt;em&gt;pod&lt;/em&gt;&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. Unlike Docker Compose which uses DNS
over a bridge network, a pod shares the network namespace, allowing containers
to communicate over &lt;em&gt;localhost&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;To define a pod, one needs to create a &lt;code class=&quot;hljs&quot;&gt;.pod&lt;/code&gt; file, and to reference it in
their &lt;code class=&quot;hljs&quot;&gt;.container&lt;/code&gt; files using the &lt;code class=&quot;hljs&quot;&gt;PodName=&lt;/code&gt; configuration option. A “few”
redeployments later, I got everything working again, and I was ready to call it
a day.&lt;/p&gt;
&lt;p&gt;And with that, &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; was basically ready.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-caution&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-stop mr-2&quot; viewbox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Caution&lt;/p&gt;&lt;p&gt;I’ve later learned that restarting a container that is part of a pod will
have the (to me, unexpected) side-effect to restart all the other containers
of that pod.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;And Low-Maintenance&lt;/h3&gt;
&lt;p&gt;Now, the end of the previous section might have given you pause.&lt;/p&gt;
&lt;p&gt;Even a static website like this one isn’t completely “stateless.” Not only does
Caddy require a configuration file to do anything meaningful, but it is also a
stateful application as it manages TLS certificates over time. Besides, I &lt;em&gt;do&lt;/em&gt;
publish technical write-ups from time to time&lt;label for=&quot;fn10&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn10&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Two in 2025, that’s true. But 2026 is only starting, you never know
what might come! &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Was I really at peace with having to destroy and redeploy &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; every
time I need to change anything on my website?&lt;/p&gt;
&lt;p&gt;On the one hand, &lt;em&gt;yes&lt;/em&gt;. I believe I could live with that. I modify my website
only a handful of times even in good months, I think my audience could survive
with a minute of downtime before being allowed to read my latest pieces. It may
be an unpopular opinion, but considering my actual use case, it &lt;em&gt;was&lt;/em&gt; good
enough. Even the fact that I do not store the TLS certificates obtained by
Caddy anywhere persistent should not be an issue. I mean, Let’s Encrypt has
fairly generous weekly issuance limits per domain &lt;label for=&quot;fn11&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn11&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;How do I know that? Well... I might have hit the limit while hacking my
way to a working setup. &lt;/span&gt;
&lt;/span&gt;. I should be fine.&lt;/p&gt;
&lt;p&gt;On the other hand, the setup was starting to grow on me, and I have &lt;em&gt;other&lt;/em&gt; use
cases in mind that could be a good fit for it. So I started researching again,
this time to understand how a deployment philosophy so focused on immutability
was managing what seemed to be conflicting requirements.&lt;/p&gt;
&lt;p&gt;I went down other rabbit holes, looking for answers. The discovery that stood
out the most to me—to the point where it became the hook of this article—was
Podman auto-updates.&lt;/p&gt;
&lt;p&gt;To deploy a new version of a containerized application, you pull the new image
and restart the container. When you commit to this pattern, why should you be
the one performing this action? Instead, your VM can regularly check registries
for new images, and &lt;em&gt;update the required containers when necessary&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In practice, Podman made this approach trivial to put in place. I just needed
to label my containers with &lt;code class=&quot;hljs&quot;&gt;io.containers.autoupdate&lt;/code&gt; set to &lt;code class=&quot;hljs&quot;&gt;registry&lt;/code&gt;,
enable the &lt;code class=&quot;hljs&quot;&gt;podman-auto-update&lt;/code&gt; timer&lt;label for=&quot;fn12&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn12&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;By default, the timer is triggered once a day, which felt
unnecessarily long, so I decided to make it hourly instead. &lt;/span&gt;
&lt;/span&gt;, and that was it. Now, every
time I update the tag &lt;code class=&quot;hljs&quot;&gt;www/soap.coffee:live&lt;/code&gt; to point to a newer version of my
image, my website is updated within the hour.&lt;/p&gt;
&lt;p&gt;And that is when the final piece clicked. At this point, publishing an image
becomes the only deployment step. I didn’t need SSH anymore.&lt;/p&gt;
&lt;h2&gt;The Road Ahead&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; has been running for a few days now, and I am quite pleased with
the system I have put in place. In retrospect, none of this is particularly
novel. It feels more like I am converging toward a set of practices the
industry has been gravitating toward for years.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;/~lthms/img/iac-meme.jpg&quot;&gt;&lt;figcaption&gt;&lt;p&gt;A man looking at the “CoreOS &amp;amp; Quadlets” butterfly and wondering whether he’s looking at Infrastructure as Code. I’m not entirely sure of the answer.&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The journey is far from being over, though. &lt;code class=&quot;hljs&quot;&gt;tinkerbell&lt;/code&gt; is up and running, and
it served you this HTML page just fine, but the moment I put SSH out of the
picture, it became a black box. Aside from some hardware metrics kindly
provided by the Vultr dashboard, I have no real visibility into what’s going on
inside. That is fine for now, but it is not a place I want to stay in forever.
I plan to spend a few more weekends building an observability stack&lt;label for=&quot;fn13&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn13&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Oh, and maybe I will move these TLS certificates in a block storage or
something. That could be a good idea. &lt;/span&gt;
&lt;/span&gt;.
That will come in handy when things go wrong—as they inevitably do. I would
rather have the means to understand failures than guess my way around them.&lt;/p&gt;
&lt;p&gt;Did I ever mention I am an enthusiastic Opentelemetry convert?&lt;/p&gt;
        
      </description>
    </item>
    
    
    
    <item>
      <title>Serving This Article from RAM for Fun and No Real Benefit</title>
      <link>https://soap.coffee/~lthms/posts/DreamWebsite.html</link>
      <guid>https://soap.coffee/~lthms/posts/DreamWebsite.html</guid>
      <pubDate>December 25, 2024</pubDate>
      <description>
        
        &lt;h1&gt;Serving This Article from RAM for Fun and No Real Benefit&lt;/h1&gt;&lt;div id=&quot;tags-list&quot;&gt;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/docker.html&quot; class=&quot;tag hover-lavender&quot; marked=&quot;&quot;&gt;docker&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/meta.html&quot; class=&quot;tag hover-sky&quot; marked=&quot;&quot;&gt;meta&lt;/a&gt; &lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#tag&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;/~lthms/tags/ocaml.html&quot; class=&quot;tag hover-peach&quot; marked=&quot;&quot;&gt;ocaml&lt;/a&gt; &lt;/div&gt;
&lt;p&gt;In 2022, Xe Iaso published a &lt;a href=&quot;https://xeiaso.net/talks/how-my-website-works/&quot; class=&quot;hover-periwinkle&quot; marked=&quot;&quot;&gt;transcript of their talk on how their website was
working at the time&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. In a nutshell, their approach consisted of a
server preprocessing the website from its source at startup, then serving its
contents from memory. If you have not already, I can only encourage you to read
the article or watch the talk, as the story they tell is very interesting. For
me personally, it sparked a question: what if, instead of preprocessing the
website at startup, one decided to embed the already preprocessed website
within the program of the HTTP server tasked to serve it?&lt;/p&gt;
&lt;p&gt;Fast-forward today, and this question has finally been answered. The webpage
you are currently reading has been served to you by an ad hoc HTTP server built
with &lt;a href=&quot;https://aantron.github.io/dream/&quot; class=&quot;hover-sky&quot; marked=&quot;&quot;&gt;Dream&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;, whose binary is the only file I need to push to my server to
deploy the latest version of my website. I have actually deployed it, and it’s
been serving the contents of this website for more than a week now.&lt;/p&gt;
&lt;p&gt;What did I learn from this fun, little experiment? Basically, that this
approach changes nothing, as far as &lt;a href=&quot;https://chromewebstore.google.com/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk?pli=1&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;Lighthouse&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; and my monitoring is
concerned. I couldn’t find any meaningful differences between a static website
served by Nginx, a piece of software with thousands and thousands of
engineering work behind it, and my little toy web server pieced together in an
hour or so. Still. It was fun, so why not write about it?&lt;/p&gt;
&lt;p&gt;This article is a kind of experience report. I’ll dive into what I have done to
turn my website into a single, static binary. Not only does it mean writing
some OCaml, which is always fun, but it also requires understanding a little
some key HTTP headers, as well as using Docker to build easily deployable
binaries. All in all, I hope it will be an interesting read for the curious
minds.&lt;/p&gt;
&lt;h2&gt;Embedding My Website in a Binary&lt;/h2&gt;
&lt;p&gt;Not much had changed much since &lt;a href=&quot;August2022.html&quot; class=&quot;hover-rose&quot; marked=&quot;&quot;&gt;I stopped using &lt;strong&gt;&lt;code class=&quot;hljs&quot;&gt;cleopatra&lt;/code&gt;&lt;/strong&gt; to generate
this website&lt;/a&gt;, and &lt;a href=&quot;Thanks2023.html&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;the article I published in 2023 still
stands&lt;/a&gt;. In a nutshell, I work in the &lt;code class=&quot;hljs&quot;&gt;site/&lt;/code&gt; directory, and &lt;a href=&quot;https://soupault.app&quot; class=&quot;hover-lemon&quot; marked=&quot;&quot;&gt;soupault&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;
generates my website in the &lt;code class=&quot;hljs&quot;&gt;out/~lthms&lt;/code&gt; directory, thanks to a collection of
built-in and ad hoc plugins&lt;label for=&quot;fn1&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn1&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;For instance, Markdown footnotes are turned into side notes with
a soupault plugin. &lt;/span&gt;
&lt;/span&gt;. To deploy the website, I was relying
on &lt;code class=&quot;hljs&quot;&gt;rsync&lt;/code&gt; to sync the contents of the &lt;code class=&quot;hljs&quot;&gt;out/~lthms&lt;/code&gt; directory with the
directory statically served by a Nginx instance on my personal server.&lt;/p&gt;
&lt;p&gt;The first step of my little toy project is to actually embed the output of
soupault into an OCaml program.&lt;/p&gt;
&lt;p&gt;That’s where &lt;a href=&quot;https://github.com/mirage/ocaml-crunch&quot; class=&quot;hover-coral&quot; marked=&quot;&quot;&gt;ocaml-crunch&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; comes in handy. It is a &lt;a href=&quot;https://mirage.io/&quot; class=&quot;hover-sky&quot; marked=&quot;&quot;&gt;MirageOS&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; project, whose
only job is to generate an OCaml module from a file system directory. It is
straightforward to use it from Dune.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-lisp&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;; file: out/dune&lt;/span&gt;
(&lt;span class=&quot;hljs-name&quot;&gt;rule&lt;/span&gt;
 (&lt;span class=&quot;hljs-name&quot;&gt;target&lt;/span&gt; website_content.ml)
 (&lt;span class=&quot;hljs-name&quot;&gt;deps&lt;/span&gt; (&lt;span class=&quot;hljs-name&quot;&gt;source_tree&lt;/span&gt; ~lthms))
 (&lt;span class=&quot;hljs-name&quot;&gt;action&lt;/span&gt;
  (&lt;span class=&quot;hljs-name&quot;&gt;run&lt;/span&gt; ocaml-crunch -m plain -o %{target} -s ~lthms)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This snippet generates the &lt;code class=&quot;hljs&quot;&gt;website_content.ml&lt;/code&gt; module, which we can then
expose through a library with the &lt;code class=&quot;hljs&quot;&gt;library&lt;/code&gt; stanza.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-lisp&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;; file: out/dune&lt;/span&gt;
(&lt;span class=&quot;hljs-name&quot;&gt;library&lt;/span&gt;
 (&lt;span class=&quot;hljs-name&quot;&gt;name&lt;/span&gt; website_content))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we are basically done. Excluding an &lt;code class=&quot;hljs&quot;&gt;Internal&lt;/code&gt; module, the signature of
&lt;code class=&quot;hljs&quot;&gt;Website_content&lt;/code&gt; is pretty straightforward.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;val&lt;/span&gt; file_list : &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;list&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;val&lt;/span&gt; read : &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; -&amp;gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; option
&lt;span class=&quot;hljs-keyword&quot;&gt;val&lt;/span&gt; hash : &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; -&amp;gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; option
&lt;span class=&quot;hljs-keyword&quot;&gt;val&lt;/span&gt; size : &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; -&amp;gt; &lt;span class=&quot;hljs-built_in&quot;&gt;int&lt;/span&gt; option
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Serving the content with Dream&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://aantron.github.io/dream/&quot; class=&quot;hover-lemon&quot; marked=&quot;&quot;&gt;Dream&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; is a cool project, and provides a straightforward API that we can
leverage to turn our list of in-memory files into an HTTP server.&lt;/p&gt;
&lt;h3&gt;Naive Approach&lt;/h3&gt;
&lt;p&gt;Our goal now is to create a &lt;code class=&quot;hljs&quot;&gt;Dream.handler&lt;/code&gt; for each item in
&lt;code class=&quot;hljs language-ocaml&quot;&gt;file_list&lt;/code&gt;. Done naively (as was my first attempt), it gives you
something of the form:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; make_handler ~content path =
  &lt;span class=&quot;hljs-type&quot;&gt;Dream&lt;/span&gt;.get path (&lt;span class=&quot;hljs-keyword&quot;&gt;fun&lt;/span&gt; req -&amp;gt;
    &lt;span class=&quot;hljs-type&quot;&gt;Lwt&lt;/span&gt;.return (&lt;span class=&quot;hljs-type&quot;&gt;Dream&lt;/span&gt;.response content)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which we can use to build the main route we will then pass to &lt;code class=&quot;hljs&quot;&gt;Dream.router&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; website_route =
  &lt;span class=&quot;hljs-type&quot;&gt;Dream&lt;/span&gt;.scope &lt;span class=&quot;hljs-string&quot;&gt;&quot;~lthms&quot;&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;[]&lt;/span&gt;
  @@ &lt;span class=&quot;hljs-type&quot;&gt;List&lt;/span&gt;.map
       (&lt;span class=&quot;hljs-keyword&quot;&gt;fun&lt;/span&gt; path -&amp;gt;
         &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; content = &lt;span class=&quot;hljs-type&quot;&gt;Option&lt;/span&gt;.get (&lt;span class=&quot;hljs-type&quot;&gt;Website_content&lt;/span&gt;.read path) &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt;
         make_handler ~content path)
       &lt;span class=&quot;hljs-type&quot;&gt;Website_content&lt;/span&gt;.file_list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this approach, we build our handlers once, and then the lookup is done by
Dream’s router. It could be an interesting experiment to see if doing the
lookup ourselves is more performant (since Dream’s router is very generic,
while in our case we don’t really need to parse anything). I remember Xe
routing is basically going through a linked list, which seems strange at first,
but works very well in practice because they have ordered said list with the
most recent articles up front, and everybody comes to their website to read the
latest article anyway.&lt;/p&gt;
&lt;p&gt;It does not take an extensive QA process to figure out that this approach
is far from being enough. To name a few things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My website assumes &lt;code class=&quot;hljs&quot;&gt;http://path/index.html&lt;/code&gt; can be accessed with
&lt;code class=&quot;hljs&quot;&gt;http://path/&lt;/code&gt; or &lt;code class=&quot;hljs&quot;&gt;http://path&lt;/code&gt;. Our little snippet does not handle this.&lt;/li&gt;
&lt;li&gt;Browsers expect the &lt;code class=&quot;hljs&quot;&gt;Content-Type&lt;/code&gt; headers to be correctly set. To give an
example, they won&apos;t load a CSS file if the &lt;code class=&quot;hljs&quot;&gt;Content-Type&lt;/code&gt; header is not set
to &lt;code class=&quot;hljs&quot;&gt;text/css&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Browsers work best for websites that take the time to provide caching
directives. Our little snippet does not care to do so.&lt;/li&gt;
&lt;li&gt;Even if my website is rather lightweight&lt;label for=&quot;fn2&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn2&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;20MBytes at the time of writing the first version of this article. &lt;/span&gt;
&lt;/span&gt;, compressing the response
of our HTTP server for clients that support it is always a good idea.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a gentle reminder of all the things Nginx can do for you with very
little configuration.&lt;/p&gt;
&lt;h3&gt;Handling &lt;code class=&quot;hljs&quot;&gt;index.html&lt;/code&gt; Synonyms&lt;/h3&gt;
&lt;p&gt;This one is rather simple. For files named &lt;code class=&quot;hljs&quot;&gt;index.html&lt;/code&gt;, we need 3 handers, not
just one. We can achieve this with an additional helper
&lt;code class=&quot;hljs&quot;&gt;make_handler_remove_suffix&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; make_handler_remove_suffix ~content path suffix
    =
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.ends_with ~suffix path &lt;span class=&quot;hljs-keyword&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; alt_path =
      &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.sub path &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt; (&lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.length path - &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.length suffix)
    &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt;
    [ make_handler ~content alt_path ]
  &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Updating the &lt;code class=&quot;hljs&quot;&gt;website_route&lt;/code&gt; definition to use &lt;code class=&quot;hljs&quot;&gt;make_handler_remove_suffix&lt;/code&gt; is
quite easy as well.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-patch&quot;&gt; let website_route =
   Dream.scope &quot;~lthms&quot; []
&lt;span class=&quot;hljs-deletion&quot;&gt;-  @@ List.map&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+  @@ List.concat_map&lt;/span&gt;
        (fun path -&amp;gt;
          let content = Option.get (Website_content.read path) in
&lt;span class=&quot;hljs-deletion&quot;&gt;-         make_handler ~content path)&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+         if path = &quot;index.html&quot; then&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+           (* Special case to deal with &quot;index.html&quot; which needs to be&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+              recognized by the route &quot;/&quot; *)&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+           [&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+             make_handler ~content &quot;/&quot;;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+             make_handler ~content &quot;&quot;;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+             make_handler ~content &quot;index.html&quot;;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+           ]&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+         else&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+           make_handler_remove_suffix ~content path&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+             &quot;/index.html&quot;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+           @ make_handler_remove_suffix ~content&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+               path &quot;index.html&quot;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+           @ [ make_handler ~content path ])&lt;/span&gt;
        Website_content.file_list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that, &lt;code class=&quot;hljs&quot;&gt;https://soap.coffee/~lthms/posts/index.html&lt;/code&gt; returns the same pages
as &lt;code class=&quot;hljs&quot;&gt;https://soap.coffee/~lthms/posts&lt;/code&gt; or &lt;code class=&quot;hljs&quot;&gt;https://soap.coffee/~lthms/posts&lt;/code&gt;.
Check.&lt;/p&gt;
&lt;h3&gt;Supporting &lt;code class=&quot;hljs&quot;&gt;Content-Type&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;hljs&quot;&gt;Content-Type&lt;/code&gt; is an HTTP header which is used by the receiver of the HTTP
message (whether it is a request or a response) to interpret its content.&lt;/p&gt;
&lt;p&gt;For instance, when building a RPC API, &lt;code class=&quot;hljs&quot;&gt;Content-Type&lt;/code&gt; is used by the server to
know how to parse the request body (&lt;code class=&quot;hljs language-http&quot;&gt;&lt;span class=&quot;hljs-attribute&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;: &lt;/span&gt;application/json&lt;/code&gt; or
&lt;code class=&quot;hljs language-http&quot;&gt;&lt;span class=&quot;hljs-attribute&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;: &lt;/span&gt;application/octet-stream&lt;/code&gt; being two popular choices, for
JSON or binary encoding, respectively).&lt;/p&gt;
&lt;p&gt;In our case, the &lt;code class=&quot;hljs&quot;&gt;Content-Type&lt;/code&gt; header is used by the HTTP server to
communicate the nature of the content to browsers. For my website, I can just
use the file extensions to infer the correct header to set. First, we list the
extensions that are actually used.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; content_types =
  [
    (&lt;span class=&quot;hljs-string&quot;&gt;&quot;.html&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;text/html&quot;&lt;/span&gt;);
    (&lt;span class=&quot;hljs-string&quot;&gt;&quot;.css&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;text/css&quot;&lt;/span&gt;);
    (&lt;span class=&quot;hljs-string&quot;&gt;&quot;.xml&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;text/xml&quot;&lt;/span&gt;);
    (&lt;span class=&quot;hljs-string&quot;&gt;&quot;.png&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;image/png&quot;&lt;/span&gt;);
    (&lt;span class=&quot;hljs-string&quot;&gt;&quot;.svg&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;image/svg+xml&quot;&lt;/span&gt;);
    (&lt;span class=&quot;hljs-string&quot;&gt;&quot;.gz&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;application/gzip&quot;&lt;/span&gt;);
    (&lt;span class=&quot;hljs-string&quot;&gt;&quot;.pub&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;text/plain&quot;&lt;/span&gt;);
  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A header in Dream is encoded as a &lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; * &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;&lt;/code&gt; value, with the
first &lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;&lt;/code&gt; being the header name and the second being the header
value.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; content_type_header path =
  &lt;span class=&quot;hljs-type&quot;&gt;List&lt;/span&gt;.filter_map
    (&lt;span class=&quot;hljs-keyword&quot;&gt;fun&lt;/span&gt; (ext, content_type) -&amp;gt;
      &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.ends_with ~suffix:ext path &lt;span class=&quot;hljs-keyword&quot;&gt;then&lt;/span&gt;
        &lt;span class=&quot;hljs-type&quot;&gt;Some&lt;/span&gt; (&lt;span class=&quot;hljs-string&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;, content_type)
      &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;None&lt;/span&gt;)
    content_types
  |&amp;gt; assert_f
       ~error_msg:&lt;span class=&quot;hljs-type&quot;&gt;Format&lt;/span&gt;.(sprintf &lt;span class=&quot;hljs-string&quot;&gt;&quot;Unsupported file type %s&quot;&lt;/span&gt; path)
       (( &amp;lt;&amp;gt; ) &lt;span class=&quot;hljs-literal&quot;&gt;[]&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with &lt;code class=&quot;hljs language-ocaml&quot;&gt;assert_f&lt;/code&gt; being defined as follows.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; assert_f ~error_msg f v =
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; f v &lt;span class=&quot;hljs-keyword&quot;&gt;then&lt;/span&gt; v &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; failwith error_msg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;assert_f&lt;/code&gt; is used to enforce that I don’t deploy a website which
contains route lacking a &lt;code class=&quot;hljs&quot;&gt;Content-Type&lt;/code&gt; header. For instance, if I remove the
&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-string&quot;&gt;&quot;html&quot;&lt;/span&gt;&lt;/code&gt; entry of the &lt;code class=&quot;hljs language-ocaml&quot;&gt;content_type&lt;/code&gt; list, I get this error
when I try to execute the server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs&quot;&gt;Fatal error: exception Failure(&quot;Unsupported file type index.html&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is because the headers are only computed once, when each &lt;code class=&quot;hljs&quot;&gt;route&lt;/code&gt; are
defined. This is a key principle of this project: compute once, serve many
time&lt;label for=&quot;fn3&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn3&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;I would love to get a compilation error instead (considering there
are no runtime values involved), but have not looked into this just yet. &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-patch&quot;&gt; let website_route =
   Dream.scope &quot;~lthms&quot; []
   @@ List.concat_map
        (fun path -&amp;gt;
          let content = Option.get (Website_content.read path) in
&lt;span class=&quot;hljs-addition&quot;&gt;+         let headers = content_type_header path in&lt;/span&gt;
          if path = &quot;index.html&quot; then
            (* Special case to deal with &quot;index.html&quot; which needs to be
               recognized by the route &quot;/&quot; *)
            [
&lt;span class=&quot;hljs-deletion&quot;&gt;-             make_handler ~content &quot;/&quot;;&lt;/span&gt;
&lt;span class=&quot;hljs-deletion&quot;&gt;-             make_handler ~content &quot;&quot;;&lt;/span&gt;
&lt;span class=&quot;hljs-deletion&quot;&gt;-             make_handler ~content &quot;index.html&quot;;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+             make_handler ~headers ~content &quot;/&quot;;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+             make_handler ~headers ~content &quot;&quot;;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+             make_handler ~headers ~content &quot;index.html&quot;;&lt;/span&gt;
            ]
          else
&lt;span class=&quot;hljs-deletion&quot;&gt;-           make_handler_remove_suffix ~content path&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+           make_handler_remove_suffix ~headers ~content path&lt;/span&gt;
              &quot;/index.html&quot;
&lt;span class=&quot;hljs-deletion&quot;&gt;-           @ make_handler_remove_suffix ~content&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+           @ make_handler_remove_suffix ~headers ~content&lt;/span&gt;
                path &quot;index.html&quot;
&lt;span class=&quot;hljs-deletion&quot;&gt;-           @ [ make_handler ~content path ])&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+           @ [ make_handler ~headers ~content path ])&lt;/span&gt;
        Website_content.file_list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(The changes in &lt;code class=&quot;hljs&quot;&gt;make_handler&lt;/code&gt; and &lt;code class=&quot;hljs&quot;&gt;make_handler_remove_prefix&lt;/code&gt; are left as an
exercise to enthusiast readers)&lt;/p&gt;
&lt;h3&gt;Compressing if Requested&lt;/h3&gt;
&lt;p&gt;Nowadays, computations are cheap, while downloading data costs time (and
sometimes money). As a consequence, it is often a good idea for a server to
compress a large HTTP response, and browsers do ask them to do so, by setting
the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding&quot; class=&quot;hover-periwinkle&quot; marked=&quot;&quot;&gt;&lt;code class=&quot;hljs language-http&quot;&gt;Accept-Encoding&lt;/code&gt;&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; header of their requests.&lt;/p&gt;
&lt;p&gt;The value of the &lt;code class=&quot;hljs language-http&quot;&gt;Accept-Encoding&lt;/code&gt; header is a comma-separated list of
supported compression algorithms, optionally ordered with a priority value &lt;code class=&quot;hljs&quot;&gt;q&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For instance, &lt;code class=&quot;hljs language-http&quot;&gt;&lt;span class=&quot;hljs-attribute&quot;&gt;Accept-Encoding&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;: &lt;/span&gt;gzip;q=0.5, deflate;q=0.3, identity&lt;/code&gt;
tells you that the browser supports three encoding methods: &lt;code class=&quot;hljs&quot;&gt;gzip&lt;/code&gt;, &lt;code class=&quot;hljs&quot;&gt;deflate&lt;/code&gt;
and &lt;code class=&quot;hljs&quot;&gt;identity&lt;/code&gt; (no compression), and the browser prefers &lt;code class=&quot;hljs&quot;&gt;gzip&lt;/code&gt; over &lt;code class=&quot;hljs&quot;&gt;deflate&lt;/code&gt;.
Besides, the request can provide several &lt;code class=&quot;hljs&quot;&gt;Accept-Encoding&lt;/code&gt; headers instead of
just one, so we can have&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-http&quot;&gt;&lt;span class=&quot;hljs-attribute&quot;&gt;Accept-Encoding&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;: &lt;/span&gt;gzip;q=0.5
&lt;span class=&quot;hljs-attribute&quot;&gt;Accept-Encoding&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;: &lt;/span&gt;deflate;q=0.3
&lt;span class=&quot;hljs-attribute&quot;&gt;Accept-Encoding&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;: &lt;/span&gt;identity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code class=&quot;hljs&quot;&gt;String&lt;/code&gt; module provides everything we need to check if a browser
supports gzip as an encoding method&lt;label for=&quot;fn4&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn4&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Spoiler: they do. I was even wondering at some point if I could
just &lt;em&gt;always&lt;/em&gt; return GZIP-compressed values, ignoring the
&lt;code class=&quot;hljs language-http&quot;&gt;Accept-Encoding&lt;/code&gt; header altogether. If you do that, though, &lt;code class=&quot;hljs&quot;&gt;curl&lt;/code&gt;
becomes annoying to use (it does not uncompress the response automatically,
and instead complains about being about to write binary to the standard
output). &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;(* For [method(; q=val)?], returns [method], except if
   [q=0]. *)&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; to_directive str =
  &lt;span class=&quot;hljs-keyword&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.split_on_char &lt;span class=&quot;hljs-string&quot;&gt;&apos;;&apos;&lt;/span&gt; str |&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;List&lt;/span&gt;.map &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.trim &lt;span class=&quot;hljs-keyword&quot;&gt;with&lt;/span&gt;
  | [ x ] -&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;Some&lt;/span&gt; x
  | [ x; y ] -&amp;gt; (
      &lt;span class=&quot;hljs-keyword&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.split_on_char &lt;span class=&quot;hljs-string&quot;&gt;&apos;=&apos;&lt;/span&gt; y |&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;List&lt;/span&gt;.map &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.trim &lt;span class=&quot;hljs-keyword&quot;&gt;with&lt;/span&gt;
      | [ &lt;span class=&quot;hljs-string&quot;&gt;&quot;q&quot;&lt;/span&gt;; &lt;span class=&quot;hljs-string&quot;&gt;&quot;0&quot;&lt;/span&gt; ] -&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;None&lt;/span&gt;
      | [ &lt;span class=&quot;hljs-string&quot;&gt;&quot;q&quot;&lt;/span&gt;; _ ] -&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;Some&lt;/span&gt; x
      | _ -&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;None&lt;/span&gt;)
  | _ -&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;None&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;(* [contains ~value:v header] returns [true] if [v] is a
   supported method listed in [header]. *)&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; contains ~&lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt; header =
  &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.split_on_char &lt;span class=&quot;hljs-string&quot;&gt;&apos;,&apos;&lt;/span&gt; header
  |&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;List&lt;/span&gt;.to_seq |&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;Seq&lt;/span&gt;.map &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.trim
  |&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;Seq&lt;/span&gt;.filter_map to_directive
  |&amp;gt; &lt;span class=&quot;hljs-type&quot;&gt;Seq&lt;/span&gt;.exists (( = ) &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We use &lt;code class=&quot;hljs language-ocaml&quot;&gt;contains&lt;/code&gt; to tell us if we can return a compressed response,
which leaves us with one final question: how to compress said response?&lt;/p&gt;
&lt;p&gt;The OCaml ecosystem seems to have picked &lt;a href=&quot;https://ocaml.org/p/camlzip/latest&quot; class=&quot;hover-sky&quot; marked=&quot;&quot;&gt;camlzip&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; library when GZIP is
involved&lt;label for=&quot;fn5&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn5&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;You know it is a legitimate OCaml library when one of the top-level
modules &lt;a href=&quot;https://ocaml.org/p/camlzip/latest/doc/Zlib/&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;is not documented at all&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. &lt;/span&gt;
&lt;/span&gt;. What is surprising with this library is that it does not
support in-memory compression: the functions expect channels, not
&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;bytes&lt;/span&gt;&lt;/code&gt;. That is quite annoying, because we are specifically doing this
&lt;strong&gt;not&lt;/strong&gt; to use files.&lt;/p&gt;
&lt;p&gt;The Internet is helpful here, and quickly suggests using pipes. It works when
you remember –or figure out– that pipes are a blocking mechanism: one does not
just write a buffer of arbitrary size in a pipe, because after something like
4KBytes, writing becomes blocking until a read happens to free some space.
That’s not a big problem: we can read and write concurrently to the pipe using
threads, and OCaml 5 makes it quite easy to do so with the &lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-type&quot;&gt;Domain&lt;/span&gt;&lt;/code&gt;
module.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; gzip content =
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; inc, ouc = &lt;span class=&quot;hljs-type&quot;&gt;Unix&lt;/span&gt;.pipe &lt;span class=&quot;hljs-literal&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; ouc = &lt;span class=&quot;hljs-type&quot;&gt;Gzip&lt;/span&gt;.open_out_chan ~level:&lt;span class=&quot;hljs-number&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;Unix&lt;/span&gt;.(out_channel_of_descr ouc) &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; _writer =
    &lt;span class=&quot;hljs-type&quot;&gt;Domain&lt;/span&gt;.spawn (&lt;span class=&quot;hljs-keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;()&lt;/span&gt; -&amp;gt;
        &lt;span class=&quot;hljs-type&quot;&gt;Gzip&lt;/span&gt;.output_substring ouc content &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;String&lt;/span&gt;.(length content);
        &lt;span class=&quot;hljs-type&quot;&gt;Gzip&lt;/span&gt;.close_out ouc)
  &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; res = &lt;span class=&quot;hljs-type&quot;&gt;In_channel&lt;/span&gt;.input_all &lt;span class=&quot;hljs-type&quot;&gt;Unix&lt;/span&gt;.(in_channel_of_descr inc) &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt;
  &lt;span class=&quot;hljs-type&quot;&gt;Unix&lt;/span&gt;.close inc;
  res
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;markdown-alert markdown-alert-caution&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-stop mr-2&quot; viewbox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Caution&lt;/p&gt;&lt;p&gt;As rightfully &lt;a href=&quot;https://discuss.ocaml.org/t/serving-this-article-from-ram-with-dream-for-fun-and-no-real-benefit/15856/6?u=lthms&quot; class=&quot;hover-coral&quot; marked=&quot;&quot;&gt;pointed out&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; by &lt;a href=&quot;https://erratique.ch/contact.en&quot; class=&quot;hover-peach&quot; marked=&quot;&quot;&gt;Daniel Bünzli&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;, the &lt;code class=&quot;hljs&quot;&gt;gzip&lt;/code&gt;
function presented in this article is full of shortcomings. To quote the
message, &lt;em&gt;the function can leak fds in case of errors and domains are not
meant to be used that way (it’s rather spawn one long running domain per CPU
you have). It’s not necessarily more complicated to correct it to use
Fun.protect invocations to make sure all your fds get closed even if the
function blows up and use Thread.create so that the netizens cut and paste
correct code.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In my opinion, this implementation is “good enough” for my use case, which is
compressing arbitrary strings before the HTTP server is even started. If it
were to be called in the handlers themselves, then definitely, it would not
be suitable.&lt;/p&gt;
&lt;p&gt;Keep that in mind if you want to borrow this code.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;We know how to decide whether to compress or not, and how to compress. The next
step is to modify &lt;code class=&quot;hljs&quot;&gt;make_handler&lt;/code&gt; accordingly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-patch&quot;&gt;&lt;span class=&quot;hljs-deletion&quot;&gt;-let make_handler ~headers ~content path =&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+let make_handler ~headers ~gzip_content ~content path =&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+  let gzip_headers = (&quot;Content-Encoding&quot;, &quot;gzip&quot;) :: headers in&lt;/span&gt;
   Dream.get path (fun req -&amp;gt;
&lt;span class=&quot;hljs-deletion&quot;&gt;-    Lwt.return (Dream.response content)))&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+      match Dream.headers req &quot;Accept-Encoding&quot; with&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+      | accepted_encodings&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+        when List.exists (contains ~value:&quot;gzip&quot;) accepted_encodings -&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+          Lwt.return @@ Dream.response ~headers:gzip_headers gzip_content&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+      | _ -&amp;gt; Lwt.return @@ Dream.response ~headers content)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code class=&quot;hljs&quot;&gt;gzip_content&lt;/code&gt; is computed only once (using our &lt;code class=&quot;hljs&quot;&gt;gzip&lt;/code&gt; function), and passed to
&lt;code class=&quot;hljs&quot;&gt;make_handler&lt;/code&gt;. This way, the only computation the handler needs to do is to
“parse” &lt;code class=&quot;hljs&quot;&gt;Accept-Encoding&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Caching&lt;/h3&gt;
&lt;p&gt;Compressing a page to reduce the number of bytes a browser needs to download is
fine. Letting the browser know it does not need to download anything because
it&apos;s previous version is still accurate is better.&lt;/p&gt;
&lt;p&gt;This is achieved through two complementary mechanisms: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;entity tags&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;
(&lt;code class=&quot;hljs language-http&quot;&gt;ETag&lt;/code&gt;)&lt;label for=&quot;fn6&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn6&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;My first encounter with entity tags was around the time GDPR was a
hot topic, because you can use them as a cheap replacement for cookies to
&lt;a href=&quot;https://levelup.gitconnected.com/no-cookies-no-problem-using-etags-for-user-tracking-3e745544176b&quot; class=&quot;hover-rose&quot; marked=&quot;&quot;&gt;track your users&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. I remained at the surface level at the time,
it was fun learning more about them through this little project. &lt;/span&gt;
&lt;/span&gt;, and &lt;a href=&quot;https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Cache-Control&quot; class=&quot;hover-lavender&quot; marked=&quot;&quot;&gt;cache policies&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; (&lt;code class=&quot;hljs language-http&quot;&gt;Cache-Control&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Entity tags are used to identify a resource, and are expected to change
every time the resource is updated. The general workflow goes like this: the
first time a browser requests &lt;code class=&quot;hljs&quot;&gt;https://soap.coffee/~lthms/index.html&lt;/code&gt;, it
caches the result along with the value of the &lt;code class=&quot;hljs language-http&quot;&gt;ETag&lt;/code&gt; header. The next
time it needs the page, it adds the header &lt;code class=&quot;hljs language-http&quot;&gt;If-None-Match&lt;/code&gt;, with the
ETag as its value. For such requests, the server is expected to return an empty
response with HTTP code 304 (&lt;em&gt;Not Modified&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;I decided to use the sha256 hash algorithm to compute the entity tag of each
resource of my website. The &lt;a href=&quot;https://ocaml.org/p/sha/latest&quot; class=&quot;hover-lemon&quot; marked=&quot;&quot;&gt;sha&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt; OCaml library looked like a good enough
candidate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ocaml&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;(* in `website_route` *)&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; etag = &lt;span class=&quot;hljs-type&quot;&gt;Sha256&lt;/span&gt;.(&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; content |&amp;gt; to_hex) &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Interestingly, one question I had to answer was whether the entity tag of a
page needed to be different whether it was compressed or not. Internet almost
unanimously answered yes. So be it. We just need to keep in mind ETag values
are expected to be surrounded by quotes, and we are good to go. It is just a
matter of suffixing the ETag with &lt;code class=&quot;hljs&quot;&gt;+gzip&lt;/code&gt; in the compressed case.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-patch&quot;&gt;&lt;span class=&quot;hljs-deletion&quot;&gt;-let make_handler ~headers ~gzip_content ~content path =&lt;/span&gt;
&lt;span class=&quot;hljs-deletion&quot;&gt;-  let gzip_headers = (&quot;Content-Encoding&quot;, &quot;gzip&quot;) :: headers in&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+let make_handler ~headers ~etag ~gzip_content ~content path =&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+  let etag_gzip = Format.sprintf &quot;\&quot;%s+gzip\&quot;&quot; etag in&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+  let etag = Format.sprintf &quot;\&quot;%s\&quot;&quot; etag in&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+  let gzip_headers =&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+    (&quot;Content-Encoding&quot;, &quot;gzip&quot;) :: (&quot;ETag&quot;, etag_gzip) :: headers in&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+  let identity_headers = (&quot;ETag&quot;, etag) :: headers in&lt;/span&gt;
   Dream.get path (fun req -&amp;gt;
&lt;span class=&quot;hljs-deletion&quot;&gt;-      match Dream.headers req &quot;Accept-Encoding&quot; with&lt;/span&gt;
&lt;span class=&quot;hljs-deletion&quot;&gt;-      | accepted_encodings&lt;/span&gt;
&lt;span class=&quot;hljs-deletion&quot;&gt;-        when List.exists (contains ~value:&quot;gzip&quot;) accepted_encodings -&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-deletion&quot;&gt;-          Lwt.return @@ Dream.response ~headers:gzip_headers gzip_content&lt;/span&gt;
&lt;span class=&quot;hljs-deletion&quot;&gt;-      | _ -&amp;gt; Lwt.return @@ Dream.response ~headers content)&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+      match Dream.headers req &quot;If-None-Match&quot; with&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+      | [ previous_etag ] when previous_etag = etag || previous_etag = etag_gzip&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+        -&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+          Lwt.return&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+          @@ Dream.response&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+               ~headers:((&quot;ETag&quot;, previous_etag) :: headers)&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+               ~code:304 &quot;&quot;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+      | _ -&amp;gt; (&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+          match Dream.headers req &quot;Accept-Encoding&quot; with&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+          | accepted_encodings&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+            when List.exists (contains ~value:&quot;gzip&quot;) accepted_encodings -&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+              Lwt.return @@ Dream.response ~headers:gzip_headers gzip_content&lt;/span&gt;
&lt;span class=&quot;hljs-addition&quot;&gt;+          | _ -&amp;gt; Lwt.return @@ Dream.response ~headers:identity_headers content))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Entity tags are useful, but you still need the browser to make an HTTP request
every single time you visit the website. By setting a cache policy, we can
remove even remove the need for this request most of the time. The
&lt;code class=&quot;hljs language-http&quot;&gt;Cache-Control&lt;/code&gt; header is used to set a number of parameters, including
the &lt;code class=&quot;hljs&quot;&gt;max-age&lt;/code&gt; value (in seconds).&lt;/p&gt;
&lt;p&gt;In my Nginx configuration, I had set &lt;code class=&quot;hljs&quot;&gt;max-age&lt;/code&gt; to a year for images. I did
the same thing here. Besides, I decided to set &lt;code class=&quot;hljs&quot;&gt;max-age&lt;/code&gt; for other resources
to 5 minutes. This seems like a good compromise: since my website does not
change very often, it is very unlikely that you happen to visit it when I
publish new content. Setting a 5-minute cache policy should let my readers
download each resource only once, yet get the freshest version at their next
visit&lt;label for=&quot;fn7&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn7&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-right sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Dealing with the &lt;code class=&quot;hljs language-http&quot;&gt;Cache-Control&lt;/code&gt; header is basically the same
exercise as setting the correct &lt;code class=&quot;hljs language-http&quot;&gt;Content-Type&lt;/code&gt; header, and this
article is already long enough, which is why there is no diff or snippet in
this section. &lt;/span&gt;
&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;And with this, we are done. We get a standalone library to server our website
in a browser-friendly manner, which I can theoretically use to replace my
current Nginx-powered setup. Although… is it &lt;em&gt;that&lt;/em&gt; simple?&lt;/p&gt;
&lt;h2&gt;Building and Deploying the Website&lt;/h2&gt;
&lt;p&gt;Files are quite easy to share and deploy. As I mentioned earlier in this
article, you just need to &lt;code class=&quot;hljs language-bash&quot;&gt;rsync&lt;/code&gt; them and be done with it. &lt;em&gt;Binaries&lt;/em&gt;,
on the other hand… One cannot just assume a binary build on a machine X will
work on another machine Y. Some additional works need to be done.&lt;/p&gt;
&lt;p&gt;The most straightforward solution I know is to rely on static binaries. &lt;a href=&quot;OCamlStaticBinaries.html&quot; class=&quot;hover-rose&quot; marked=&quot;&quot;&gt;I have
already written about how to generate static binaries for OCaml
projects&lt;/a&gt;, so I had a pretty strong head start, but one drawback of the
approach I’ve described there (and that I have been using for &lt;a href=&quot;https://github.com/lthms/spatial-shell/releases&quot; class=&quot;hover-lemon&quot; marked=&quot;&quot;&gt;Spatial Shell’s
releases&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#github&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;&lt;label for=&quot;fn8&quot; class=&quot;sidenote-number margin-toggle&quot;&gt;&lt;/label&gt;&lt;input id=&quot;fn8&quot; type=&quot;checkbox&quot; class=&quot;margin-toggle&quot;&gt;&lt;span class=&quot;note-left sidenote note&quot;&gt;&lt;span class=&quot;footnote-p&quot;&gt;Not that there were many of them lately. &lt;/span&gt;
&lt;/span&gt;) is that it is rather slow (I have a script creating
a new local switch each time) and requires static libraries to be installed
(which Arch Linux does not provide).&lt;/p&gt;
&lt;p&gt;And so, I figured, why not build the static binary in Docker? This allows me to
use Alpine to get a static version of my system dependencies, and can be quite
fast thanks to &lt;a href=&quot;https://docs.docker.com/build/cache/&quot; class=&quot;hover-coral&quot; marked=&quot;&quot;&gt;Docker build cache&amp;nbsp;&lt;span class=&quot;icon&quot;&gt;&lt;svg&gt;&lt;use href=&quot;/~lthms/img/icons.svg#external-link&quot;&gt;&lt;/use&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/a&gt;. The Dockerfile is quite simple:
one stage for building the system and OCaml dependencies, and one stage for
building the static binary.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-dockerfile&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; alpine:&lt;span class=&quot;hljs-number&quot;&gt;3.21&lt;/span&gt; AS build_environment

&lt;span class=&quot;hljs-comment&quot;&gt;# Use alpine /bin/ash and set shell options&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;# See https://docs.docker.com/build/building/best-practices/#using-pipes&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SHELL&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; [&lt;span class=&quot;hljs-string&quot;&gt;&quot;/bin/ash&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;-euo&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;pipefail&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;-c&quot;&lt;/span&gt;]&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;USER&lt;/span&gt; root
&lt;span class=&quot;hljs-keyword&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; /root&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;RUN&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; apk add autoconf automake bash build-base ca-certificates opam gcc \ &lt;/span&gt;
  git rsync gmp-dev libev-dev openssl-libs-static pkgconf zlib-static \
  openssl-dev zlib-dev
&lt;span class=&quot;hljs-keyword&quot;&gt;RUN&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; opam init --bare --&lt;span class=&quot;hljs-built_in&quot;&gt;yes&lt;/span&gt; --disable-sandboxing&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; makefile dune-project .&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;RUN&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; make _opam/.init OCAML=&lt;span class=&quot;hljs-string&quot;&gt;&quot;ocaml-option-static,ocaml-option-no-compression,ocaml.5.2.1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;RUN&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;eval&lt;/span&gt; $(opam &lt;span class=&quot;hljs-built_in&quot;&gt;env&lt;/span&gt;) &amp;amp;&amp;amp; make server-deps&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; build_environment AS builder

&lt;span class=&quot;hljs-keyword&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; server ./server&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; out ./out&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; dune .&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;RUN&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;eval&lt;/span&gt; $(opam &lt;span class=&quot;hljs-built_in&quot;&gt;env&lt;/span&gt;) &amp;amp;&amp;amp; dune build server/main.exe --profile=static&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; alpine:&lt;span class=&quot;hljs-number&quot;&gt;3.21&lt;/span&gt; AS soap.coffee

&lt;span class=&quot;hljs-keyword&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; --from=builder /root/_build/default/server/main.exe /bin/soap.coffee&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, building my static binary becomes as simple as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker build . -f ./build.Dockerfile \
  --target soap.coffee \
  -t soap.coffee:latest
docker create --name soap-coffee-build soap.coffee:latest
docker &lt;span class=&quot;hljs-built_in&quot;&gt;cp&lt;/span&gt; soap-coffee-build:/bin/soap.coffee .
docker &lt;span class=&quot;hljs-built_in&quot;&gt;rm&lt;/span&gt; -f soap-coffee-build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;cp&lt;/span&gt;&lt;/code&gt; does not work on an image, but on a container, so we need to
create one which can be destroyed shortly after.&lt;/p&gt;
&lt;p&gt;This little binary weights 38MBytes, which seems relatively reasonable to me,
considering my website weights 20MBytes. I guess it could be easy to reduce
this size by embedding the compressed version of my articles and images,
instead of the uncompressed one. But really, for my website, I’m not really
interested in investing the extra effort.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I would not recommend anyone to use this in production for anything remotely
important, but from my perspective, it was both fun and insightful. I was able
to refresh my memories about HTTP “internal,” among other things.&amp;nbsp;Again, as
far as I can tell, deploying my website this way did not bring me any benefit,
performance-wise; even worse, I am pretty sure the Dream server will not behave
as well as Nginx when it comes to handling the load (since it is limited to one
core, instead of several with Nginx).&lt;/p&gt;
&lt;p&gt;That being said, I am way too invested now… which is why, yes, you are
reading a blog post served to you directly from memory 🎉.&lt;/p&gt;
        
      </description>
    </item>
    
    
  </channel>
</rss>
