<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://benkenawell.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://benkenawell.com/" rel="alternate" type="text/html" /><updated>2026-03-04T12:31:20+00:00</updated><id>https://benkenawell.com/feed.xml</id><title type="html">Ben’s Blog</title><subtitle>Some blog posts and extraneous information about me, Ben Kenawell</subtitle><entry><title type="html">Tailnet Zero-Downtime Deployments</title><link href="https://benkenawell.com/2026/03/04/kamal-and-tailscale.html" rel="alternate" type="text/html" title="Tailnet Zero-Downtime Deployments" /><published>2026-03-04T00:00:00+00:00</published><updated>2026-03-04T00:00:00+00:00</updated><id>https://benkenawell.com/2026/03/04/kamal-and-tailscale</id><content type="html" xml:base="https://benkenawell.com/2026/03/04/kamal-and-tailscale.html"><![CDATA[<p><a href="https://tailscale.com/">Tailscale</a> is amazing for enabling private mesh network overlays. <!--more--> For connecting your phone to a server you having running at home, easily and securely.  If you’re building things you want to deploy on your tailnet, what’s the best way to do that?</p>

<p>I want everything to run on my own hardware, minimizing my costs and letting me use what I already have.  Tailscale lets me talk with my computers easily. <a href="https://kamal-deploy.org/">Kamal</a> lets me deploy from my local machine with ease and zero downtime (my wife will never know!).  Tailscale’s <a href="https://tailscale.com/docs/features/tailscale-serve">Serve</a> feature work great with Kamal Proxy’s accessory setup.</p>

<p>Kamal has one onerous requirement, it requires a container registry.  This registry has to be available from your laptop and server.  If you use an external service, like GitHub’s Artifact Repository or Digital Ocean’s container registry, then you should be good to go.  If you, like me, want to also run your own container registry on your tailnet, I wrote <a href="/2026/03/02/kamal-and-zot.html">another article</a> about that.</p>

<h2 id="how-it-works">How it works</h2>

<p>Using a ephermeral tailscale auth key, Tailscale will route traffic on our network to the accessory tailscale container that Kamal deploys for us.  We use docker’s dns routing to point Tailscale’s Serve feature to the kamal-proxy container that Kamal deploys.  Kamal’s proxy will check the hostname for us then route to the application container for our app.  Because we’re deploying behind kamal-proxy, Kamal will handle zero downtime deploys for us, no configuration needed!</p>

<h2 id="limitations">Limitations</h2>

<p>Kamal proxy routes via the hostname for you, so you can’t access these services via the IP address.</p>

<h2 id="deploy-more-services">Deploy more services</h2>

<p>Kamal Proxy can handle multiple apps deployed to the same server. We register a different tailscale sidecar for each one to make our deployments simpler and each service can have it’s own identifier in our Tailscale Admin Dashboard. Requests to both services will run through the same kamal proxy container, who will route it properly via hostname.</p>

<h2 id="appendix-a-configuration">Appendix A: Configuration</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># excerpts from my deploy.yml for Kamal</span>

<span class="c1"># my server is on tailscale as well</span>
<span class="na">servers</span><span class="pi">:</span>
  <span class="na">web</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">&lt;server name&gt;.&lt;tailnet name&gt;.ts.net</span>

<span class="c1"># To connect with Zot on my tailnet</span>
<span class="na">build</span><span class="pi">:</span>
  <span class="na">driver</span><span class="pi">:</span> <span class="s">docker</span>

<span class="c1"># tell the proxy my tailscale hostname</span>
<span class="na">proxy</span><span class="pi">:</span>
  <span class="c1"># tailscale terminates the ssl connection</span>
  <span class="na">ssl</span><span class="pi">:</span> <span class="kc">false</span>
  <span class="c1"># same hostname as the tailscale sidecar</span>
  <span class="na">host</span><span class="pi">:</span> <span class="s">&lt;service name&gt;.&lt;tailnet name&gt;.ts.net</span>
  <span class="c1"># rails port number</span>
  <span class="na">app_port</span><span class="pi">:</span> <span class="m">3000</span>
  <span class="c1"># rails takes a long time to boot on my little server...</span>
  <span class="na">healthcheck</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/up</span>
    <span class="na">interval</span><span class="pi">:</span> <span class="m">5</span>
    <span class="na">timeout</span><span class="pi">:</span> <span class="m">600</span>
  <span class="na">run</span><span class="pi">:</span>
    <span class="c1"># don't publish ports to the host machine,</span>
    <span class="c1"># all the traffic will come through the docker network</span>
    <span class="c1"># via the tailscale accessory</span>
    <span class="na">publish</span><span class="pi">:</span> <span class="kc">false</span>

<span class="na">accessories</span><span class="pi">:</span>
  <span class="na">sidecar</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/tailscale/tailscale:stable</span>
    <span class="c1"># for kamal, the host to run this sidecar on, same as servers.web configuration</span>
    <span class="na">host</span><span class="pi">:</span> <span class="s">&lt;server name&gt;.&lt;tailnet name&gt;.ts.net</span>
    <span class="na">files</span><span class="pi">:</span>
      <span class="c1"># I don't think directories works the way I way, so</span>
      <span class="c1"># copy and mount the file directly.</span>
      <span class="pi">-</span> <span class="s">config/tailscale/ts.json:/config/ts.json</span>
    <span class="na">env</span><span class="pi">:</span>
      <span class="na">clear</span><span class="pi">:</span>
        <span class="na">TS_USERSPACE</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">TS_STATE_DIR</span><span class="pi">:</span> <span class="s">/var/lib/tailscale</span>
        <span class="na">TS_SERVE_CONFIG</span><span class="pi">:</span> <span class="s">/config/ts.json</span>
        <span class="na">TS_EXTRA_ARGS</span><span class="pi">:</span> <span class="s">--advertise-tags=tag:container</span>
      <span class="na">secret</span><span class="pi">:</span>
        <span class="c1"># loaded with .kamal/secrets</span>
        <span class="pi">-</span> <span class="s">TS_AUTHKEY</span>
    <span class="na">options</span><span class="pi">:</span>
      <span class="c1"># must be the same as the proxy's service name so tailscale and kamal proxy agree</span>
      <span class="na">hostname</span><span class="pi">:</span> <span class="s">&lt;service name&gt;</span>
      <span class="na">cap-add</span><span class="pi">:</span> <span class="s">NET_ADMIN</span>
      <span class="na">device</span><span class="pi">:</span> <span class="s">/dev/net/tun:/dev/net/tun</span>
      <span class="c1"># derive the volume from the service name so we can have multiple tailscale sidecars on the same server</span>
      <span class="na">volume</span><span class="pi">:</span> <span class="s">&lt;service name&gt;-tailscale-state:/var/lib/tailscale</span>
</code></pre></div></div>]]></content><author><name>ben</name></author><summary type="html"><![CDATA[Tailscale is amazing for enabling private mesh network overlays.]]></summary></entry><entry><title type="html">Container Registry for Kamal, Secured with Tailscale</title><link href="https://benkenawell.com/2026/03/02/kamal-and-zot.html" rel="alternate" type="text/html" title="Container Registry for Kamal, Secured with Tailscale" /><published>2026-03-02T00:00:00+00:00</published><updated>2026-03-02T00:00:00+00:00</updated><id>https://benkenawell.com/2026/03/02/kamal-and-zot</id><content type="html" xml:base="https://benkenawell.com/2026/03/02/kamal-and-zot.html"><![CDATA[<p>I think this is a niche confluence of technologies.  <!--more-->  Hopefully my experience can help someone else who’s trying to deploy services with <a href="https://kamal-deploy.org/">Kamal</a>, all while behind a <a href="https://tailscale.com/">Tailnet</a>.  This enables you to use your own hardware, secure in your Tailnet, to deploy easily and zero downtime with Kamal.</p>

<p>Since I’m only deploying personal web apps/containers with this setup, I tend to think of it as pretty ephemeral, but if you take the time with this (and maybe have a mirror), Zot is capable of being a complete container registry for docker images and any <a href="https://oras.land/">OCI Artifacts</a>.</p>

<h2 id="setting-up-zot">Setting up Zot</h2>

<p><a href="https://zotregistry.dev/v2.1.14/">Zot</a> has pretty good docs, and it’s pretty simple to setup too.  We’re going to put it behind Tailscale, so if we’re a little lenient with some of the security options we should still be safe.  Some of this is <em>not best practices</em>.  At best, these are okay for a homelab setup when <em>nothing is exposed to the internet</em>.</p>

<p>In <a href="#appendix-a-zot-configuration-files">Appendix A</a>, I have a bunch of configuration files to help get you started.  Run the compose on any docker instance you want to use as your registry server. The server doesn’t need to be on your tailnet because we’ll put the registry on it directly.</p>

<p>Kamal requires a password be set for a registry it accesses through HTTPS, so you will need to <a href="https://zotregistry.dev/v2.1.14/articles/authn-authz/#htpasswd">setup a password</a> to use in Zot.</p>

<h2 id="configure-kamal">Configure Kamal</h2>

<p>You only need to set up Zot once, but Kamal you’ll configure for every project you want to deploy.  I wrote <a href="/2026/03/04/kamal-and-tailscale.html">another article</a> that goes into more detail about the Kamal configuration for deploying on a Tailnet.</p>

<p>In Kamal’s deploy.yml file, <a href="https://kamal-deploy.org/docs/configuration/builders/">the <code class="language-plaintext highlighter-rouge">builder</code> section</a> <strong>must</strong> contain “driver: docker”.  The <a href="https://docs.docker.com/build/builders/drivers/">docker driver</a> has access to your machine’s DNS configuration, but Kamal’s default “docker-container” does not.  I think this might change eventually, but I’m not sure. If you’re using a registry that doesn’t need Tailscale’s DNS to work, don’t worry about this setting.</p>

<p>I haven’t used a remote builder. I imagine it will need access to your Tailnet as well, to upload the built image.  I think the docker driver limits the architectures we can build an image for, so remote might be important if your laptop and server have different architectures.</p>

<h2 id="appendix-a-zot-configuration-files">Appendix A: Zot Configuration Files</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># example compose.yml with tailscale</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">registry</span>
<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">registry-tailscale-state</span><span class="pi">:</span>
  <span class="na">zot-data</span><span class="pi">:</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">sidecar</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/tailscale/tailscale:stable</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">registry</span>
    <span class="na">cap_add</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">NET_ADMIN</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">registry-tailscale-state:/var/lib/tailscale</span>
      <span class="pi">-</span> <span class="s">/path/to/configuration/directory:/config</span> <span class="c1"># holds the ts.json, below</span>
    <span class="na">devices</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/dev/net/tun:/dev/net/tun</span>
    <span class="na">env_file</span><span class="pi">:</span> <span class="s">stack.env</span> <span class="c1"># put all the tailscale environment variables here</span>
  <span class="na">app</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/project-zot/zot:latest</span>
    <span class="na">depends_on</span><span class="pi">:</span> 
      <span class="pi">-</span> <span class="s">sidecar</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="c1"># zot configuration</span>
      <span class="pi">-</span> <span class="s">/share/Docker/stack-configs/ocireg/config.yaml:/etc/zot/config.yaml</span>
      <span class="c1"># users and passwords</span>
      <span class="pi">-</span> <span class="s">/share/Docker/stack-configs/ocireg/htpasswd:/etc/zot/htpasswd</span>
      <span class="c1"># where our images will be stored</span>
      <span class="pi">-</span> <span class="s">zot-data:/data/zot</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">serve /etc/zot/config.yaml</span>

</code></pre></div></div>

<p>Example ts.json, <a href="https://tailscale.com/docs/features/tailscale-serve">Serve Functionality</a></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"TCP"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"443"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"HTTPS"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"Web"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"${TS_CERT_DOMAIN}:443"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"Handlers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"/"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"Proxy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://app:8080"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"AllowFunnel"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"${TS_CERT_DOMAIN}:443"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> 

</span></code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># example Zot configuration, config.yaml</span>
<span class="na">distSpecVersion</span><span class="pi">:</span> <span class="s">1.0.1</span>
<span class="na">storage</span><span class="pi">:</span>
  <span class="na">rootDirectory</span><span class="pi">:</span> <span class="s">/data/zot</span>
<span class="na">http</span><span class="pi">:</span>
  <span class="na">address</span><span class="pi">:</span> <span class="s">0.0.0.0</span>
  <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
  <span class="na">auth</span><span class="pi">:</span>
    <span class="na">htpasswd</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/etc/zot/htpasswd"</span>
  <span class="c1"># accessControl:</span>
  <span class="c1">#   repositories:</span>
  <span class="c1">#     **:</span>
  <span class="c1">#       defaultPolicy:</span>
  <span class="c1">#         - read</span>
  <span class="c1">#         - create</span>
  <span class="c1">#         - update</span>
  <span class="c1">#         - delete</span>
<span class="na">extensions</span><span class="pi">:</span>
  <span class="na">ui</span><span class="pi">:</span>
    <span class="na">enable</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">search</span><span class="pi">:</span>
    <span class="na">enable</span><span class="pi">:</span> <span class="kc">true</span>

</code></pre></div></div>]]></content><author><name>ben</name></author><summary type="html"><![CDATA[I think this is a niche confluence of technologies.]]></summary></entry><entry><title type="html">What Bash Does Better</title><link href="https://benkenawell.com/2026/02/19/what-bash-does-better.html" rel="alternate" type="text/html" title="What Bash Does Better" /><published>2026-02-19T00:00:00+00:00</published><updated>2026-02-19T00:00:00+00:00</updated><id>https://benkenawell.com/2026/02/19/what-bash-does-better</id><content type="html" xml:base="https://benkenawell.com/2026/02/19/what-bash-does-better.html"><![CDATA[<p>Bash is eschewed online for being hard to read and write. <!--more--> Depending on who you listen to, you should reach for something else anytime you need something longer than what fits in your terminal.  But I’ve been having a great time writing bash scripts.  They are at least twice concise as my boss’s equivalent javascript scripts, measuring objectively on lines of code and subjectively on how many lines I have to read to understand what a piece does.</p>

<p>There are some niceties that I like to set up for myself.  For example, the <code class="language-plaintext highlighter-rouge">die</code> function gives me an easy way to write checks</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>die<span class="o">()</span> <span class="o">{</span>
  <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span> <span class="o">&gt;</span>&amp;2
  <span class="nb">exit </span>1
<span class="o">}</span>

<span class="o">[[</span> <span class="s2">"</span><span class="si">$(</span>git rev-parse <span class="nt">--is-in-work-tree</span> 2&gt;/dev/null<span class="si">)</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"true"</span> <span class="o">]]</span> <span class="o">||</span> die <span class="s2">"Not in git repo"</span>
</code></pre></div></div>

<p>I often need to <a href="https://devhints.io/bash">look up</a> the string manipulations, but it’s <em>so simple</em> to read them.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">VERBOSE</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">VERBOSE</span><span class="k">:-</span><span class="nv">false</span><span class="k">}</span><span class="s2">"</span> <span class="c"># set VERBOSE to false if it is unset or null.</span>
</code></pre></div></div>

<p>Bash’s real super power over a traditional language is the <em>input and output flexibility</em>.  Its control flow is second-to-none.  The mechanism is super, super simple, strings all around really. But streams are the default, and they are <strong>powerful</strong>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while </span><span class="nb">read</span> <span class="nt">-r</span> line<span class="p">;</span> <span class="k">do</span>
 <span class="c"># run a command every time a notification is sent.</span>
<span class="k">done</span> &lt; &lt;<span class="o">(</span>curl <span class="nt">-sN</span> https://ntfy.sh/test/json<span class="o">)</span>
</code></pre></div></div>

<p>The thing on the last line there looks a little weird, doesn’t it? <code class="language-plaintext highlighter-rouge">&lt; &lt;(command)</code> is another super power of bash. How commands can be called.  You can treat a file as a string, a string as a file, open a named pipe on your system for inter process communication, inline a script in another language, and more! Bash takes it all in stride.  The only thing I’ve seen recently that comes close is <a href="https://weborigami.org/">WebOrigami</a>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">A</span><span class="o">=</span>3 <span class="nv">B</span><span class="o">=</span>3 node <span class="nt">-e</span> <span class="s2">"console.log(Number(process.env.A) + Number(process.env.B))"</span> <span class="c"># 6</span>
<span class="c"># heredoc</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
This is a long
 inline string with
 enters, spaces and everything!
</span><span class="no">EOF

</span><span class="c"># herestring</span>
<span class="nv">json</span><span class="o">=</span><span class="s1">'{"this": "Test", "hello": ["sun", "moon", "world"]}'</span>
<span class="nv">hello</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>jq <span class="nt">-n</span> <span class="s1">'.hello[2]'</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>I can even control the process forking of my command with ease. <code class="language-plaintext highlighter-rouge">()</code> for a subshell, <code class="language-plaintext highlighter-rouge">$()</code> for a subshell, piping stdout bach to me. <code class="language-plaintext highlighter-rouge">&lt;()</code> for a subshell, giving me back a file descriptor for it. Functions run in my process automatically, but I can run them in a subshell if I want. Commands may run in a subshell automatically, but I other scripts I can <code class="language-plaintext highlighter-rouge">source</code> and run in my process instead.</p>

<p>Lastly, its simple I/O mechanisms are available in <em>every language</em> and it can call <em>any executable on your system with ease, by name</em>.  I consider <a href="https://jqlang.org/">jq</a> part of my basic toolset nowadays.  And <a href="https://github.com/charmbracelet/gum">gum</a> a close second.  They’re installed on my system, I don’t need to import them every time. I wrote a <a href="https://www.npmjs.com/package/@benkenawell/parseargs">parseargs package</a> because I didn’t like the current options and know Node well, now I can call that easily as well! Eventually I’ll rewrite it in Zig to be faster, but my scripts won’t need to change at all.</p>

<p>The importing is a double edged sword, I know. It’s a reason to limit the amount of dependencies I use, luckily bash has a lot of good already in it!  But some dependencies are worth convincing other people to use.  I trust my taste here and so do my colleagues, but sometimes I will write helpers that don’t require others to have packages installed that I do.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>log<span class="o">()</span> <span class="o">{</span>
  <span class="k">if </span><span class="nb">command</span> <span class="nt">-v</span> gum &amp;&gt;/dev/null<span class="p">;</span> <span class="k">then
    </span>gum log <span class="nt">--structured</span> <span class="nt">--level</span> info <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
  <span class="k">else
    </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
  <span class="k">fi</span>
<span class="o">}</span>
</code></pre></div></div>

<p>There’s some syntax to learn with bash. It’s probably somewhere between Ruby and Perl in that.  There’s no one place to go and learn about it.  You probably only use it for quick, one off scripts and don’t fire your whole engineering brain making it robust.  But read enough tips and tricks, try new things each time, and I think you’ll find yourself reaching for it more and more often given that you can find it almost everywhere and it is amazingly expressive.</p>]]></content><author><name>Ben</name></author><summary type="html"><![CDATA[Bash is eschewed online for being hard to read and write.]]></summary></entry><entry><title type="html">Don’t Underestimate a CTE</title><link href="https://benkenawell.com/2026/02/12/dont-underestimate-a-cte.html" rel="alternate" type="text/html" title="Don’t Underestimate a CTE" /><published>2026-02-12T00:00:00+00:00</published><updated>2026-02-12T00:00:00+00:00</updated><id>https://benkenawell.com/2026/02/12/dont-underestimate-a-cte</id><content type="html" xml:base="https://benkenawell.com/2026/02/12/dont-underestimate-a-cte.html"><![CDATA[<p>CTEs are a powerful tool for making your SQL more legible. <!--more--> They also help me model my data and iterate quickly before materializing the final shape of a table, without mucking about modifying a table shape.</p>

<h2 id="the-problem">The Problem</h2>

<p>My wife wants to know when she’ll work a certain number of hours so she can take an exam. There’s a projection forward, but I also want to take into account days/hours she’s already worked and days that are out of the norm.</p>

<h2 id="the-start">The Start</h2>

<p>I could make one table with entries for all her hours worked, but then I need to track in that table which are a projection and which are “real”. Thinking myself clever, I decided the distinction warranted two tables. One for simple entries of hours worked and one to hold all the projections.</p>

<p>How do I fill in all those projection dates? I came up with a simple algorithm that basically takes all the week days and gives her 8 hours a day. I used a CTE to come up with this, then I was going to insert them into projections and go from there.</p>

<h2 id="the-iteration">The Iteration</h2>

<p>I made a <em>table expression</em> to insert data into a real table. But why not just use that expression in my calculation directly?  If I have a algorithmic projection, what parameters could I change that would cause the calculation to update? Those parameters are what I want to store in a table!</p>

<p>If she’s works part time, I can tune that parameter directly instead of trying to reconcile what’s in the table with the new data I want. The SQL CTE will just recalculate it for me.</p>

<p>CTEs are super nice to iterate quickly, since there’s nothing stored to clean up. DBeaver has a nice concept of variables I used to help me decide what parameters should be part of my configuration.  What I put in the DBeaver variables become columns in the table directly.</p>

<h2 id="the-solution-and-next-steps">The Solution and Next Steps</h2>

<p>Now that I have a more stable idea of what I want, the next step is materializing it in the database. A View for my projection and a Table for my configuration. Both make the query a little more complicated, since now I have to get the configuration from a new table. But both will help make my final calculation, when will my wife hit her testing hours, easier. And we can tweak parameters until we’ve run through all the scenarios we want. Even generating a little rails app where she could tweak the parameters herself without wading through SQL code shouldn’t be too hard. If I need to change the projection, I can change the view. If we need more parameters, I can tweak the table.</p>

<p>It wasn’t my initial design, but CTEs helped me find my way to a more powerful solution.</p>]]></content><author><name>Ben</name></author><summary type="html"><![CDATA[CTEs are a powerful tool for making your SQL more legible.]]></summary></entry><entry><title type="html">A List of DSLs for a Website</title><link href="https://benkenawell.com/2025/11/07/a-list-of-dsls-for-a-website.html" rel="alternate" type="text/html" title="A List of DSLs for a Website" /><published>2025-11-07T00:00:00+00:00</published><updated>2025-11-07T00:00:00+00:00</updated><id>https://benkenawell.com/2025/11/07/a-list-of-dsls-for-a-website</id><content type="html" xml:base="https://benkenawell.com/2025/11/07/a-list-of-dsls-for-a-website.html"><![CDATA[<p>The most basic form of a website needs ~4 or 5 languages. <!--more--></p>

<blockquote>
  <p>written after reading <a href="https://unplannedobsolescence.com/blog/what-dynamic-typing-is-for/">this article</a></p>
</blockquote>

<ol>
  <li>HTML</li>
  <li>CSS</li>
  <li>Javascript</li>
  <li>SQL</li>
  <li>backend language</li>
</ol>

<p>3 are required to use the browser most effectively. 1 is required for solid data management. The last one is the only one we <em>really</em> get to choose.  Even if we chose Javascript, I could agree it’s 5 languages: Node and other runtimes are <em>very close</em> but ultimately different environments for the same language and they should be treated differently.</p>

<p>In a production Node application, with a React frontend, we could easily double that count.</p>

<ol>
  <li>HTML</li>
  <li>JSX</li>
  <li>CSS</li>
  <li>Panda CSS</li>
  <li>React</li>
  <li>Javascript/Typescript/Node</li>
  <li>SQL</li>
  <li>Prisma</li>
  <li>Lua</li>
</ol>

<p>9 languages! 6 or 7 DSLs! Some are over top of others, but you write better JSX if you understand HTML better.  Same for Panda and CSS, Prisma and SQL.  For every DSL layer on top of a DSL doesn’t abstract it away as much as you’d think.</p>

<h2 id="what-makes-a-dsl">What makes a DSL?</h2>

<p>Would <em>any</em> templating language be considered a DSL?  Something like Liquid or Nunjucks?  They’re such simpler abstractions than JSX/React, you really get the semantics of the underlying DSL much, much more clearly.  There’s hardly even another layer to think about.  Ultimately they do require a little more knowledge, but they also don’t really know anything about the HTML layer beneath them.  They could be used to template Javascript or SQL the exact same way.</p>

<p>More advanced templaters like ERB (with Rails) or Laravel’s Blade definitely feel like they spill over into DSL land.  They start to feel more aware of the langauge they’re abstracting.  Since they ultimately render down to HTML though, they’re still better at separating what they do vs the templated code than a React/JSX that tries to mix the two and bring a runtime into the mix.</p>

<h2 id="do-we-gain-enough-through-the-abstraction-the-additional-dsl-to-justify-it">Do we gain enough through the abstraction, the additional DSL, to justify it?</h2>

<p>Each person and group can decide for themselves and has a different tolerance for then.  10-15 years ago, React, SASS, etc. smoothed a lot of rough edges on the DSLs that make up web pages.  But the platform has come a long way since then.  For me, I prefer working with the DSLs directly and dropping into the full Javascript language when needed over just smoothing edges that aren’t rough or pointy anymore.</p>]]></content><author><name>Ben</name></author><summary type="html"><![CDATA[The most basic form of a website needs ~4 or 5 languages.]]></summary></entry><entry><title type="html">A Weekend With Caddy</title><link href="https://benkenawell.com/2025/07/20/a-weekend-with-caddy.html" rel="alternate" type="text/html" title="A Weekend With Caddy" /><published>2025-07-20T00:00:00+00:00</published><updated>2025-07-20T00:00:00+00:00</updated><id>https://benkenawell.com/2025/07/20/a-weekend-with-caddy</id><content type="html" xml:base="https://benkenawell.com/2025/07/20/a-weekend-with-caddy.html"><![CDATA[<p>I’ve been dabbling with <a href="https://caddyserver.com">Caddy</a> web server.<!--more--> It’s the easiest reverse proxy I’ve found for self signed development certs.  I set up a Caddy server to run my personal blog, a static website.  This weekend, I took it a step further and used some caddy templating to make a mostly static site dynamic.  The outcome?  Every single one of these projects has felt like a magical experience.  In this post, I focus on this past weekend when I used some caddy templating to make a mostly static site dynamic.</p>

<p>I host a countdown timer for various friends and family’s weddings.  Previously, it was an entirely static site I made for my own wedding, hosted on Github Pages.  Then I changed it for the next wedding.  But then I couldn’t see how long I had been married for!</p>

<p>I needed the barest amount of dynamic content on my page.  I didn’t want to ship everybody’s wedding dates to the client, so it needed to happen server side.  I make web apps (React, et al) for a living but that was just way too much (effort, code, time, etc) for what should be a simple problem, one interpolated date!</p>

<p>I had been testing Caddy to host my personal blog, a static website.  It’s been performing phenomenally, I’m able to deploy faster to it (via rsync) than to SourceHut Pages most of the time.  I had recently come across the <a href="https://caddyserver.com/docs/modules/http.handlers.templates#docs">Caddy templates module</a>.  For my blog, I was resistant to becoming too dependent on a feature like that, in case I needed to switch servers or something.  But the concept was the exact right amount of complexity for my wedding countdown timer project.</p>

<p>It took me a few hours to adapt my static site: loading a json file server side then interpolating the date into my timer-element web component and deploying the changes on my public facing web server (adapting the Caddyfile I had there to host two sites at the same time!).  But the end result?  I’m very impressed!  I was able to add a bunch more weddings to my site by adding lines to the json file.  Everyone I texted about it was happy to see the counters to their weddings!  It will last as long as Caddy does, I didn’t need to introduce or maintain any other moving parts.</p>

<p>For the future, I’m exploring templating via a <a href="https://en.wikipedia.org/wiki/Common_Gateway_Interface">CGI script</a>.  It’s slightly more work, but much more server agnostic.  Luckily, caddy has a third party module to support CGI and an amazing build tool, xcaddy, to produce supporting binaries.  We’ll see where my ideas take me, but I know I’ll be using Caddy for a lot in the future! Thank you <a href="https://github.com/sponsors/mholt">Matt Holt</a> for the amazing project!</p>]]></content><author><name>Ben</name></author><summary type="html"><![CDATA[I’ve been dabbling with Caddy web server.]]></summary></entry><entry><title type="html">Tailscale Sidecar Run Deep Dive</title><link href="https://benkenawell.com/2025/07/10/tailscale-sidecar-run-deep-dive.html" rel="alternate" type="text/html" title="Tailscale Sidecar Run Deep Dive" /><published>2025-07-10T00:00:00+00:00</published><updated>2025-07-10T00:00:00+00:00</updated><id>https://benkenawell.com/2025/07/10/tailscale-sidecar-run-deep-dive</id><content type="html" xml:base="https://benkenawell.com/2025/07/10/tailscale-sidecar-run-deep-dive.html"><![CDATA[<p>From my other post <a href=""></a>, we have a tailscale container running alongside another container.  It’s not a long file, but there is a lot of configuration going on. In this post, I’m going to step through the <code class="language-plaintext highlighter-rouge">run</code> script line by line. <!--more--></p>

<h2 id="full-file">Full File</h2>

<p>First, the full file for reference</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>

<span class="c"># load our environment variables</span>
<span class="nb">source</span> .env

<span class="c"># run our sidecar container</span>
podman run <span class="nt">--detach</span> <span class="se">\</span>
  <span class="nt">--hostname</span> <span class="s2">"</span><span class="nv">$HOSTNAME</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">--name</span> <span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span><span class="se">\</span>
  <span class="nt">--cap-add</span> net_admin <span class="se">\</span>
  <span class="nt">--device</span> /dev/net/tun:/dev/net/tun <span class="se">\</span>
  <span class="nt">--volume</span> freshrss_tailscale_state:/var/lib/tailscale <span class="se">\</span>
  <span class="nt">--env-file</span> .ts.env <span class="se">\</span>
  ghcr.io/tailscale/tailscale:latest

<span class="c"># run our FreshRSS service</span>
podman run <span class="nt">--detach</span> <span class="se">\</span>
  <span class="nt">--restart</span> unless-stopped <span class="se">\</span>
  <span class="nt">--log-opt</span> max-size<span class="o">=</span>10m <span class="se">\</span>
  <span class="nt">--network</span> container:<span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">TZ</span><span class="o">=</span>America/New_York <span class="se">\</span>
  <span class="nt">-e</span> <span class="s1">'CRON_MIN=1,31'</span> <span class="se">\</span>
  <span class="nt">-v</span> freshrss_data:/var/www/FreshRSS/data <span class="se">\</span>
  <span class="nt">-v</span> freshrss_extensions:/var/www/FreshRSS/extensions <span class="se">\</span>
  <span class="nt">--name</span> <span class="s2">"</span><span class="nv">$CONTAINER_NAME</span><span class="s2">"</span> <span class="se">\</span>
  docker.io/freshrss/freshrss
</code></pre></div></div>

<h2 id="line-by-line">Line by Line</h2>

<h3 id="bash-setup">Bash setup</h3>

<p>The first few lines are just bash setup.</p>

<dl>
  <dt><code class="language-plaintext highlighter-rouge">#!/usr/bin/env bash</code></dt>
  <dd>run this script through the bash interpreter.  After making the script executable<sup id="fnref:chmod"><a href="#fn:chmod" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, this tells our computer how to run it.</dd>
  <dt><code class="language-plaintext highlighter-rouge">source .env</code></dt>
  <dd>We set up a .env file with bash variables<sup id="fnref:variables"><a href="#fn:variables" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> inside. The <code class="language-plaintext highlighter-rouge">source</code> command loads those variables into our current process.  We could put any bash in there, but by convention we’re only setting variables.</dd>
</dl>

<h3 id="tailscale-sidecar-container">Tailscale Sidecar Container</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman run <span class="nt">--detach</span> <span class="se">\</span>
  <span class="nt">--hostname</span> <span class="s2">"</span><span class="nv">$HOSTNAME</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">--name</span> <span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span><span class="se">\</span>
  <span class="nt">--cap-add</span> net_admin <span class="se">\</span>
  <span class="nt">--device</span> /dev/net/tun:/dev/net/tun <span class="se">\</span>
  <span class="nt">--volume</span> freshrss_tailscale_state:/var/lib/tailscale <span class="se">\</span>
  <span class="nt">--env-file</span> .ts.env <span class="se">\</span>
  ghcr.io/tailscale/tailscale:latest
</code></pre></div></div>

<dl>
  <dt><code class="language-plaintext highlighter-rouge">podman run --detach</code></dt>
  <dd>tell our container engine, podman, to run a container detached so we aren’t connected to it’s stdout</dd>
  <dt><code class="language-plaintext highlighter-rouge">--hostname "$HOSTNAME"</code></dt>
  <dd>set the <a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#hostname-h-name">hostname</a> for the container. Tailscale uses this as the machine name. We’re using the HOSTNAME variable we set in our .env, which makes it easy to share between scripts</dd>
  <dt><code class="language-plaintext highlighter-rouge">--name "$SIDECAR_NAME"</code></dt>
  <dd>give the <a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#name-name">container a name</a>. We reference this in our service container, so we’ve made it a variable to ensure it has the same value both places.</dd>
  <dt><code class="language-plaintext highlighter-rouge">--cap-add net_admin</code></dt>
  <dd><a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#cap-add-capability">add the net_admin</a> capability to the linux container. This gives the container some priviledges. Tailscale needs it to do some of its networking.</dd>
  <dt><code class="language-plaintext highlighter-rouge">--device /dev/net/tun:/dev/net/tun</code></dt>
  <dd><a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#device-host-device-container-device-permissions">give Tailscale access</a> to the tun device so it can network effectively.</dd>
  <dt><code class="language-plaintext highlighter-rouge">--volume freshrss_tailscale_state:/var/lib/tailscale</code></dt>
  <dd>save the Tailscale state into a <a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options">volume</a> named “freshrss_tailscale_state”.  /var/lib/tailscale is where the information needed for Tailscale to remember this device, so we can teardown and recreate this container at will.  This state directory is set with TS_STATE_DIR, which we set in the .ts.env and is loaded by the next line.</dd>
  <dt><code class="language-plaintext highlighter-rouge">--env-file .ts.env</code></dt>
  <dd>this is the file the holds the environment variables this Tailscale container needs to authenticate and store state in the correct place.  <a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#environment">env-file</a> is a nice alternative to setting envvars individually, like we do with FreshRSS.  It can keep secrets, like our oauth token, out of git (don’t commit your .ts.env file!) and collects all the envvars we need into one place.  In fact, we could do this same thing with FreshRSS if we wanted or <a href="https://github.com/FreshRSS/FreshRSS/tree/edge/Docker#environment-variables">our configuration</a> got more complex!</dd>
  <dt><code class="language-plaintext highlighter-rouge">ghcr.io/tailscale/tailscale:latest</code></dt>
  <dd>tells podman which <a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#image">container image</a> to run, “ghcr.io/tailscale/tailscale,” and which version, the “latest” tag.  We rely on the command from the Dockerfile this container image was built with to automatically start Tailscale, so we don’t need to specify anything else! ghcr.io is GitHub’s container registry, so we know that’s where tailscale hosts it!</dd>
</dl>

<h3 id="freshrss-container">FreshRSS Container</h3>

<p>FreshRSS is our main service in the example, but it could be any container you want to run on your tailnet.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run our FreshRSS service</span>
podman run <span class="nt">--detach</span> <span class="se">\</span>
  <span class="nt">--restart</span> unless-stopped <span class="se">\</span>
  <span class="nt">--log-opt</span> max-size<span class="o">=</span>10m <span class="se">\</span>
  <span class="nt">--network</span> container:<span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">TZ</span><span class="o">=</span>America/New_York <span class="se">\</span>
  <span class="nt">-e</span> <span class="s1">'CRON_MIN=1,31'</span> <span class="se">\</span>
  <span class="nt">-v</span> freshrss_data:/var/www/FreshRSS/data <span class="se">\</span>
  <span class="nt">-v</span> freshrss_extensions:/var/www/FreshRSS/extensions <span class="se">\</span>
  <span class="nt">--name</span> <span class="s2">"</span><span class="nv">$CONTAINER_NAME</span><span class="s2">"</span> <span class="se">\</span>
  docker.io/freshrss/freshrss
</code></pre></div></div>

<dl>
  <dt><code class="language-plaintext highlighter-rouge">podman run --detach</code></dt>
  <dd><a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html">run</a> this container in detached mode too</dd>
  <dt><code class="language-plaintext highlighter-rouge">--restart unless-stopped</code></dt>
  <dd>set the <a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#restart-policy">restart policy</a> for the container.  <code class="language-plaintext highlighter-rouge">unless-stopped</code> will always try to restart the container unless we explicitly run stop to stop the container. Not really necessary here, since the tailscale sidecar won’t restart this way. But we could have that too if we wanted.</dd>
  <dt><code class="language-plaintext highlighter-rouge">--log-opt max-size=10m</code></dt>
  <dd>freshrss suggests this setting. It sets the max size of the log file. <a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#log-opt-name-value">More info</a></dd>
  <dt><code class="language-plaintext highlighter-rouge">--network continaer:"SIDECAR_NAME"</code></dt>
  <dd>this line does the tailscale magic.  It makes the tailscale conatiner (with name $SIDECAR_NAME) <a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#network-mode-net">the networking stack</a> for this container, so our tailscale sidecar can connect to tailscale <em>and talk to the service within this container</em>.  We’ve parameterized the name of the tailscale sidecar container so we know we’re using the right one!</dd>
  <dt><code class="language-plaintext highlighter-rouge">-e TZ=America/New_York</code></dt>
  <dd>sets an <a href="https://docs.podman.io/en/latest/markdown/podman-run.1.html#env-e-env">environment variable inside the container</a>. In this case, FreshRSS uses the TZ variable to set <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">timezone information</a>, so I’ve set it to the US East Coast.</dd>
  <dt><code class="language-plaintext highlighter-rouge">-e 'CRON_MIN=1,31</code></dt>
  <dd>another environment variable. This one FreshRSS uses to set when to run a cronjob and update feeds. It’s wrapped in single quotes so we can be sure our shell sends those exact characters through, instead of expanding it somehow.</dd>
  <dt><code class="language-plaintext highlighter-rouge">-v freshrss_data:/var/www/FreshRSS/data</code></dt>
  <dd>mount a <a href="https://docs.podman.io/en/v4.4/markdown/options/volume.html">volume</a> in the container.  A volume is a place we can store things to persist even when the container has been destroyed.  In this case, we’re binding to the directory FreshRSS uses to store its data.</dd>
  <dt><code class="language-plaintext highlighter-rouge">-v freshrss_extensions:/var/www/FreshRSS/extensions</code></dt>
  <dd>mount another volume into the container.  This is the directory FreshRSS uses to save extensions if we load any.</dd>
  <dt><code class="language-plaintext highlighter-rouge">--name "$CONTAINER_NAME"</code></dt>
  <dd>set the name of the container to the value we stored in $CONTAINER_NAME in our .env file.  This makes it much easier to find this container when we run <a href="https://docs.podman.io/en/latest/markdown/podman-ps.1.html">podman ps</a></dd>
  <dt><code class="language-plaintext highlighter-rouge">docker.io/freshrss/freshrss</code></dt>
  <dd>the name of the container to run.  Note the <code class="language-plaintext highlighter-rouge">docker.io</code> on the front here; docker sets this by default if you don’t use it.  Other container engines require the full uri (but have settings where you can set defaults).</dd>
</dl>

<h2 id="wrap-up">Wrap Up</h2>

<p>That’s the entire file, line by line!  Of course you can read all about all these options and more in the <a href="https://docs.podman.io/en/latest/">docs</a> but hopefully this was a nice little tutorial of all of them in plain English for you. Container engines have a lot of options and the best way to learn is by practice.</p>

<p>Like a pod of seals, float on!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:chmod">
      <p><code class="language-plaintext highlighter-rouge">chmod +x run</code> <a href="#fnref:chmod" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:variables">
      <p>key=”value” pairs, where the quotes help in case there’s space! <a href="#fnref:variables" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Ben</name></author><summary type="html"><![CDATA[From my other post , we have a tailscale container running alongside another container. It’s not a long file, but there is a lot of configuration going on. In this post, I’m going to step through the run script line by line.]]></summary></entry><entry><title type="html">Tailscale Sidecar Tutorial</title><link href="https://benkenawell.com/2025/07/07/tailscale-sidecar-tutorial.html" rel="alternate" type="text/html" title="Tailscale Sidecar Tutorial" /><published>2025-07-07T00:00:00+00:00</published><updated>2025-07-07T00:00:00+00:00</updated><id>https://benkenawell.com/2025/07/07/tailscale-sidecar-tutorial</id><content type="html" xml:base="https://benkenawell.com/2025/07/07/tailscale-sidecar-tutorial.html"><![CDATA[<p>Adding <a href="https://tailscale.com/">Tailscale</a> to your self hosted containers lets you easily set up those services with a secure, fully qualified domain name. <!--more-->  It is easy to remember and easy to set up.  Tailscale has their own <a href="https://tailscale.com/kb/1282/docker">Docker docs</a>, but I want to expand on that starting point and use a different container engine.</p>

<p>There’s a lot to Tailscale that facilitates this magic.  First, the Tailnet name means you can give each container a name.  It’s easy to identify on the Tailscale Dashboard, it’s easy to type into a URL bar, it’s as easy to use as any URL! Second, OAuth tokens make it simple to connect containers (not just machines!) into your Tailnet.  Third, HTTPS certificates mean web services behave like you would expect. They look secure in the browser and have access to <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts">secure context APIs</a>.  Lastly, Tailscale Serve proxies you to the correct port in your container.</p>

<p>Those services together make the experience pretty seamless to connect, once you know how all those pieces fit together.  This post will walk you through all the steps you need to set this up today.  We’ll be using <a href="https://www.freshrss.org/">FreshRSS</a> as the service we’re proxying.  I’m <em>not</em> going to use docker compose. Instead, I’ll be using <a href="https://podman.io/">podman</a> and a handful of bash scripts to connect our containers together.  This way, the tutorial will be a little more generic and should work for any oci compliant engine (docker, podman, nerdctl, etc).</p>

<h2 id="prerequisites">Prerequisites</h2>

<ul>
  <li>Have a Tailscale account.</li>
  <li>Have a computer on your Tailnet.</li>
  <li>Have a container engine installed. Podman, docker, and nerdctl should all work interchangably, but I’ll use podman for this tutorial</li>
  <li>A project directory on your computer. Something like <code class="language-plaintext highlighter-rouge">~/services/freshrss-sidecar</code> would be great.  All files and folders will be created in this directory.</li>
  <li>Some kind of text editor.</li>
  <li>A Linux/Mac computer – not a requirement, but all my commands will be for Unix-y operating systems.</li>
</ul>

<h2 id="tutorial">Tutorial</h2>

<h3 id="pull-the-container-images">Pull the container images</h3>

<p>If you skip this step, podman will pull these the first time we run the container.  Either way is fine!</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># pull tailscale</span>
podman pull ghcr.io/tailscale/tailscale:latest
<span class="c"># pull freshrss</span>
podman pull docker.io/freshrss/freshrss:latest
</code></pre></div></div>

<h3 id="set-up-some-environment-variables">Set up some environment variables</h3>

<p>This <code class="language-plaintext highlighter-rouge">.env</code> file will hold a few configuration variables we can share across our bash scripts.  You can parameterize more or less, but this is a good start. <code class="language-plaintext highlighter-rouge">HOSTNAME</code> is how we’ll reference it on our tailnet at the end.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># file: .env</span>
<span class="nv">SIDECAR_NAME</span><span class="o">=</span><span class="s2">"freshrss_tailscale"</span>
<span class="nv">CONTAINER_NAME</span><span class="o">=</span><span class="s2">"freshrss"</span>
<span class="nv">HOSTNAME</span><span class="o">=</span><span class="s2">"rss"</span>
</code></pre></div></div>

<p>Next, create a <code class="language-plaintext highlighter-rouge">.ts.env</code> file.  This one will hold all the environment variables our tailscale sidecar container needs to connect to our tailnet.  We can fill in everything in this file but our TS_AUTHKEY argument. We’ll generate that shortly.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># file: .ts.env</span>
<span class="nv">TS_AUTHKEY</span><span class="o">=</span>tskey-client-notareal-tailscaleoauthtoken
<span class="nv">TS_EXTRA_ARGS</span><span class="o">=</span><span class="nt">--advertise-tags</span><span class="o">=</span>tag:container
<span class="nv">TS_STATE_DIR</span><span class="o">=</span>/var/lib/tailscale
<span class="nv">TS_USERSPACE</span><span class="o">=</span><span class="nb">false
</span><span class="nv">TS_SERVE_CONFIG</span><span class="o">=</span>/config/ts.json
</code></pre></div></div>

<h3 id="pick-a-tailscale-tailnet-name">Pick a Tailscale Tailnet Name</h3>

<p>In your Tailscale dashboard, go to DNS and find your <a href="https://tailscale.com/kb/1217/tailnet-name">Tailnet name</a>.  Tailscale uses <code class="language-plaintext highlighter-rouge">yak-bebop</code> as their example. You can’t pick the one you want (only generate it), and you also can’t change it once you turn on HTTPS.  So take some time to generate one that you like.  I like a fun name, but you can pick the default too!</p>

<p>We can make most of this tutorial work without HTTPS, so we won’t turn it on until the end.</p>

<h3 id="generate-an-oauth-client">Generate an OAuth client</h3>

<p>An [OAuth client] gives you a more secure way to connect to your tailnet versus and Auth Key.  Before we can generate the token, we need a tag to give our container.  The official docs give a good reference for <a href="https://tailscale.com/kb/1068/tags#define-a-tag">defining a tag</a>.  My short version: go to Tailscale Access Controls tab and add an entry under the <code class="language-plaintext highlighter-rouge">tagOwner</code> key named <code class="language-plaintext highlighter-rouge">tag:container</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"tagOwners"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nl">"tag:container"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"autogroup:admin"</span><span class="p">],</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<p>Save that with the button beneath and now we have a group we can assign the <a href="https://tailscale.com/kb/1215/oauth-clients">OAuth token</a> to. So head over to your Tailscale Settings page, the “OAuth clients” option along the sidebar and click the “Generate OAuth Client” button.  Type a description like “FreshRSS Test Token” and make sure you give it the “Devices:Core” and “Keys:Auth Keys” write permissions.  Make sure you have the tag:container that we just created chosen under the “Add tag” dropdown.  It should automatically assign to the second permission if you’ve set it for the first one.</p>

<p>Remember, “Devices:Core” and “Keys:Auth Keys” write permissions, assigned to your new tag, tag:container!</p>

<p>Click “Generate Client” and copy the key it gives you into your <code class="language-plaintext highlighter-rouge">.ts.env</code> file.  You’ll never see this key again, but if you lose it just delete this one and regenerate a new one.  The TS_AUTHKEY line should now have your auth token in place of our previous placeholder, giving you a file that looks like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">TS_AUTHKEY</span><span class="o">=</span>tskey-client-yournew-tailscaleoauthtoken
<span class="nv">TS_EXTRA_ARGS</span><span class="o">=</span><span class="nt">--advertise-tags</span><span class="o">=</span>tag:container
<span class="nv">TS_STATE_DIR</span><span class="o">=</span>/var/lib/tailscale
<span class="nv">TS_USERSPACE</span><span class="o">=</span><span class="nb">false
</span><span class="nv">TS_SERVE_CONFIG</span><span class="o">=</span>/config/ts.json
</code></pre></div></div>

<h3 id="running-our-container">Running our Container</h3>

<p>Now we have everything we need to run our container and access it over http.  We need two containers, so we’ll write a short script to help coordinate it.  I call it <code class="language-plaintext highlighter-rouge">run</code>, to mimic the <code class="language-plaintext highlighter-rouge">podman run</code> command. It will create 3 volumes, use the images we pulled in the first step, and our .env and .ts.env files to load everything up.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>

<span class="c"># load our environment variables</span>
<span class="nb">source</span> .env

<span class="c"># run our sidecar container</span>
podman run <span class="nt">--detach</span> <span class="se">\</span>
  <span class="nt">--hostname</span> <span class="s2">"</span><span class="nv">$HOSTNAME</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">--name</span> <span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span><span class="se">\</span>
  <span class="nt">--cap-add</span> net_admin <span class="se">\</span>
  <span class="nt">--device</span> /dev/net/tun:/dev/net/tun <span class="se">\</span>
  <span class="nt">--volume</span> freshrss_tailscale_state:/var/lib/tailscale <span class="se">\</span>
  <span class="nt">--env-file</span> .ts.env <span class="se">\</span>
  ghcr.io/tailscale/tailscale:latest

<span class="c"># run our FreshRSS service</span>
podman run <span class="nt">--detach</span> <span class="se">\</span>
  <span class="nt">--restart</span> unless-stopped <span class="se">\</span>
  <span class="nt">--log-opt</span> max-size<span class="o">=</span>10m <span class="se">\</span>
  <span class="nt">--network</span> container:<span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">TZ</span><span class="o">=</span>America/New_York <span class="se">\</span>
  <span class="nt">-e</span> <span class="s1">'CRON_MIN=1,31'</span> <span class="se">\</span>
  <span class="nt">-v</span> freshrss_data:/var/www/FreshRSS/data <span class="se">\</span>
  <span class="nt">-v</span> freshrss_extensions:/var/www/FreshRSS/extensions <span class="se">\</span>
  <span class="nt">--name</span> <span class="s2">"</span><span class="nv">$CONTAINER_NAME</span><span class="s2">"</span> <span class="se">\</span>
  docker.io/freshrss/freshrss
</code></pre></div></div>

<p>Make the file executable by running <code class="language-plaintext highlighter-rouge">chmod +x run</code>, the run the script with <code class="language-plaintext highlighter-rouge">./run</code>.  Or you can call <code class="language-plaintext highlighter-rouge">bash run</code> and not need to run the chmod command.</p>

<p>Shortly, we should see a “machine” with the tag:container under our Tailscale Dashboard’s machine tab called “rss”.</p>

<p>Navigate a web browser to <code class="language-plaintext highlighter-rouge">http://rss.${tailnet-name}.ts.net</code> (eg, <code class="language-plaintext highlighter-rouge">http://rss.yak-bebop.ts.net</code>) from your tailnet connected computer and you should see the FreshRSS welcome page!  Because we don’t have https certificates working anyway, you could use <code class="language-plaintext highlighter-rouge">http://rss</code> to go to the same site, but the short names don’t work with HTTPS certs so stick with the longer version.</p>

<p>You could be done at this point, but Tailscale Serve ensures you can work with many different container ports and HTTPS Certs will make your experience much better in a modern browser.</p>

<h3 id="tailscale-serve-config">Tailscale Serve Config</h3>

<p>This is the last configuration we need, <a href="https://tailscale.com/blog/reintroducing-serve-funnel">Tailscale Serve</a>. Tailscale won’t provision a TLS cert for us without this.  And the config will tell tailscale what port to proxy to, so we don’t need to type that into our browser either.  We didn’t need a port number with FreshRSS because it exposes itself on the correct port, port 80.  But for most services we’ll want this set up.  And 80 is the wrong port for https (that’s port 443).  Make a <code class="language-plaintext highlighter-rouge">config</code> directory with <code class="language-plaintext highlighter-rouge">ts.json</code> in it, to mount to our tailscale sidecar container.  Under the Proxy key is where you would set the port for your service; FreshRSS is on port 80.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"TCP"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"443"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"HTTPS"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"Web"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"${TS_CERT_DOMAIN}:443"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"Handlers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"/"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"Proxy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://127.0.0.1:80"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"AllowFunnel"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"${TS_CERT_DOMAIN}:443"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Since this serve config tells Tailscale to generate a https certificate for us, let’s turn that on in Tailscale before we use it.</p>

<h3 id="tailscale-https-certificates">Tailscale HTTPS Certificates</h3>

<p>In your Tailscale Dashboard, go to the DNS tab and scroll to the bottom.  Click Enable HTTPS and go through any of its prompts.  Done!  Now let’s return to the terminal and restart these containers. The <a href="https://tailscale.com/kb/1153/enabling-https">official docs</a> are easy to follow for this step.</p>

<h3 id="restart-with-https">Restart with https</h3>

<p>Our service container, freshrss, depends on the tailscale sidecar to function.  That means we can’t stop tailscale until our  freshrss container has stopped.  Let’s write a small script to help us always stop the containers in the correct order.  Call it <code class="language-plaintext highlighter-rouge">stop</code> to again mimic the podman cli.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>

<span class="c"># reuse our envvars to get the container names</span>
<span class="nb">source</span> .env

podman stop <span class="s2">"</span><span class="nv">$CONTAINER_NAME</span><span class="s2">"</span>
podman stop <span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span>
</code></pre></div></div>

<p>Then, we can also make a <code class="language-plaintext highlighter-rouge">rm</code> script to fully remove the containers.  This could be one script, but I like breaking it into two for a few reasons. One, I might not always want to fully remove the containers. Two, it mimics the podman cli which has pretty good ergonomics for all its power.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>

<span class="nb">source</span> .env

podman <span class="nb">rm</span> <span class="s2">"</span><span class="nv">$CONTAINER_NAME</span><span class="s2">"</span>
podman <span class="nb">rm</span> <span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span>
</code></pre></div></div>

<p>We need to add a line to our run script to mount the tailscale serve config, making the full file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>

<span class="nb">source</span> .env

podman run <span class="nt">--detach</span> <span class="se">\</span>
  <span class="nt">--hostname</span> <span class="s2">"</span><span class="nv">$HOSTNAME</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">--name</span> <span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span><span class="se">\</span>
  <span class="nt">--cap-add</span> net_admin <span class="se">\</span>
  <span class="nt">--device</span> /dev/net/tun:/dev/net/tun <span class="se">\</span>
  <span class="nt">--volume</span> freshrss_tailscale_state:/var/lib/tailscale <span class="se">\</span>
  <span class="nt">--volume</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">/config"</span>:/config <span class="se">\</span>
  <span class="nt">--env-file</span> .ts.env <span class="se">\</span>
  ghcr.io/tailscale/tailscale:latest

podman run <span class="nt">--detach</span> <span class="se">\</span>
  <span class="nt">--restart</span> unless-stopped <span class="se">\</span>
  <span class="nt">--log-opt</span> max-size<span class="o">=</span>10m <span class="se">\</span>
  <span class="nt">--network</span> container:<span class="s2">"</span><span class="nv">$SIDECAR_NAME</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">TZ</span><span class="o">=</span>America/New_York <span class="se">\</span>
  <span class="nt">-e</span> <span class="s1">'CRON_MIN=1,31'</span> <span class="se">\</span>
  <span class="nt">-v</span> freshrss_data:/var/www/FreshRSS/data <span class="se">\</span>
  <span class="nt">-v</span> freshrss_extensions:/var/www/FreshRSS/extensions <span class="se">\</span>
  <span class="nt">--name</span> <span class="s2">"</span><span class="nv">$CONTAINER_NAME</span><span class="s2">"</span> <span class="se">\</span>
  docker.io/freshrss/freshrss
</code></pre></div></div>

<p>After all that, run the <code class="language-plaintext highlighter-rouge">stop</code> script, then the <code class="language-plaintext highlighter-rouge">rm</code> script, then the <code class="language-plaintext highlighter-rouge">run</code> script again.  You should see everything come back up and work, with any state/login/rss feeds you already setup in FreshRSS!</p>

<h2 id="troubleshooting">Troubleshooting</h2>

<h3 id="deleting-the-tailscale-state-volume">Deleting the Tailscale state volume</h3>

<p>If we delete the “freshrss_tailscale_state” volume, tailscale won’t be able to reuse the same hostname.  Instead you’ll end up seeing “rss-1”, “rss-2”, etc. in your list of tailscale machines.  To rememdy this, just use the tailscale dashboard to remove those machines, delete the “freshrss_tailscale_state” volume again and rerun the <code class="language-plaintext highlighter-rouge">run</code> script to reset the tailscale hostname</p>

<h3 id="tailscale-isnt-showing-my-machine-or-it-isnt-connected">Tailscale isn’t showing my machine or it isn’t connected</h3>

<p>If you can’t see the rss machine, or it isn’t connected, run <code class="language-plaintext highlighter-rouge">podman ps -a</code> to get a list of all the containers, including stopped ones.  If you can see it has exited, you can run <code class="language-plaintext highlighter-rouge">podman logs freshrss_tailscale</code> to see what error messages it might have thrown.</p>

<h2 id="taking-it-further">Taking it further</h2>

<p>Now that we have the container up and running, there’s more we can do to automate!  Podman has a great Quadlets feature that will run this all via systemd for us, adding a lot of resiliency. We can add more envvars to our .env file to make our scripts more configurable.  We could write an <code class="language-plaintext highlighter-rouge">up</code> script to mimic a <code class="language-plaintext highlighter-rouge">docker compose up</code> style command.  We could automate backing up FreshRSS by copying out the volume data somewhere.  We could automate upgrades using <code class="language-plaintext highlighter-rouge">podman rename</code> before spinning up a new instance and inserting it where the old one was.</p>

<p>We could enjoy our FreshRSS instance and download a reader like <a href="https://f-droid.org/en/packages/com.capyreader.app/">Capy Reader</a> to use it on our phone.</p>

<p>There are so many possibilities!  I hope you enjoyed this tutorial.</p>]]></content><author><name>Ben</name></author><summary type="html"><![CDATA[Adding Tailscale to your self hosted containers lets you easily set up those services with a secure, fully qualified domain name.]]></summary></entry><entry><title type="html">Note Taking: Obsidian and Fastmail</title><link href="https://benkenawell.com/2025/05/28/obsidian-and-fastmail.html" rel="alternate" type="text/html" title="Note Taking: Obsidian and Fastmail" /><published>2025-05-28T00:00:00+00:00</published><updated>2025-05-28T00:00:00+00:00</updated><id>https://benkenawell.com/2025/05/28/obsidian-and-fastmail</id><content type="html" xml:base="https://benkenawell.com/2025/05/28/obsidian-and-fastmail.html"><![CDATA[<p>Everyone has a note taking app they prefer.  For almost 5 years now I’ve paid for <a href="https://standardnotes.com/">Standard Notes</a> <!--more--> to give me plain text notes synced across my devices.  Back then, I was much more bullish on encryption and privacy and this worked great cross platform.  It gave me basic searching and sorting functions.</p>

<h2 id="old-editor">Old Editor</h2>

<p>I never really liked their editors, even their Markdown or Spreadsheet ones.  They didn’t feel good, especially on mobile.  Like they were making as many as possible, but not refining any of them.  So I always used the plaintext editor, even when typing in Markdown.</p>

<p>I didn’t want to fall into the hamster wheel trap of the next new note taking app.  Standard Notes stored and synced notes like I needed. Migrating notes to a new experience every couple of months would be such a pain, especially into a new format. And just to try some other editor that will probably charge me money to sync.</p>

<p>A lot has changed in the time since I started using Standard Notes. I’ve gotten less strict about a E2E solution for everything.  I favor open standards over the encrypted stuff now, in many cases.  I switched email providers from <a href="https://proton.me/mail">Proton Mail</a> to <a href="https://www.fastmail.com/">Fastmail</a> (again, a big UX choice).  I’ve learned much more about self hosting and have gotten more comfortable with how to put things on the web (including this blog).  And it has been 4 or 5 years since I’ve changed notes apps, Standard Notes hadn’t changed much in that time.</p>

<h2 id="new-editor">New Editor</h2>

<p>I had heard about  <a href="https://obsidian.md/">Obsidian</a> on a couple podcasts, but mostly ignored it.  I was setting up a new laptop at work, where I used the free version of Standard Notes, and decided this would be a good time to give it a try.  The promise of all my notes being Markdown files on my computer was super cool.  I figured I could script against them myself even!  I don’t need any syncing at work, it’s just the one computer.</p>

<p>And wow, I was blown away!  A few weeks in and I’m still amazed at how open Obsidian is, how responsive Obsidian is, and how many great features they’ve packed in.  I’ve even taken a look at <a href="https://codemirror.net/">CodeMirror</a>, and might use it in future projects.</p>

<p>I liked it so much at work I decided to download it on my Android phone and see if it could live up to my hype there.  And it nailed it!  Really just wonderful.  The <a href="https://help.obsidian.md/Extending+Obsidian/Obsidian+URI">obsidian links</a> make it easy to extends via <a href="https://www.macrodroid.com/">macros</a> and the markdown looks great.</p>

<h2 id="syncing-solution">Syncing Solution</h2>

<p>So I also downloaded it on <a href="/2025/04/19/new-laptop-framework.html">my laptop</a> too.  But now I needed a way to sync between my phone and computer.  Since I’ve begun to self host other things, I started to look into that.  But they all seemed like they might be a lot of work, I didn’t want something that would be more maintenance than useful.</p>

<p>I looked into the official <a href="https://obsidian.md/sync">Obsidian Sync</a>, which costs about the same amount as my Standard Notes subscription at $4/mo.  But by now my imagination was going wild with embeddable photos and who knows what else!  The official plan only gave me 1GB and sharing with my wife would be another $4/mo for her.</p>

<p>As I was looking at the possibilities, I stumbled into WebDAV as an option via the <a href="https://remotelysave.com/">RemotelySave plugin</a> .  My experience with the DAV protocols are somewhat mixed.  They’re powerful but tough to use.  Fortunately, my Fastmail email has <a href="https://www.fastmail.help/hc/en-us/articles/1500000277882-Remote-file-access">a full WebDAV server</a>.  You can even view the files in their app!  And my account comes with 10GB of storage, plus I could have my wife just log in with a different app password.</p>

<h2 id="migration">Migration</h2>

<p>Coming back to this complaint from above, I haven’t migrated much yet. Standard Notes will give me my notes in a .txt, and Obsidian will automatically recognize those if I change them to a .md extension.  Some of the titles might be mangled and I might lose some metadata.  But I had very little metadata, and titles can be repaired.</p>

<p>I’ve realized that maybe most of my notes are fairly <a href="https://notes.andymatuschak.org/Evergreen_notes?stackedNotes=zKGjQtsTKgscAoq271ZzKqw">transient</a> anyway. The posts on this blog have a little more staying power, I think.  So this is a work in progress, who knows how much I’ll end up moving over.</p>
<h2 id="conclusion">Conclusion</h2>

<p>Time will tell how long this version of note taking lasts for me.  But Obsidian is an amazing product and RemotelySave with Fastmail makes it so easy to sync that for now, this is definitely a winner.</p>]]></content><author><name>Ben</name></author><summary type="html"><![CDATA[Everyone has a note taking app they prefer. For almost 5 years now I’ve paid for Standard Notes]]></summary></entry><entry><title type="html">Defining Part Stack Developer</title><link href="https://benkenawell.com/2025/05/10/defining-part-stack-developer.html" rel="alternate" type="text/html" title="Defining Part Stack Developer" /><published>2025-05-10T00:00:00+00:00</published><updated>2025-05-10T00:00:00+00:00</updated><id>https://benkenawell.com/2025/05/10/defining-part-stack-developer</id><content type="html" xml:base="https://benkenawell.com/2025/05/10/defining-part-stack-developer.html"><![CDATA[<p>On <a href="https://www.linkedin.com/in/benjamin-kenawell/">my LinkedIn page</a>, my title is “Part Stack Web Dev.” It came from an article I read or podcast I listened to that I haven’t been able to find again, so I wanted to write a bit about it. <!--more--></p>

<p>I work at a start up small enough that there are no defined levels or strict job titles, so when I heard about the tongue in cheek “part stack” I decided it would be a fun title to give myself.  It’s supposed to be a joke on “Full stack Developer” job titles where they mean “front end web pages/SPA and backend application servers,” which feels like a majority of job postings.</p>

<h2 id="part-stack">Part Stack</h2>

<p>But there’s also a whole stack of computers and services to serve your application, why aren’t <em>those</em> considered part of a full stack engineer’s job?  I think of that as an Operations role, so maybe those pieces fall under a “DevOps Engineer” title, which smashes the “developer” and “operations” roles together.  Maybe all together that’s a “Full Stack DevOps Engineer”?</p>

<p>Of course, all of these vary by company and HR department. And luckily there’s a job description and interview process where companies and prospective employees can get a better understanding of what a role might entail.  We know what all of these titles mean, to a certain degree, so what is a Part Stack Engineer?</p>

<p>I work on the <strong>part of the stack</strong> that drives your business and serves your customers.  I can take a website/web application from Hello World to IPO.  I can launch a site if you’ll give me a bare metal server or VPS to deploy it on.  I can build an application server capable of scaling to meet your demand, making it secure and observable.  I can write HTML/CSS/JS to make your website look and feel like the brand you want to present.</p>

<h2 id="web-dev">Web Dev</h2>

<p>If you noticed that everything on that list is related to the web, you might also remember my job title includes “Web Dev.” I would happily call myself “Web Engineer,” I don’t mean to debate developer vs engineer here.  Web Dev just rolls off the tongue better than Web Eng.  The “web” part is important to me.  We live in a world where you want your data to be accessible on your laptop, and your phone, and maybe a work computer, tablet, etc.  It has to live somewhere, so a server (<a href="https://once.com/">yours</a> or mine) is a good place to live.  HTTP/WWW is definitely the dominant paradigm for accessing those kinds of things, everybody has a web browser.</p>

<p>Having a web server <a href="https://htmx.org/essays/hateoas/">also deliver the markup</a> makes the whole thing so much more straightforward to develop; you’ll save 100s of hours a year of developer time.  There are <a href="https://native.hotwired.dev/">similar</a> <a href="https://hyperview.org/">projects</a> for mobile apps. I can deliver a mobile ready website, but an app is one of my weakest areas.  So I stuck the “Web” part in my self-proclaimed title because my works revolves around the web.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The web encompasses a lot of modern consumer computing. Any part of that stack is something I am interested in. At this point, 6 years into my professional programming career, I think I have at least a passing familiarity with most of it. And plenty of experience to plan, build, deploy, and operate a web application.  So I call myself a “Part Stack Web Dev,” a title I think encompasses a majority of my experience and interests.</p>

<hr />

<p>PS, if you think you know where “Part Stack” came from, I’d <a href="https://mastodon.social/@benkenawell">love to know</a> so I can link to it from here.</p>]]></content><author><name>Ben</name></author><summary type="html"><![CDATA[On my LinkedIn page, my title is “Part Stack Web Dev.” It came from an article I read or podcast I listened to that I haven’t been able to find again, so I wanted to write a bit about it.]]></summary></entry></feed>