<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Michael Stapelbergs Website</title>
  <link href="https://michael.stapelberg.ch/feed.xml" rel="self"/>
  <link href="https://michael.stapelberg.ch/"/>
  <id>https://michael.stapelberg.ch/</id>
  <generator>Hugo -- gohugo.io</generator>
  <entry>
    <title type="html"><![CDATA[How my minimal, memory-safe Go rsync steers clear of vulnerabilities]]></title>
    <link href="https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/"/>
    <id>https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/</id>
    <published>2026-05-24T16:20:22+02:00</published>
    <content type="html"><![CDATA[<p>Back in January 2025, multiple different security researchers published a total
of <a href="https://www.openwall.com/lists/oss-security/2025/01/14/3">6 security vulnerabilities in
rsync</a>, some of which
allow arbitrary code execution and file leaks, so naturally I was wondering
whether/how my <a href="https://github.com/gokrazy/rsync">gokrazy/rsync</a> implementation
was affected. Did implementing my own (compatible, but minimal) rsync in Go, a
modern and memory-safe programming language, really rule out entire classes of
security vulnerabilities?</p>
<p>This deep dive article was in the making since January 2025, but was delayed
because we uncovered more unpublished vulnerabilities in the process! The
“Security Vulnerabilities” section now covers all 12 vulnerabilities from the
January 2025 batch and the May 2026 batch.</p>
<p><strong>If you are running (upstream, samba)
<a href="https://github.com/RsyncProject/rsync">rsync</a> in production, upgrade to version
3.4.3 or newer.</strong></p>
<p>If you are running <a href="https://github.com/gokrazy/rsync">gokrazy/rsync</a> in
production, upgrade to version v0.3.3 or newer.</p>
<p>Feel free to skip over the nitty-gritty security issue details and jump directly to:</p>
<ul>
<li>The <a href="#go-verdict">verdict on whether using Go has helped</a>.</li>
<li>The <a href="#gokrazy-rsync-verdict">verdict on whether a minimal re-implementation like gokrazy/rsync helps</a>.</li>
<li>My <a href="#openrsync">comparison with OpenBSD’s <code>openrsync</code></a> (written in C).</li>
<li><a href="#defenseindepth">Defense in depth mechanisms</a> one can use on Linux.</li>
<li>The <a href="#conclusion">conclusion</a>.</li>
</ul>
<h2 id="context-my-own-rsync">Context: My own rsync</h2>
<p>For context, I blogged about <a href="/posts/2022-06-18-rsync-overview/">rsync, how I use it, and how it
works</a> back in June 2022. See also <a href="/posts/tags/rsync/">all posts
tagged “rsync”</a>.</p>
<p>The original motivation for writing my own rsync (back then only a server, today
all directions are supported) was to provide the software packages of <a href="https://distr1.org/">distri,
my Linux distribution research project for fast package
management</a>, which I wanted to host on
<a href="https://router7.org/">router7</a>, my small home Linux+Go internet router, which
in turn is built on <a href="https://gokrazy.org/">gokrazy</a>, my Go appliance platform.</p>
<p>I am still running multiple gokrazy/rsync servers for this original purpose, and
also many others! Having rsync available as a primitive (that you can link into
your Go programs!) is really nice.</p>
<h2 id="vulns">Security Vulnerabilities</h2>
<p>This article covers the following security vulnerabilities:</p>
<ul>
<li>CVE-2024-12084 to 12088 <a href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">(original report)</a></li>
<li>CVE-2024-12747 (discovered separately by Aleksei Gorban &ldquo;loqpa&rdquo;)</li>
<li>CVE-2026-29518 (discovered by Damien Neil and myself! and independently by <a href="https://nullx3d.com/">Nullx3D</a>)</li>
<li>CVE-2026-43617 to 43620</li>
<li>CVE-2026-45232</li>
</ul>
<p>The first batch of the vulnerabilities above was announced on the <a href="https://www.openwall.com/lists/oss-security/2025/01/14/3">oss-security
mailing list</a>, but
note that the original report has more detail compared to the oss-security
summaries!</p>
<p>The later vulnerabilities were announced via <a href="https://github.com/RsyncProject/rsync/security/advisories">GitHub Security Advisories on the
rsync project</a>.</p>
<h3 id="jan2025">January 2025 batch</h3>
<h4 id="cve-2024-12084">CVE-2024-12084: Heap Buffer Overflow (9.8)</h4>
<p><strong>Summary:</strong></p>
<ul>
<li>rsync performed insufficient validation: It read the (attacker-controlled)
checksum length from the network and compared the length against
<code>MAX_DIGEST_LEN</code>.</li>
<li>However, rsync’s data structures always declared a 16 byte buffer: <code>char sum2[SUM_LENGTH]</code>
<ul>
<li><code>SUM_LENGTH</code> is always 16 (bytes), which is sufficient to hold an
<a href="https://en.wikipedia.org/wiki/MD4">MD4</a> or
<a href="https://en.wikipedia.org/wiki/MD5">MD5</a> checksum.</li>
<li><code>MAX_DIGEST_LEN</code> used to be 16 (bytes), but can be larger when rsync is
compiled with SHA256 or SHA512 checksum support.</li>
</ul>
</li>
<li>Hence, the bounds check was ineffective! An attacker could write out of bounds.</li>
<li>This issue was introduced with <a href="https://github.com/RsyncProject/rsync/commit/ae16850dc58e884eb9f5cb7f772342b2db28f471">commit <code>ae16850</code> in September
2022</a>,
which added SHA256/SHA512 checksum support.</li>
</ul>
<details>
<summary>Click to expand the <strong>full description of the improper checksum length validation</strong> (quoting the <a href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google Security
report</a>)</summary>
<blockquote>
<p>When the checksums are read by the daemon, two different checksums are read:</p>
<ol>
<li>A 32-bit Adler-CRC32 Checksum</li>
<li>A digest of the file chunk. The digest algorithm is determined at the beginning of the protocol negotiation.
The corresponding code can be seen below:
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/sender.c#L96-L100">sender.c</a>:</li>
</ol>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span>s<span style="color:#666">-&gt;</span>sums <span style="color:#666">=</span> <span style="color:#06287e">new_array</span>(<span style="color:#007020;font-weight:bold">struct</span> sum_buf, s<span style="color:#666">-&gt;</span>count);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">for</span> (i <span style="color:#666">=</span> <span style="color:#40a070">0</span>; i <span style="color:#666">&lt;</span> s<span style="color:#666">-&gt;</span>count; i<span style="color:#666">++</span>) {
</span></span><span style="display:flex;"><span>    s<span style="color:#666">-&gt;</span>sums[i].sum1 <span style="color:#666">=</span> <span style="color:#06287e">read_int</span>(f);
</span></span><span style="display:flex;"><span>    <span style="color:#06287e">read_buf</span>(f, s<span style="color:#666">-&gt;</span>sums[i].sum2, s<span style="color:#666">-&gt;</span>s2length);
</span></span></code></pre></div><p>Most importantly, note that <code>sum2</code> field is filled with <code>s-&gt;s2length</code> bytes. <code>sum2</code> always has a size of 16:
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/rsync.h#L955-L962">rsync.h</a></p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020">#define SUM_LENGTH 16
</span></span></span><span style="display:flex;"><span><span style="color:#007020"></span><span style="color:#60a0b0;font-style:italic">// …
</span></span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"></span><span style="color:#007020;font-weight:bold">struct</span> sum_buf {
</span></span><span style="display:flex;"><span>    OFF_T offset;           <span style="color:#60a0b0;font-style:italic">/**&lt; offset in file of this chunk */</span>
</span></span><span style="display:flex;"><span>    int32 len;              <span style="color:#60a0b0;font-style:italic">/**&lt; length of chunk of file */</span>
</span></span><span style="display:flex;"><span>    uint32 sum1;            <span style="color:#60a0b0;font-style:italic">/**&lt; simple checksum */</span>
</span></span><span style="display:flex;"><span>    int32 chain;            <span style="color:#60a0b0;font-style:italic">/**&lt; next hash-table collision */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#902000">short</span> flags;            <span style="color:#60a0b0;font-style:italic">/**&lt; flag bits */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#902000">char</span> sum2[SUM_LENGTH];  <span style="color:#60a0b0;font-style:italic">/**&lt; checksum  */</span>
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p><code>s2length</code> is an attacker-controlled value and can have a value up to <code>MAX_DIGEST_LEN</code> bytes, as the next snipper shows:</p>
<p><a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/io.c#L1979-L1984">io.c</a></p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span>sum<span style="color:#666">-&gt;</span>s2length <span style="color:#666">=</span> protocol_version <span style="color:#666">&lt;</span> <span style="color:#40a070">27</span> <span style="color:#666">?</span> <span style="color:#002070;font-weight:bold">csum_length</span> : (<span style="color:#902000">int</span>)<span style="color:#06287e">read_int</span>(f);
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (sum<span style="color:#666">-&gt;</span>s2length <span style="color:#666">&lt;</span> <span style="color:#40a070">0</span> <span style="color:#666">||</span> sum<span style="color:#666">-&gt;</span>s2length <span style="color:#666">&gt;</span> MAX_DIGEST_LEN) {
</span></span><span style="display:flex;"><span>    <span style="color:#06287e">rprintf</span>(FERROR, <span style="color:#4070a0">&#34;Invalid checksum length %d [%s]</span><span style="color:#4070a0;font-weight:bold">\n</span><span style="color:#4070a0">&#34;</span>,
</span></span><span style="display:flex;"><span>            sum<span style="color:#666">-&gt;</span>s2length, <span style="color:#06287e">who_am_i</span>());
</span></span><span style="display:flex;"><span>    <span style="color:#06287e">exit_cleanup</span>(RERR_PROTOCOL);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The problem here is that <code>MAX_DIGEST_LEN</code> can be larger than 16 bytes, depending on the digest support the binary was compiled with:</p>
<p><a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/lib/md-defines.h#L11-L21">md-defines.h</a></p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020">#define MD4_DIGEST_LEN 16
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#define MD5_DIGEST_LEN 16
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#if defined SHA512_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span><span style="color:#007020"></span>  <span style="color:#007020">#define MAX_DIGEST_LEN SHA512_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#elif defined SHA256_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span><span style="color:#007020"></span>  <span style="color:#007020">#define MAX_DIGEST_LEN SHA256_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#elif defined SHA_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span><span style="color:#007020"></span>  <span style="color:#007020">#define MAX_DIGEST_LEN SHA_DIGEST_LENGTH
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#else
</span></span></span><span style="display:flex;"><span><span style="color:#007020"></span>  <span style="color:#007020">#define MAX_DIGEST_LEN MD5_DIGEST_LEN </span><span style="color:#60a0b0;font-style:italic">/* 16 bytes */</span><span style="color:#007020">
</span></span></span><span style="display:flex;"><span><span style="color:#007020">#endif
</span></span></span></code></pre></div><p><code>SHA256</code> support is common and sets the <code>MAX_DIGEST_LENGTH</code> value to 64. As a
result, an attacker can write up to 48 bytes past the <code>sum2</code> buffer limit.</p>
</blockquote>
</details>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/0902b52f6687b1f7952422080d50b93108742e53">upstream fix for
CVE-2024-12084</a>
changes the <code>sum2</code> field to a dynamically-allocated <code>sum2_array</code> field, which is
allocated with <code>xfer_sum_len</code> length, and fixes the bounds check to check
against the <code>xfer_sum_len</code> (checksum length for this transfer’s algorithm).</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes: Missing or incorrect bounds checks will not result in a heap buffer
overflow in Go! Instead, attempting to write out of bounds will result in a
panic because the Go runtime performs bounds checks.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync also had insufficient validation! Our issue was different, though:
It wasn’t size confusion, we just were not doing any validation of the sum
header at all — oops!</p>
<p>We can confirm that the Go runtime’s bounds check triggers
on an attempt to <a href="https://github.com/gokrazy/rsync/blob/3a9d1ec136e049b2bffc9db15230eb70308ac90d/rsyncd/sender.go#L136">write out of
bounds</a>
by changing the code like so and running the tests:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#000080;font-weight:bold">diff --git i/types.go w/types.go
</span></span></span><span style="display:flex;"><span><span style="color:#000080;font-weight:bold">index 5601697..899fcb8 100644
</span></span></span><span style="display:flex;"><span><span style="color:#000080;font-weight:bold"></span><span style="color:#a00000">--- i/types.go
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+++ w/types.go
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span><span style="color:#800080;font-weight:bold">@@ -59,7 +59,7 @@ func (sh *SumHead) WriteTo(c *rsyncwire.Conn) error {
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span> 	var buf rsyncwire.Buffer
</span></span><span style="display:flex;"><span> 	buf.WriteInt32(sh.ChecksumCount)
</span></span><span style="display:flex;"><span> 	buf.WriteInt32(sh.BlockLength)
</span></span><span style="display:flex;"><span><span style="color:#a00000">-	buf.WriteInt32(sh.ChecksumLength)
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+	buf.WriteInt32(512 /*sh.ChecksumLength*/)
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> 	buf.WriteInt32(sh.RemainderLength)
</span></span><span style="display:flex;"><span> 	return c.WriteString(buf.String())
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>As expected, the Go runtime panics with the following message:</p>
<div style="font-size: 80%">
<pre tabindex="0"><code>panic: runtime error: slice bounds out of range [:512] with length 16

goroutine 277 [running]:
github.com/gokrazy/rsync/rsyncd.(*sendTransfer).receiveSums(0xc0000d7b68)
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/sender.go:136 +0x339
github.com/gokrazy/rsync/rsyncd.(*sendTransfer).sendFiles(0xc0000d7b68, 0xc000120820)
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/sender.go:46 +0x134
github.com/gokrazy/rsync/rsyncd.(*Server).handleConnSender(0xc000476090, {{0x95ed9b, 0x7}, {0xc000426810, 0x2a}, {0x0, 0x0, 0x0}}, {0xa2a120, 0xc0000b2ba0}, ...)
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:397 +0x26a
github.com/gokrazy/rsync/rsyncd.(*Server).HandleConn(0xc000476090, {{0x95ed9b, 0x7}, {0xc000426810, 0x2a}, {0x0, 0x0, 0x0}}, {0xa2a120, 0xc0000b2ba0}, ...)
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:351 +0x37a
github.com/gokrazy/rsync/rsyncd.(*Server).HandleDaemonConn(0xc000476090, {0x94db80?, 0xc00018a040?}, {0x7fd15838b118, 0xc000428028}, {0xa2bd90, 0xc0002303c0})
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:307 +0xdbb
github.com/gokrazy/rsync/rsyncd.(*Server).Serve.func2()
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:450 +0xaf
created by github.com/gokrazy/rsync/rsyncd.(*Server).Serve in goroutine 260
	/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:448 +0xd2
</code></pre></div>
<p>Of course, crashing the entire server is not the best failure mode, so I <a href="https://github.com/gokrazy/rsync/commit/178216f10f2e05fd74bf865f2d0725fce4f907cd">added
the missing bounds checking to turn the panic into an
error</a>.</p>
<h4 id="cve-2024-12085">CVE-2024-12085: Stack Info Leak Defeats ASLR (7.5)</h4>
<p><strong>Summary:</strong></p>
<p>Because of the same lack of validation as in the previous CVE-2024-12084
vulnerability, an attacker could select a checksum algorithm with short
checksums (e.g. <code>xxhash64</code> with 8 byte checksums), but then claim they were
sending longer checksums (e.g. 9 bytes), making the victim leak one byte of
uninitialized stack content in the response.</p>
<p>Leaking one byte of stack content may seem benign, but as the <a
href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google
Security report</a> puts it:</p>
<blockquote>
<p>The first pair of vulnerabilities are a Heap Buffer Overflow and an Info Leak.
When combined, they allow a client to execute arbitrary code on the machine a
Rsync server is running on. The client only requires anonymous read-access to
the server.</p>
</blockquote>
<details>
<summary>Click to expand the <strong>full description of the info leak</strong>
(quoting the <a
href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google
Security report</a>)</summary>
<blockquote>
<p>The daemon matches checksums of chunks the client sent to the server against
the local file contents in
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/match.c#L140-L145"><code>hash_search()</code></a>. Part
of the function prologue is to allocate a buffer on the stack of
<code>MAX_DIGEST_LEN</code> bytes:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">static</span> <span style="color:#902000">void</span> <span style="color:#06287e">hash_search</span>(<span style="color:#902000">int</span> f, <span style="color:#007020;font-weight:bold">struct</span> sum_struct <span style="color:#666">*</span>s,
</span></span><span style="display:flex;"><span>                        <span style="color:#007020;font-weight:bold">struct</span> map_struct <span style="color:#666">*</span>buf, OFF_T len)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    OFF_T offset, aligned_offset, end;
</span></span><span style="display:flex;"><span>    int32 k, want_i, aligned_i, backup;
</span></span><span style="display:flex;"><span>    <span style="color:#902000">char</span> sum2[MAX_DIGEST_LEN];
</span></span></code></pre></div><p>The daemon then iterates over the checksums the client sent and generates a
digest for each of the chunks and compares them to the remote digest:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (<span style="color:#666">!</span>done_csum2) {
</span></span><span style="display:flex;"><span>    map <span style="color:#666">=</span> (schar <span style="color:#666">*</span>)<span style="color:#06287e">map_ptr</span>(buf, offset, l);
</span></span><span style="display:flex;"><span>    <span style="color:#06287e">get_checksum2</span>((<span style="color:#902000">char</span> <span style="color:#666">*</span>)map, l, sum2);
</span></span><span style="display:flex;"><span>    done_csum2 <span style="color:#666">=</span> <span style="color:#40a070">1</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (<span style="color:#06287e">memcmp</span>(sum2, s<span style="color:#666">-&gt;</span>sums[i].sum2, s<span style="color:#666">-&gt;</span>s2length) <span style="color:#666">!=</span> <span style="color:#40a070">0</span>) {
</span></span><span style="display:flex;"><span>    false_alarms<span style="color:#666">++</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">continue</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Notably, the number of bytes that are compared again are <code>s-&gt;s2length</code>
bytes. In this case, the comparison does not go out of bounds since
<code>s-&gt;s2length</code> can be a maximum of <code>MAX_DIGEST_LEN</code>.</p>
<p>However, the local <code>sum2</code> buffer, not to be confused with the
attacker-controlled <code>s-&gt;sums[i].sum2</code>, is a buffer on the stack that is not
cleared and thus contains uninitialized stack contents.</p>
<p>A malicious client can send a (known) <code>xxhash64</code> checksum for a given chunk of
a file, which leads to the daemon writing 8 bytes to the stack buffer
<code>sum2</code>. The attacker can then set <code>s-&gt;s2length</code> to 9 bytes. The result of such
a setup would be that the first 8 bytes match and an attacker-controlled 9th
byte is compared with an unknown value of uninitialized stack data.</p>
<p>An attacker can divide a file into 255 chunks and as a result leak one byte
per file download. An attacker can incrementally repeat the process, either in
the same connection or by resetting the connection.</p>
<p>As a result, they can leak <code>MAX_DIGEST_LEN - 8</code> bytes of uninitialized stack
data, which can contain pointers to Heap objects, Stack cookies, local
variables and pointers to global variables and return pointers. With those
pointers they can defeat ASLR.</p>
</blockquote>
</details>
<p><strong>Upstream fix:</strong></p>
<p>There are two relevant upstream fixes:</p>
<ul>
<li>The <a href="https://github.com/RsyncProject/rsync/commit/0902b52f6687b1f7952422080d50b93108742e53">“Some checksum buffer
fixes”</a>
commit prevents this attack because the attacker-controlled <code>s-&gt;s2length</code> can
no longer be larger than the transfer’s checksum length.</li>
<li>The <a href="https://github.com/RsyncProject/rsync/commit/589b0691e59f761ccb05ddb8e1124991440db2c7">“prevent information leak off the
stack”</a>
commit initializes the <code>sum2</code> memory to zero, thereby making any stack leak
through <code>sum2</code> impossible.</li>
</ul>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes: By design, Go initializes all variables to the zero value. Go programmers
do not need to remember to explicitly initialize variables.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not affected by this vulnerability: Variables are always
initialized in Go.</p>
<p>Additionally, selecting checksums other than MD4 was only introduced in protocol
version 30 (gokrazy/rsync implements protocol version 27).</p>
<!--
Go does not do ASLR (https://github.com/golang/go/issues/27583#issuecomment-1002725264).
Not sure if it makes sense to mention that here.
-->
<h4 id="cve-2024-12087">CVE-2024-12087: Path Traversal using Symlinks (7.5)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google Security
report</a>)</p>
<blockquote>
<p>When the syncing of symbolic links is enabled, either through the <code>-l</code> or <code>-a</code>
(<code>--archive</code>) flags, a malicious server can make the client write arbitrary
files outside of the destination directory.  A malicious server can send the
client a file list such as:</p>
<pre tabindex="0"><code>symlink -&gt; /arbitrary/directory
symlink/poc.txt
</code></pre><p>Symbolic links, by default, can be absolute or contain characters such as <code>../../</code>.</p>
<p>In practice, the client validates the file list and when it sees the
<code>symlink/poc.txt</code> entry, it will look for a directory called <code>symlink</code>,
otherwise it will error out. If the server sends <code>symlink</code> as [both, a
directory and a symbolic link], [the client] will only keep the directory
entry, thus the attack requires some more details to work.</p>
<p>In <code>inc_recurse</code> mode, which the server can enable for the client, the server
sends the client multiple file lists. The deduplication of the entries happens
on a per-file-list basis. As a result, a malicious server can send a client
multiple file lists, where:</p>
<pre tabindex="0"><code># file list 1:
.
./symlink (directory)
./symlink/poc.txt (regular file)

# file list 2:
./symlink -&gt; /arbitrary/path (symlink)
</code></pre><p>As a result, the <code>symlink</code> directory is created first and <code>symlink/poc.txt</code> is
considered a valid entry in the file list. Then, the attacker changes the type
of <code>symlink</code> to a symbolic link.</p>
<p>When the server then instructs the client to create the <code>symlink/poc.txt</code>
file, it will follow the symbolic link and thus files can be created outside
of the destination directory.</p>
</blockquote>
<p><strong>Can Go help prevent this?</strong></p>
<p>No. This vulnerability is caused by a logic error: when multiple file lists are
used, the merged file list needs to be re-verified.</p>
<p>But see <a href="#goosroot">Defense in depth: Go’s <code>os.Root</code></a></p>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/688f5c379a433038bde36897a156d589be373a98">upstream fix for
CVE-2024-12087</a>
adds the missing validation.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not affected by this vulnerability: gokrazy/rsync does not
implement the incremental recursion mode (<code>--inc-recursive</code>).</p>
<p>The trade-off here is implementation complexity vs. resource usage: the
incremental recursion mode allows working with the file set in a “windowed” way,
as opposed to having to scan the entire file set before any transfer can
begin. See also my <a href="/posts/2022-07-02-rsync-how-does-it-work/">How does rsync
work?</a> blog post.</p>
<h4 id="cve-2024-12088">CVE-2024-12088: Bypass for <code>--safe-links</code> (7.5)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google Security
report</a>)</p>
<blockquote>
<p>The <code>--safe-links</code> CLI flag makes the client validate any symbolic links it
receives from the server. The desired behavior is that symbolic links target
can only be 1) relative to the destination directory and 2) never point
outside of the destination directory.</p>
<p>The <code>unsafe_symlink()</code> function is responsible for validating these symbolic
links. The function calculates the traversal depth of a symbolic link target,
relative to its position within the destination directory.</p>
<p>As an example, the following symbolic link is considered unsafe:</p>
<p><code>{DESTINATION}/foo -&gt; ../../</code></p>
<p>As it points outside the destination directory. On the other hand, the
following symbolic link is considered safe as it still points within the
destination directory:</p>
<p><code>{DESTINATION}/foo -&gt; a/b/c/d/e/f/../../</code></p>
<p>This function can be bypassed as it does not consider if the destination of a
symbolic link contains other symbolic links in the path. For example, take the
following two symbolic links:</p>
<p><code>{DESTINATION}/a -&gt; .</code>
<code>{DESTINATION}/foo -&gt; a/a/a/a/a/a/../../</code></p>
<p>In this case, foo would actually point outside the destination
directory. However, the <code>unsafe_symlink()</code> function assumes that <code>a/</code> is a
directory and that the symbolic link is safe.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/407c71c7ce562137230e8ba19149c81ccc47c387">upstream fix for
CVE-2024-12088</a>
makes <code>unsafe_symlink()</code> stricter by not allowing <code>../</code> anywhere within the
path, except at the very beginning.</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>No. This vulnerability is caused by a logic error: the validation function was
incorrect. We could have implemented that same bug.</p>
<p>But see <a href="#goosroot">Defense in depth: Go’s <code>os.Root</code></a></p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not vulnerable: The <code>--safe-links</code> feature is not yet
implemented in gokrazy/rsync.</p>
<h4 id="cve-2024-12086">CVE-2024-12086: Arbitrary File Leak (6.8)</h4>
<p><strong>Summary:</strong></p>
<p>The rsync receiver (in client mode) did not sanitize file names provided by the
rsync sender, or otherwise prevent opening files outside the destination tree. A
malicious sender could instruct a receiver to compare checksums of arbitrary
files outside the destination tree. By observing the receiver’s reaction to a
provided one-byte checksum, a malicious sender can leak arbitrary files.</p>
<details>
<summary>Click to expand the <strong>full description of the file leak</strong>
(quoting the <a
href="https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj">Google
Security report</a>)</summary>
<blockquote>
<p>When a client connects to a malicious server the server is able to leak the
contents of an arbitrary file on the client’s machine.  In
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/rsync.c#L322C5-L322C23"><code>read_ndx_and_attrs()</code></a>
the client will read <code>fnamecmp</code> type as well as the <code>xname</code> from the server if
the server sets the appropriate flags. The flag <code>sanitize_paths</code> will not be
set for the client.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (iflags <span style="color:#666">&amp;</span> ITEM_BASIS_TYPE_FOLLOWS)
</span></span><span style="display:flex;"><span>    fnamecmp_type <span style="color:#666">=</span> <span style="color:#06287e">read_byte</span>(f_in);
</span></span><span style="display:flex;"><span><span style="color:#666">*</span>type_ptr <span style="color:#666">=</span> fnamecmp_type;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (iflags <span style="color:#666">&amp;</span> ITEM_XNAME_FOLLOWS) {
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> ((len <span style="color:#666">=</span> <span style="color:#06287e">read_vstring</span>(f_in, xname, MAXPATHLEN)) <span style="color:#666">&lt;</span> <span style="color:#40a070">0</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#06287e">exit_cleanup</span>(RERR_PROTOCOL);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> (sanitize_paths) { <span style="color:#60a0b0;font-style:italic">/* not enabled when client receives */</span>
</span></span><span style="display:flex;"><span>        <span style="color:#06287e">sanitize_path</span>(xname, xname, <span style="color:#4070a0">&#34;&#34;</span>, <span style="color:#40a070">0</span>, SP_DEFAULT);
</span></span><span style="display:flex;"><span>        len <span style="color:#666">=</span> <span style="color:#06287e">strlen</span>(buf);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>} <span style="color:#007020;font-weight:bold">else</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#666">*</span>buf <span style="color:#666">=</span> <span style="color:#4070a0">&#39;\0&#39;</span>;
</span></span><span style="display:flex;"><span>    len <span style="color:#666">=</span> <span style="color:#666">-</span><span style="color:#40a070">1</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#666">*</span>len_ptr <span style="color:#666">=</span> len;
</span></span></code></pre></div><p>The caller
(<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/receiver.c#L521"><code>recv_files()</code></a>)
then uses the server provided values to determine a file to compare the
incoming data with.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">case</span> <span style="color:#002070;font-weight:bold">FNAMECMP_FUZZY</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> (file<span style="color:#666">-&gt;</span>dirname) {
</span></span><span style="display:flex;"><span>        <span style="color:#06287e">pathjoin</span>(fnamecmpbuf, <span style="color:#007020;font-weight:bold">sizeof</span> fnamecmpbuf, file<span style="color:#666">-&gt;</span>dirname, xname);
</span></span><span style="display:flex;"><span>        fnamecmp <span style="color:#666">=</span> fnamecmpbuf;
</span></span><span style="display:flex;"><span>    } <span style="color:#007020;font-weight:bold">else</span>
</span></span><span style="display:flex;"><span>        fnamecmp <span style="color:#666">=</span> xname;
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">break</span>;
</span></span><span style="display:flex;"><span><span style="">…</span>
</span></span><span style="display:flex;"><span>fd1 <span style="color:#666">=</span> <span style="color:#06287e">do_open</span>(fnamecmp, O_RDONLY, <span style="color:#40a070">0</span>);
</span></span></code></pre></div><p>In
<a href="https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/receiver.c#L368"><code>receive_data()</code></a>
the contents of the file specified by <code>xname</code> are copied into the destination
file. This can be achieved by the server sending a negative token.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">while</span> ((i <span style="color:#666">=</span> <span style="color:#06287e">recv_token</span>(f_in, <span style="color:#666">&amp;</span>data)) <span style="color:#666">!=</span> <span style="color:#40a070">0</span>) {
</span></span><span style="display:flex;"><span>..snip..
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> (i <span style="color:#666">&gt;</span> <span style="color:#40a070">0</span>) {
</span></span><span style="display:flex;"><span>..snip..
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>..snip..
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">if</span> (fd <span style="color:#666">!=</span> <span style="color:#666">-</span><span style="color:#40a070">1</span> <span style="color:#666">&amp;&amp;</span> map <span style="color:#666">&amp;&amp;</span> <span style="color:#06287e">write_file</span>(fd, <span style="color:#40a070">0</span>, offset, map, len) <span style="color:#666">!=</span> (<span style="color:#902000">int</span>)len)
</span></span></code></pre></div><p>The server sends a checksum to compare. If they don&rsquo;t match, a 0 is returned.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">if</span> (fd <span style="color:#666">!=</span> <span style="color:#666">-</span><span style="color:#40a070">1</span> <span style="color:#666">&amp;&amp;</span> <span style="color:#06287e">memcmp</span>(file_sum1, sender_file_sum, xfer_sum_len) <span style="color:#666">!=</span> <span style="color:#40a070">0</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">return</span> <span style="color:#40a070">0</span>;
</span></span></code></pre></div><p>When the return value is 0 the receiver will then send a <code>MSG_REDO</code> to the
generator. The generator will then write a message to the server.</p>
<p>The server can use this as a signal to determine if the checksum they sent was
correct. By starting off with a <code>blength</code> of 1 a malicious server is able to
determine the contents of the target file byte by byte.</p>
</blockquote>
</details>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/c35e28331f10ba6eba370611abd78bde32d54da7">upstream fix for
CVE-2024-12086</a>
prevents opening files outside the destination tree by verifying the
sender-provided path.</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes, Go offers an API to prevent this, see <a href="#goosroot">Defense in depth: Go’s
<code>os.Root</code></a>.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not vulnerable: the fuzzy matching feature was introduced with
rsync protocol version 29, but gokrazy/rsync implements protocol version 27.</p>
<h4 id="cve-2024-12747">CVE-2024-12747: Symlink Race Condition (5.6)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://access.redhat.com/security/cve/CVE-2024-12747">Red Hat Security
Advisory</a>)</p>
<blockquote>
<p>A flaw was found in rsync. This vulnerability arises from a race condition
during rsync&rsquo;s handling of symbolic links. Rsync&rsquo;s default behavior when
encountering symbolic links is to skip them. If an attacker replaced a regular
file with a symbolic link at the right time, it was possible to bypass the
default behavior and traverse symbolic links. Depending on the privileges of
the rsync process, an attacker could leak sensitive information, potentially
leading to privilege escalation.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/0590b09d9a34ae72741b91ec0708a820650198b0">upstream fix for
CVE-2024-12747</a>
changes <code>open()</code> calls in the rsync sender to use the <code>O_NOFOLLOW</code> option. The
paths are not expected to be symlinks at that point in the algorithm (symlinks
would be handled with <a href="https://manpages.debian.org/readlink.2"><code>readlink(2)</code></a>
).</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes, Go offers an API to prevent this, see <a href="#goosroot">Defense in depth: Go’s
<code>os.Root</code></a>.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync was vulnerable before <a href="https://github.com/gokrazy/rsync/commit/1b1fbf65b8ed696db464bf53445e8f9b97a37210">commit
<code>1b1fbf6</code></a>,
which introduces the same <code>O_NOFOLLOW</code> mitigation that upstream rsync uses.</p>
<details>
<summary>Click to expand the <strong>reproduction steps</strong> to trigger the
issue in <code>gokrazy/rsync</code></summary>
<p>To reproduce the issue, use the following steps:</p>
<ol>
<li>
<p>Check out gokrazy/rsync v0.2.7:</p>
<pre tabindex="0"><code>git clone https://github.com/gokrazy/rsync
cd rsync
git checkout v0.2.7
</code></pre></li>
<li>
<p>Patch the code as follows to undo the fix and execute the attack:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#000080;font-weight:bold">diff --git i/internal/nofollow/nofollow_unix.go w/internal/nofollow/nofollow_unix.go
</span></span></span><span style="display:flex;"><span><span style="color:#000080;font-weight:bold"></span><span style="color:#a00000">--- i/internal/nofollow/nofollow_unix.go
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+++ w/internal/nofollow/nofollow_unix.go
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span><span style="color:#800080;font-weight:bold">@@ -2,8 +2,6 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span>
</span></span><span style="display:flex;"><span> package nofollow
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a00000">-import &#34;golang.org/x/sys/unix&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span> // Maybe resolves to unix.O_NOFOLLOW on unix systems,
</span></span><span style="display:flex;"><span> // 0 on other platforms.
</span></span><span style="display:flex;"><span><span style="color:#a00000">-const Maybe = unix.O_NOFOLLOW
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+const Maybe = 0 // unix.O_NOFOLLOW
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>
</span></span><span style="display:flex;"><span><span style="color:#000080;font-weight:bold">diff --git i/internal/sender/do.go w/internal/sender/do.go
</span></span></span><span style="display:flex;"><span><span style="color:#000080;font-weight:bold"></span><span style="color:#a00000">--- i/internal/sender/do.go
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+++ w/internal/sender/do.go
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span><span style="color:#800080;font-weight:bold">@@ -2,6 +2,8 @@ package sender
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span>
</span></span><span style="display:flex;"><span> import (
</span></span><span style="display:flex;"><span> 	&#34;fmt&#34;
</span></span><span style="display:flex;"><span><span style="color:#00a000">+	&#34;os&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	&#34;path/filepath&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> 	&#34;sort&#34;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> 	&#34;github.com/gokrazy/rsync/internal/log&#34;
</span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -55,6 +57,15 @@ func (st *Transfer) Do(crd *rsyncwire.CountingReader, cwr *rsyncwire.CountingWri
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span> 		st.Logger.Printf(&#34;file list sent&#34;)
</span></span><span style="display:flex;"><span> 	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#00a000">+	// HACK: swap out the passwd file with a symlink to /etc/passwd
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	if err := os.Remove(filepath.Join(modPath, &#34;passwd&#34;)); err != nil {
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+		return nil, err
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	}
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	if err := os.Symlink(&#34;../passwd&#34;, filepath.Join(modPath, &#34;passwd&#34;)); err != nil {
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+		return nil, err
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	}
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	st.Logger.Printf(&#34;HACK: swapped passwd file for symlink&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> 	// Sort the file list. The client sorts, so we need to sort, too (in the
</span></span><span style="display:flex;"><span> 	// same way!), otherwise our indices do not match what the client will
</span></span><span style="display:flex;"><span> 	// request.
</span></span></code></pre></div></li>
</ol>
<p>Running the <code>TestReceiverSymlinkTraversal</code> test now shows that the server
traversed the symlink:</p>
<pre tabindex="0"><code>    receiver_test.go:371: unexpected file contents: diff (-want +got):
          bytes.Join({
        - 	&#34;benign&#34;,
        + 	&#34;secret&#34;,
          }, &#34;&#34;)
</code></pre></details>
<p><strong>A surprising discovery</strong></p>
<p>When I shared a draft of this article with Damien Neil, member of the Go
Security Team and the author of <a href="#goosroot">the traversal-resistant <code>os.Root</code>
API</a>, he pointed out:</p>
<blockquote>
<p>I believe the <a href="https://github.com/gokrazy/rsync/commit/1b1fbf65b8ed696db464bf53445e8f9b97a37210">gokrazy fix for
CVE-2024-12747</a>
is insufficient. You&rsquo;re calling <code>os.Open</code> with <code>O_NOFOLLOW</code>, but <code>O_NOFOLLOW</code>
only prevents symlink traversal in the last path component.</p>
<p>This is probably still vulnerable to replacing an earlier path component so
<code>os.Open(&quot;dir/passwd&quot;)</code> can be redirected by symlinking <code>dir</code> to <code>/etc</code>.</p>
</blockquote>
<p>We reported this to the rsync security contact address in April 2025. In
December 2025 I learned that someone else had also independently discovered and
reported this issue.</p>
<p>Ultimately, this resulted in CVE-2026-29518, published on 2026-05-20.</p>
<h3 id="may2026">May 2026 batch</h3>
<h4 id="cve-2026-29518">CVE-2026-29518: Symlink Race Condition (7.0)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/pull/895/changes/8471fdd1561049ef5f58df44a1811a50bd9a531d">rsync 3.4.3 NEWS entry</a>)</p>
<blockquote>
<p>TOCTOU symlink race condition allowing local privilege escalation in
daemon mode without chroot.</p>
<p>An rsync daemon configured with <code>use chroot = no</code> is exposed
to a time-of-check / time-of-use race on parent path components. A local
attacker with write access to a module can replace a parent directory
component with a symlink between the receiver&rsquo;s check and its open(),
redirecting reads (basis-file disclosure) and writes (file overwrite)
outside the module. Under elevated daemon privilege this allows privilege
escalation.</p>
<p>Default <code>use chroot = yes</code> is not exposed.</p>
<p>Reach: local attacker on the daemon host, write access to a module path,
daemon configured with <code>use chroot = no</code>.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/859d44fa4f1420775e4ba050337ef32092f2894c">upstream fix for
CVE-2026-29518</a>
uses <code>secure_relative_open()</code>, which is similar to Go’s <code>os.Root</code> API.</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes, Go offers an API to prevent this, see <a href="#goosroot">Defense in depth: Go’s
<code>os.Root</code></a>.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync was vulnerable until I switched <a href="https://github.com/gokrazy/rsync/commit/b6d50e5e9fd1fdf196f09947a6bc796b1ac48f46">the
sender</a>
and <a href="https://github.com/gokrazy/rsync/commit/cc42e03cad132ba19e0905f149c7c34dab45cd3a">the
receiver</a>
to <a href="#goosroot">the traversal-resistant <code>os.Root</code> API</a>.</p>
<h4 id="cve-2026-43618">CVE-2026-43618: Integer overflow leaks remote memory (8.1)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-g37v-g3gj-pmwq">GitHub Security Advisory</a>)</p>
<blockquote>
<p>Description: The receiver&rsquo;s compressed-token decoder accumulated a 32-bit
signed counter without overflow checking. A malicious sender can trigger an
overflow that, with careful manipulation, leaks process memory contents to
the attacker &ndash; environment variables, passwords, heap and library pointers
&ndash; significantly weakening ASLR and facilitating further exploitation.</p>
<p>Reach: authenticated daemon connection with compression enabled (the
default for protocols &gt;= 30 when both peers advertise it). Disabling
compression on the daemon (&ldquo;refuse options = compress&rdquo; in rsyncd.conf) is
the available workaround.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/c44c90e9460c666c965446a8c0957f0b9fa4c66a">upstream fix for
CVE-2026-43618</a>
introduces the missing checks.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not vulnerable because it does not implement compression. See
<a href="https://github.com/gokrazy/rsync/issues/35#issuecomment-2988582190">gokrazy/rsync issue
#35</a> for
details on why compression support sounds simple, but is non-trivial.</p>
<h4 id="cve-2026-43620">CVE-2026-43620: DOS after Out-of-bounds read (6.5)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-28pw-r563-rxvm">GitHub Security Advisory</a>)</p>
<blockquote>
<p>The 2025 fix that added a <code>parent_ndx&lt;0</code> guard in <code>send_files()</code>
was not applied to the visually-identical block in <code>recv_files()</code>. A
malicious rsync server can drive any connecting client into a deterministic
<code>SIGSEGV</code> by setting <code>CF_INC_RECURSE</code> in the compatibility flags, sending a
flist whose first sorted entry is not a leading &ldquo;.&rdquo; directory (which causes
<code>recv_file_list()</code> to set <code>parent_ndx = -1</code>), then sending a transfer record
with <code>ndx=0</code> and a non-<code>ITEM_TRANSFER</code> iflag word. The receiver reads
<code>dir_flist-&gt;files[-1]</code> and dereferences the result. On glibc x86-64 the
dereferenced pointer is mmap chunk metadata that lands at an unmapped
address, hence a clean <code>SEGV_MAPERR</code>; non-glibc allocators have not been
audited.</p>
<p>Reach: any rsync client doing a normal pull from an attacker-controlled
URL. Works for both rsync:// URLs and remote-shell pulls. <code>inc_recurse</code> is the
protocol-30+ default; no special options are required on the victim.</p>
<p>Workaround: <code>--no-inc-recursive</code> on the client.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/0cf200ecbb8baaf58070d825c4fbd892b4a63b69">upstream fix for
CVE-2026-43620</a>
adds the <code>parent_ndx&lt;0</code> guard to <code>recv_files()</code> as well.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>Just like for <a href="#cve-2024-12087">CVE-2024-12087</a>, gokrazy/rsync is not affected
by this vulnerability: gokrazy/rsync does not implement the incremental
recursion mode (<code>--inc-recursive</code>).</p>
<h4 id="cve-2026-43619">CVE-2026-43619: More symlink races (6.3)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-4h9m-w5ff-j735">GitHub Security
Advisory</a>)</p>
<blockquote>
<p>Description: Earlier fixes for symlink races on the receiver&rsquo;s open() call
(CVE-2026-29518) missed the same race class on every other path-based
system call: chmod, lchown, utimes, rename, unlink, mkdir, symlink, mknod,
link, rmdir, lstat. On rsync daemons with &ldquo;use chroot = no&rdquo; a local
attacker with filesystem access on the daemon host can swap a symlink into
a parent directory component between the receiver&rsquo;s check and one of these
syscalls, redirecting it outside the exported module. The fix routes each
affected path-based syscall through a parent dirfd opened under
RESOLVE_BENEATH-equivalent kernel-enforced confinement (openat2 on Linux
5.6+, O_RESOLVE_BENEATH on FreeBSD 13+ and macOS 15+, per-component
O_NOFOLLOW walk elsewhere). Default &ldquo;use chroot = yes&rdquo; is not exposed.</p>
<p>Reach: local attacker on the daemon host, write access to a module path,
daemon configured with use chroot = no.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/30656c5e358b1c6033f4caf24f3e11b29d25c9f4">upstream fix for
CVE-2026-43619</a>
uses the <code>*at</code> family of syscalls, just like Go’s <code>os.Root</code>.</p>
<p><strong>Can Go help prevent this?</strong></p>
<p>Yes, Go offers an API to prevent this, see <a href="#goosroot">Defense in depth: Go’s
<code>os.Root</code></a>.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not affected, because it uses Go’s <code>os.Root</code> API throughout.</p>
<h4 id="cve-2026-43617">CVE-2026-43617: Hostname/ACL bypass (4.8)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-rjfm-3w2m-jf4f">GitHub Security
Advisory</a>)</p>
<blockquote>
<p>On an rsync daemon configured with the global <code>daemon chroot = /X</code> rsyncd.conf
setting, the reverse-DNS lookup of the connecting client was performed <em>after</em>
the daemon had chrooted into <code>/X</code>. If <code>/X</code> did not contain the files glibc
needs for resolution (<code>/etc/resolv.conf</code>, <code>/etc/nsswitch.conf</code>, <code>/etc/hosts</code>,
NSS service modules), the lookup failed and the connecting hostname was set to
&ldquo;UNKNOWN&rdquo;. Hostname-based deny rules (&ldquo;hosts deny = *.evil.example&rdquo;) therefore
could not match, and an attacker controlling their PTR record could connect
from a hostname the administrator had intended to deny. IP-based ACLs are
unaffected. The per-module <code>use chroot</code> setting is unrelated to this issue.</p>
<p>Reach: rsync daemon configured with <code>daemon chroot = /X</code> AND hostname-based
ACLs AND <code>/X</code> does not include the libc resolver fixtures.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/c38f20c5ffabacd0c0c483786ab224a36e14bf43">upstream fix for
CVE-2026-43617</a>
moves the DNS lookup to an earlier point in the protocol.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync is not vulnerable because we only implement IP-based allow/deny
lists, not hostname-based allow/deny lists.</p>
<h4 id="cve-2026-45232">CVE-2026-45232: stack out-of-bounds write (3.1)</h4>
<p><strong>Description:</strong> (quoting the <a href="https://github.com/RsyncProject/rsync/security/advisories/GHSA-8f85-j2cv-59m8">GitHub Security Advisory</a>)</p>
<blockquote>
<p>The rsync client&rsquo;s HTTP <code>CONNECT</code> proxy support contains an off-by-one
out-of-bounds stack write in <code>establish_proxy_connection()</code> (<code>socket.c</code>). After
issuing the <code>CONNECT</code> request, rsync reads the proxy&rsquo;s first response line one
byte at a time into a 1024-byte stack buffer with the bound <code>cp &lt; &amp;buffer[sizeof buffer - 1]</code>, so the loop only ever writes <code>buffer[0..sizeof-2]</code>. If the proxy
(or a man-in-the-middle in front of it) returns 1023+ bytes on the first
response line without a <code>'\n'</code> terminator, the loop exits with <code>cp == &amp;buffer[sizeof buffer - 1]</code> — a slot the loop never wrote, so <code>*cp</code> holds stale
stack bytes left there by the earlier <code>snprintf()</code> that formatted the outgoing
<code>CONNECT</code> request. The post-loop code then does:</p>
<pre tabindex="0"><code>if (*cp != &#39;\n&#39;)   /* (*cp is uninitialised stack data) */
    cp++;          /* cp now &amp;buffer[sizeof]: one past end */
*cp-- = &#39;\0&#39;;      /* one-byte OOB write on the stack */
</code></pre><p>The <code>'\0'</code> lands one byte past the end of the on-stack <code>buffer[1024]</code>,
corrupting whatever lives in the adjacent stack slot. AddressSanitizer reports
<code>stack-buffer-overflow</code> at <code>socket.c:95</code> in the <code>establish_proxy_connection</code>
frame.</p>
</blockquote>
<p><strong>Upstream fix:</strong></p>
<p>The <a href="https://github.com/RsyncProject/rsync/commit/a5fc5ebe7a8ef1aa72f6e344599f97fd4427ecba">upstream fix for
CVE-2026-45232</a>
validates the attacker-supplied data.</p>
<p><strong>How does gokrazy/rsync fare?</strong></p>
<p>gokrazy/rsync does not implement such proxy support, so it is not vulnerable.</p>
<h3 id="go-verdict">Go verdict</h3>
<p>Let’s summarize how Go fares:</p>
<ul>
<li>The Go runtime’s bounds checks turn more serious security issues into a panic.
<ul>
<li>A panic is still a denial-of-service risk, but that’s much preferable.</li>
</ul>
</li>
<li>Go initializes memory to zero, making info leaks like CVE-2024-12085 impossible.</li>
<li>Go’s <code>os.Root</code> API prevents most of the remaining vulnerabilities.</li>
<li>Only one out of twelve vulnerabilities (CVE-2026-43617) is a proper bug in
the application logic that using Go could not have prevented.</li>
</ul>
<table>
  <thead>
      <tr>
          <th>CVE number</th>
          <th>Cause</th>
          <th>Risk (C)</th>
          <th>Does Go help?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2024-12084</td>
          <td>insufficient validation</td>
          <td>Heap Buffer Overflow</td>
          <td>✅ bounds check (panics!)</td>
      </tr>
      <tr>
          <td>2024-12085</td>
          <td>insufficient validation</td>
          <td>Info Leak</td>
          <td>✅ zero init</td>
      </tr>
      <tr>
          <td>2024-12086</td>
          <td>missing validation</td>
          <td>Arbitrary File Leak</td>
          <td>✅ <code>os.Root</code></td>
      </tr>
      <tr>
          <td>2024-12087</td>
          <td>insufficient validation</td>
          <td>Write Arbitrary Files</td>
          <td>✅ <code>os.Root</code></td>
      </tr>
      <tr>
          <td>2024-12088</td>
          <td>insufficient validation</td>
          <td>Create Arbitrary Symlinks</td>
          <td>✅ <code>os.Root</code></td>
      </tr>
      <tr>
          <td>2024-12747</td>
          <td>TOCTOU</td>
          <td>Leak Privileged Files</td>
          <td>✅ <code>os.Root</code></td>
      </tr>
      <tr>
          <td>2026-29518</td>
          <td>TOCTOU</td>
          <td>Leak Privileged Files</td>
          <td>✅ <code>os.Root</code></td>
      </tr>
      <tr>
          <td>2026-43617</td>
          <td>missing validation</td>
          <td>Deny List Ineffective</td>
          <td>❌ logic bug</td>
      </tr>
      <tr>
          <td>2026-43618</td>
          <td>insufficient validation</td>
          <td>Info Leak</td>
          <td>✅ bounds check (panics!)</td>
      </tr>
      <tr>
          <td>2026-43619</td>
          <td>TOCTOU</td>
          <td>Leak Privileged Files</td>
          <td>✅ <code>os.Root</code></td>
      </tr>
      <tr>
          <td>2026-43620</td>
          <td>insufficient validation</td>
          <td>crash (DOS)</td>
          <td>✅ bounds check (panics!)</td>
      </tr>
      <tr>
          <td>2026-45232</td>
          <td>missing validation</td>
          <td>Memory Write</td>
          <td>✅ bounds check (panics!)</td>
      </tr>
  </tbody>
</table>
<h3 id="gokrazy-rsync-verdict">gokrazy/rsync verdict</h3>
<p>Aside from being written in Go, another key difference between gokrazy/rsync and
the official upstream rsync is that the gokrazy implementation is <strong>minimal</strong>:</p>
<ul>
<li>gokrazy/rsync is unaffected by many vulnerabilities because it does not
implement the feature in question, for example <code>--inc-recursive</code>.</li>
<li>Like all other wire protocol-compatible rsync implementations, gokrazy/rsync
targets protocol version 27, because later protocol versions introduce
significant complexity.</li>
<li>In some cases, features that would be good to implement come with significant
blockers, e.g. compression is tricky, see <a href="https://github.com/gokrazy/rsync/issues/35#issuecomment-2988582190">gokrazy/rsync issue
#35</a> for
details.</li>
</ul>
<p>Let’s have a look at whether gokrazy/rsync was affected by each CVE at the time
of publishing:</p>
<table>
  <thead>
      <tr>
          <th>CVE number</th>
          <th>Cause</th>
          <th>gokrazy/rsync impl?</th>
          <th>gokrazy/rsync affected?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2024-12084</td>
          <td>insufficient validation</td>
          <td>yes</td>
          <td>⚠️ panic</td>
      </tr>
      <tr>
          <td>2024-12085</td>
          <td>insufficient validation</td>
          <td>no (proto 30)</td>
          <td>✅ not vuln</td>
      </tr>
      <tr>
          <td>2024-12086</td>
          <td>missing validation</td>
          <td>no (proto 29)</td>
          <td>✅ not vuln</td>
      </tr>
      <tr>
          <td>2024-12087</td>
          <td>insufficient validation</td>
          <td>no (<code>inc-rec</code>)</td>
          <td>✅ not vuln</td>
      </tr>
      <tr>
          <td>2024-12088</td>
          <td>insufficient validation</td>
          <td>no (<code>safe-links</code>)</td>
          <td>✅ not vuln</td>
      </tr>
      <tr>
          <td>2024-12747</td>
          <td>TOCTOU</td>
          <td>yes</td>
          <td>❌ vuln</td>
      </tr>
      <tr>
          <td>2026-29518</td>
          <td>TOCTOU</td>
          <td>yes</td>
          <td>⚠️ patched</td>
      </tr>
      <tr>
          <td>2026-43617</td>
          <td>missing validation</td>
          <td>no (host deny lists)</td>
          <td>✅ not vuln</td>
      </tr>
      <tr>
          <td>2026-43618</td>
          <td>insufficient validation</td>
          <td>no (compression)</td>
          <td>✅ not vuln</td>
      </tr>
      <tr>
          <td>2026-43619</td>
          <td>TOCTOU</td>
          <td>yes</td>
          <td>⚠️ patched</td>
      </tr>
      <tr>
          <td>2026-43620</td>
          <td>insufficient validation</td>
          <td>no (<code>inc-rec</code>)</td>
          <td>✅ not vuln</td>
      </tr>
      <tr>
          <td>2026-45232</td>
          <td>missing validation</td>
          <td>no (proxy)</td>
          <td>✅ not vuln</td>
      </tr>
  </tbody>
</table>
<p>To be clear: all known vulnerabilities are fixed in gokrazy/rsync! The table
above documents what the state was at the time when each CVE was published. In
other words:</p>
<p>When the January 2025 vulnerabilities were published, gokrazy/rsync panicked
(CVE-2024-12084) and was vulnerable to a TOCTOU race (CVE-2024-12747). In the
process of fixing the TOCTOU issue, we discovered CVE-2026-29518, which was
fixed in gokrazy/rsync before the CVE was published. CVE-2026-43619 was
discovered even later, but was also already fixed in gokrazy/rsync with the same
fix: using Go’s <code>os.Root</code> everywhere.</p>
<h3 id="terminology">Imprecise terminology</h3>
<p>As I was reading the vulnerability reports, I noticed that the reports were
slightly misleading by their choice of words: most reports just spoke of
“server” and “client”. However, in an rsync transfer, both sides, the rsync
client and the rsync server can assume either role: sender (upload files) or
receiver (download files)!</p>
<p>Some setups come with further restrictions that make certain attacks harder or
impossible to pull off. For example, when running in daemon mode, file system
access can be restricted to the pre-configured module paths (but not in command
mode!).</p>
<p>Here is a diagram to give you an overview of the 4 different setups and
role/protocol layering:</p>















<a href="https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/rsync-combinations.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/rsync-combinations_hu_b83596d5e1578bab.png 2x,https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/rsync-combinations_hu_80c1da434b4b343b.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/rsync-combinations_hu_b1c2c5d30df87dad.png"
  alt="diagram showing 4 different rsync setups: (1) rsync --daemon (2) rsync -e ssh; rsync://server/module/dir (3) rsync server:/some/path (4) rsync /src" title="diagram showing 4 different rsync setups: (1) rsync --daemon (2) rsync -e ssh; rsync://server/module/dir (3) rsync server:/some/path (4) rsync /src"
  width="600"
  height="547"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>In the context of our vulnerability reports, I would say that the Arbitrary File
Leak vulnerability (CVE-2024-12086)’s original title “Server leaks arbitrary
client files” can easily be misunderstood.</p>
<p>Instead, I would say: The <strong>rsync receiver</strong> will leak arbitrary files to a
<strong>malicious sender</strong>.</p>
<p>I have verified that a malicious client sender can make an unpatched remote
rsync open files outside the destination tree (e.g. the <code>/etc/shadow</code> system
password database) when running in command mode, for example over SSH. (But,
when running in daemon mode, the server enables additional path sanitization,
which prevents this attack.)</p>
<p>Similarly, the Symlink Path Traversal vulnerability (CVE-2024-12087) speaks
about a “malicious server”, but again, it should be “malicious sender”, which
can be either the client or the server.</p>
<h2 id="openrsync">Comparison with OpenBSD’s openrsync (C)</h2>
<p>The OpenBSD project is known for its security focus, so how does openrsync
compare?</p>
<p>openrsync is not affected by the Heap Buffer Overflow (CVE-2024-12084) and Stack
Info Leak (CVE-2024-12085) vulnerabilities because it validates the checksum
length and only supports one checksum size/algorithm (MD4).</p>
<p>openrsync is not affected by CVE-2024-12086, CVE-2024-12087 and CVE-2024-12088
because it does not implement the relevant features (like gokrazy/rsync). Even
if it was vulnerable, openrsync’s defense-in-depth measures like using
<a href="https://man.openbsd.org/unveil.2">OpenBSD’s <code>unveil(2)</code></a> and
<a href="https://man.openbsd.org/pledge.2"><code>pledge(2)</code></a> to restrict file system access
would have prevented successful exploitation — at least when running on OpenBSD.</p>
<p>openrsync is not affected by CVE-2024-12747 because it <a href="https://github.com/kristapsdz/openrsync/commit/8f1927c35021cd1fbf31cb8c5a6393cc37ba4de8">used <code>O_NOFOLLOW</code> from
the very moment they implemented symlink
support</a>. But,
because <code>O_NOFOLLOW</code> is not a sufficient fix for this issue, openrsync <strong>is</strong>
affected by CVE-2026-29518!</p>
<p>The above covers the January 2025 batch of vulnerabilities; the May 2026 batch
is similar in that most features just are not implemented.</p>
<p>Overall, I say: Well done, Kristaps and contributors! By diligently implementing
validation, restricting the attack surface and employing defense-in-depth
measures, openrsync manages to not be affected by almost all of the reported
vulnerabilities.</p>
<h2 id="defenseindepth">Defense in depth</h2>
<p>Which APIs and environments can we use on Linux for defense-in-depth measures?</p>
<p>I’ll go through the ones <code>gokrazy/rsync</code> supports, ordered by traditional to
modern.</p>
<h3 id="mountnamespaces">Linux mount namespaces</h3>
<p>Within a few weeks after starting the <code>gokrazy/rsync</code> project, I <a href="https://github.com/gokrazy/rsync/commit/d63aaed3be4f69ac98581c4b0ac2646c37f1f8c0">added support
for dropping privileges and using mount/pid namespaces on
Linux</a>
to restrict the file system objects that my rsync server could work with.</p>
<p>This approach works very well to mitigate path traversal attacks, but requires
privileges, meaning we need to run as <code>root</code> or <a href="https://github.com/gokrazy/rsync/commit/da9802e33c89af947d4958ec31e51b14f43ebfb2">in a Linux user
namespace</a>
(if enabled on your distribution / system).</p>
<p>That limitation makes mount namespaces well-suited for server setups, but
usually unavailable for interactive one-off transfers that are typically running
under a human’s user account.</p>
<h3 id="systemdhardening">systemd hardening</h3>
<p>In the same commit that introduced Linux mount/pid namespace support, I also
included a systemd service file that restricted file system access to home
directories and encouraged folks in the README to further restrict file system
access, depending on what their use-case allows.</p>
<p>These file system restrictions, if set up correctly, mitigate the File Leak
(CVE-2024-12086) and Path Traversal (CVE-2024-12087) vulnerabilities.</p>
<p>The Symlink Race Condition (CVE-2024-12747) relies on privilege escalation
through the rsync process, but thanks to the
<a href="https://0pointer.net/blog/dynamic-users-with-systemd.html">DynamicUser</a>
feature, our process has fewer privileges than other users.</p>
<p>Similarly to mount namespaces, these measures are great for server setups, but
too cumbersome to set up for interactive one-off usages.</p>
<h3 id="landlock">Linux Landlock</h3>
<p>I stumbled upon Justine’s blog post <a href="https://justine.lol/pledge/#paths">Porting OpenBSD pledge() to Linux
(2022)</a> and was reminded that Linux offers
the <a href="https://docs.kernel.org/userspace-api/landlock.html">Landlock API</a> for
unprivileged, per-process access control, similar to OpenBSD’s
<a href="https://man.openbsd.org/unveil"><code>unveil(2)</code></a> system call, which openrsync
uses. The basic idea is that once your program knows the directory it works
with, it makes a call like <code>unveil(&quot;/home/michael/backups&quot;, &quot;rw&quot;);</code> and no
longer has access to other file system locations.</p>
<p>I had previously heard of Landlock at a Go Meetup, so I knew there was Go
support for Landlock. Back in 2022, I enabled Landlock support in the gokrazy
kernel images.</p>
<p>So I gave it a shot in March 2025 and <a href="https://github.com/gokrazy/rsync/commit/f84d1e39ec6613599cd9610974e89be5ca183a6d">implemented Landlock support to restrict
file system
access</a>. It
took me a few hours, which seems a little longer than one might expect at
first. Making Landlock work (and/or skipping it) in our test environment ran
into a couple of road blocks: Our tests had defined many functions that get run
in the same process, but when repeatedly adding rulesets, we would exceed the
limit of 16 (!) policy layers per process.</p>
<p>Once I had it set up just right, it is a beautiful solution. Now we can restrict
rsync transfers to their sources (read-only) or destination directories
(read-write), even for unprivileged invocations of <code>gokrazy/rsync</code>! 🎉</p>
<hr>
<p>The downside to Landlock is that Landlock operates at the process level. This
means that Landlock policies must include the files that your program needs,
e.g. <code>gokrazy/rsync</code> needs to be able to read <code>/etc/passwd</code> for user id lookup,
so if the attacker is after the <code>/etc/passwd</code> file, Landlock does not help.</p>
<h3 id="goosroot">Go’s os.Root</h3>
<p>In February 2025, the Go 1.24 release introduced the
<a href="https://pkg.go.dev/os#Root"><code>os.Root</code></a> API, which is resistant against path
traversal, see <a href="https://go.dev/blog/osroot">The Go Blog: Traversal-resistant file
APIs</a> (by Damien Neil, March 2025). This API allows
more fine-grained control (per file system operation) compared to Landlock.</p>
<p>Go 1.25 (released in August 2025) added more methods to <code>os.Root</code>, making it a
convenient choice for most file system usage.</p>
<p>I have converted all of <code>gokrazy/rsync</code>’s file system usage to use <code>os.Root</code>,
which is a great fit: users configure input/output directories, but the
filenames received over the network are untrusted. That’s exactly what <code>os.Root</code>
was designed for!</p>
<hr>
<p>When I first looked into using <code>os.Root</code>, I thought that some system calls could
inherently not be made with this API, like for example <a href="https://manpages.debian.org/mknod.2"><code>mknod(2)</code></a>
 to create device node files. Damien explained:</p>
<blockquote>
<p>It won&rsquo;t support mknod, though.</p>
<p>However, you should be able to use it to enable a safe mknod:</p>
<ul>
<li>os.Root.OpenFile the parent directory of the target,</li>
<li>File.Fd to get the file descriptor for that directory,</li>
<li><a href="https://pkg.go.dev/golang.org/x/sys/unix#Mknodat">https://pkg.go.dev/golang.org/x/sys/unix#Mknodat</a> to create the file.</li>
</ul>
</blockquote>
<p>If you’re curious how that looks in practice, check out <code>gokrazy/rsync</code>’s usage
in <a href="https://github.com/gokrazy/rsync/blob/45444634288e753178af56c61e064ffb08636695/internal/receiver/generatormknod_linux.go#L15-L29"><code>internal/receiver/generatormknod_linux.go</code>, line
15-29</a>.</p>
<p>Another stumbling block was when I realized that unlike with <a href="https://manpages.debian.org/mknodat.2"><code>mknodat(2)</code></a>
, Linux only implements <a href="https://manpages.debian.org/bind.2"><code>bind(2)</code></a>
, but no <code>bindat</code> (as of Linux 7.0)!</p>
<p>Luckily, <a href="https://mastodon.social/@pid_eins/115869484548013992">Lennart Poettering pointed
out</a> that there’s a trick
to skip path resolution without <code>bindat</code>:</p>
<blockquote>
<p>you can probably bind to <code>/proc/self/&lt;fd&gt;/foobar</code> in the meantime&hellip;</p>
</blockquote>
<p>And indeed, this works! Path resolution is skipped because we only specify a
basename (last component of a path) after the known-safe <code>/proc/self/&lt;fd&gt;</code>, not
a path (see <a href="https://github.com/gokrazy/rsync/blob/45444634288e753178af56c61e064ffb08636695/internal/receiver/generatormknod_linux.go#L49-L56">line
49-56</a>).</p>
<p>With these two tips, <code>gokrazy/rsync</code> v0.3.1 and newer are fully using <code>os.Root</code>,
meaning all file system access is traversal-safe! 🥳</p>
<h2 id="conclusion">Conclusion</h2>
<p><strong>Lacking validation causes vulnerabilities</strong></p>
<p>It is interesting to note that aside from the TOCTOU vulnerabilities
(CVE-2024-12747, CVE-2026-29518 and CVE-2026-43619), all other vulnerabilities
were caused by missing or incorrect input validation. In three cases, there was
just no validation to begin with. In another case (CVE-2024-12088), the subject
matter of file system path resolution is tricky enough that the existing
validation did not cover all edge cases.</p>
<p>As the <a href="#go-verdict">Go verdict</a> section explains in more detail, the most
valuable structural fixes are to provide bounds checking (= always-on
validation) and safe-by-default APIs like Go’s <code>os.Root</code>.</p>
<p><strong>Too much complexity</strong></p>
<p>A few of the vulnerabilities came from evolution of the rsync protocol: The
code used to correctly perform sufficient validation, but then new features were
added. For example, when checksum algorithm negotiation was added (protocol
version 30), the validation was not correctly updated. When incremental
recursion was added (also protocol version 30), the validation that made sense
for individual file lists was not updated for the new processing approach of
merging incremental file lists.</p>
<p>Avoiding complexity avoids vulnerabilities! Both gokrazy/rsync and also
openrsync were not vulnerable to 8 out of the 12 security vulnerabilities simply
because they do not implement the feature with the vulnerability.</p>
<p>Of course, these features were added to rsync because they were valuable to
someone at some point, and of course I am not saying that we should just… not
develop software any further, ever.</p>
<p>But, I consider it ideal to <strong>use an implementation whose complexity is
appropriate for and proportional to the complexity of the use-case</strong>. In other
words: for simple use-cases, reach for a simple implementation. Only reach for
the fully-featured implementation where needed.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Stamp It! All Programs Must Report Their Version]]></title>
    <link href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/"/>
    <id>https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/</id>
    <published>2026-04-05T16:20:00+02:00</published>
    <content type="html"><![CDATA[<p>Recently, during a production incident response, I guessed the root cause of an
outage correctly within less than an hour (cool!) and submitted a fix just to
rule it out, only to then spend many hours fumbling in the dark because we
lacked visibility into version numbers and rollouts… 😞</p>
<p>This experience made me think about software versioning again, or more
specifically about build info (build versioning, version stamping, however you
want to call it) and version reporting. I realized that for the i3 window
manager, I had solved this problem well over a decade ago, so it was really
unexpected that the problem was decidedly not solved at work.</p>
<p>In this article, I’ll explain how 3 simple steps (Stamp it! Plumb it! Report
it!) are sufficient to save you hours of delays and stress during incident
response.</p>
<h2 id="low-versioning-standards">Why are our versioning standards so low?!</h2>
<p>Every household appliance has incredibly detailed versioning! Consider this
dishwasher:</p>















<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-04-feuermurmel-dishwasher-versioning.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-04-feuermurmel-dishwasher-versioning_hu_1d046eb0df440b85.jpg 2x,https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-04-feuermurmel-dishwasher-versioning_hu_1a09cac140bcb1d2.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-04-feuermurmel-dishwasher-versioning_hu_9dd1db4e61dd3ec0.jpg"
  alt="a dishwasher, with many precise bits of version information" title="a dishwasher, with many precise bits of version information"
  width="600"
  height="450"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p><em>(Thank you Feuermurmel for sending me this lovely example!)</em></p>
<p>I observed a couple household appliance repairs and am under the impression that
if a repair person cannot identify the appliance, they would most likely refuse
to even touch it.</p>
<p>So why are our standards so low in computers, in comparison? Sure, consumer
products are typically versioned <em>somehow</em> and that’s typically good enough
(except for, say, USB 3.2 Gen 1×2!). But recently, I have encountered too many
developer builds that were not adequately versioned!</p>
<h2 id="software-versioning">Software Versioning</h2>
<p>Unlike a physical household appliance with a stamped metal plate, software is
constantly updated and runs in places and structures we often cannot even see.</p>
<p>Let’s dig into what we need to increase our versioning standard!</p>
<p>Usually, software has a <strong>name</strong> and some <strong>version number</strong> of varying granularity:</p>
<ul>
<li>Chrome</li>
<li>Chrome 146</li>
<li>Chrome 146.0.7680.80</li>
<li>Chrome f08938029c887ea624da7a1717059788ed95034d-refs/branch-heads/7680_65@{#34}</li>
</ul>
<p>All of these identify the Chrome browser on my computer, but each at different
granularity.</p>
<p>All are correct and useful, depending on the context. Here’s an example for each:</p>
<ol>
<li>“This works in Chrome for me, did you test in Firefox?”</li>
<li>“Chrome 146 contains broken middle-click-to-paste-and-navigate”</li>
<li>“I run Chrome 146.0.7680.80 and cannot reproduce your issue”</li>
<li>“Apply this patch on top of Chrome f08938029c887ea624da7a1717059788ed95034d-refs/branch-heads/7680_65@{#34} and follow these steps to reproduce: […]”</li>
</ol>
<p>After creating the <a href="https://i3wm.org">i3 window manager</a>, I quickly learned that
for user support, it is very valuable for programs to clearly identify
themselves. Let me illustrate with the following case study.</p>
<h2 id="i3-moreversion">Case Study: i3’s <code>--version</code> and <code>--moreversion</code></h2>
<p>When running <code>i3 --version</code>, you will see output like this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% i3 --version
</span></span><span style="display:flex;"><span>i3 version 4.24 (2024-11-06) © 2009 Michael Stapelberg and contributors
</span></span></code></pre></div><p>Each word was carefully deliberated and placed. Let me dissect:</p>
<ol>
<li><code>i3 version 4.24</code>: I could have shortened this to <code>i3 4.24</code> or maybe <code>i3 v4.24</code>, but I figured it would be helpful to be explicit because <code>i3</code> is such
a short name. Users might mumble aloud “What’s an i-3-4-2-4?”, but when
putting “version” in there, the implication is that i3 is some computer thing
(→ a computer program) that exists in version 4.24.</li>
<li><code>(2024-11-06)</code> is the release date so that you can immediately tell if
“<code>4.24</code>” is recent.</li>
<li><code>© 2009 Michael Stapelberg</code> signals when the project was started and who is
the main person behind it.</li>
<li><code>and contributors</code> gives credit to the many people who helped. i3 was never a
one-person project; it was always a group effort.</li>
</ol>
<p>When doing user support, there are a couple of questions that are conceptually
easy to ask the affected user and produce very valuable answers for the
developer:</p>
<ol>
<li>Question: “Which version of i3 are you using?”
<ul>
<li>Since i3 is not a typical program that runs in a window (but a window
manager / desktop environment), there is no Help → About menu
option.</li>
<li>Instead, we started asking: What is the output of <code>i3 --version</code>?</li>
</ul>
</li>
<li>Question: “<em>Are you reporting a new issue or a preexisting issue? To confirm,
can you try going back to the version of i3 you used previously?</em>”. The
technical terms for “going back” are downgrade, rollback or revert.
<ul>
<li>Depending on the Linux distribution, this is either trivial or a nightmare.</li>
<li>With NixOS, it’s trivial: you just boot into an older system “generation”
by selecting that version in the bootloader. Or you revert in git, if your
configs are version-controlled.</li>
<li>With imperative Linux distributions like Debian Linux or Arch Linux, if
you did not take a file system-level snapshot, there is no easy and
reliable way to go back after upgrading your system. If you are lucky, you
can just <code>apt install</code> the older version of i3. But you might run into
dependency conflicts (“version hell”).</li>
<li>I know that it is <em>possible</em> to run older versions of Debian using
<a href="https://snapshot.debian.org/">snapshot.debian.org</a>, but it is just not
very practical, at least when I last tried.</li>
</ul>
</li>
<li>Can you check if the issue is still present in the latest i3 development version?
<ul>
<li>Of course, I could also try reproducing the user issue with the latest
release version, and <strong>then one additional time</strong> on the latest
development version.</li>
<li>But this way, the verification step moves to the affected user, which is
good because it filters for highly-motivated bug reporters (higher chance
the bug report actually results in a fix!) and it makes the user reproduce
the bug <em>twice</em>, figuring out if it’s a flaky issue, hard-to-reproduce, if
the reproduction instructions are correct, etc.</li>
<li>A natural follow-up question: “<em>Does this code change make the issue go
away?</em>” This is easy to test for the affected user who now has a
development environment.</li>
</ul>
</li>
</ol>
<p>Based on my experiences with asking these questions many times, I noticed a few
patterns in how these debugging sessions went. In response, I introduced another
way for i3 to report its version in i3 v4.3 (released in September 2012): a
<code>--moreversion</code> flag! Now I could ask users a small variation of the first
question: What is the output of <code>i3 --moreversion</code>? Note how this also transfers
well over spoken word, for example at a computer meetup:</p>
<blockquote>
<p><strong>Michael:</strong> Which version are you using?</p>
<p><strong>User:</strong> How can I check?</p>
<p><strong>Michael:</strong> Run this command: <code>i3 --version</code></p>
<p><strong>User:</strong> It says 4.24.</p>
<p><strong>Michael:</strong> Good, that is recent enough to include the bug fix. Now, we need
more version info! Run <code>i3 --moreversion</code> please and tell me what you see.</p>
</blockquote>
<p>When you run <code>i3 --moreversion</code>, it does not just report the version of the i3
program you called, it also connects to the running i3 window manager process in
your X11 session using <a href="https://i3wm.org/docs/ipc.html">its IPC (interprocess communication)
interface</a> and reports the running i3 process’s
version, alongside other key details that are helpful to show the user, like
which configuration file is loaded and when it was last changed:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% i3 --moreversion
</span></span><span style="display:flex;"><span>Binary i3 version:  4.24 (2024-11-06) © 2009 Michael Stapelberg and…
</span></span><span style="display:flex;"><span>Running i3 version: 4.24 (2024-11-06) (pid 2521)
</span></span><span style="display:flex;"><span>Loaded i3 config:
</span></span><span style="display:flex;"><span>  /home/michael/.config/i3/config (main)
</span></span><span style="display:flex;"><span>  (last modified: 2026-03-15T23:09:27 CET, 1101585 seconds ago)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>The i3 binary you just called:
</span></span><span style="display:flex;"><span>/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24/bin/i3
</span></span><span style="display:flex;"><span>The i3 binary you are running: i3
</span></span></code></pre></div><p>This might look like a lot of detail on first glance, but let me spell out why
this output is such a valuable debugging tool:</p>
<ol>
<li>
<p>Connecting to i3 via the IPC interface is an interesting test in and of
itself. If a user sees <code>i3 --moreversion</code> output, that implies they will also
be able to run debugging commands like (for example) <code>i3-msg -t get_tree &gt; /tmp/tree.json</code> to capture the full layout state.</p>
</li>
<li>
<p>During a debugging session, running <code>i3 --moreversion</code> is an easy check to
see if the version you just built is actually effective (see the <code>Running i3 version</code> line).</p>
<ul>
<li>Note that this is the same check that is relevant during production
incidents: verifying that <em>effectively running</em> matches <em>supposed to be
running</em> versions.</li>
</ul>
</li>
<li>
<p>Showing the full path to the loaded config file will make it obvious if the
user has been editing the wrong file. If the path alone is not sufficient,
the modification time (displayed both absolute and relative) will flag
editing the wrong file.</p>
</li>
</ol>
<p>I use NixOS, BTW, so I automatically get a stable identifier
(<code>0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24</code>) for <em>the specific build</em> of i3.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% ls -l $(which i3)
</span></span><span style="display:flex;"><span>lrwxrwxrwx 1 root root 58 1970-01-01 01:00 /run/current-system/sw/bin/i3
</span></span><span style="display:flex;"><span>-&gt; /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24/bin/i3
</span></span></code></pre></div><p>To see the build recipe (“derivation” in Nix terminology) which produced this
Nix store output (<code>0zn9r4263…-i3-4.24</code>), I can run <code>nix derivation show</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% nix derivation show /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  &#34;/nix/store/z7ly4kvgixf29rlz01ji4nywbajfifk4-i3-4.24.drv&#34;: {
</span></span><span style="display:flex;"><span>[…]
</span></span></code></pre></div><details>
<summary>Click here to expand the full <code>nix derivation show</code> output if you are curious</summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% nix derivation show /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  &#34;/nix/store/z7ly4kvgixf29rlz01ji4nywbajfifk4-i3-4.24.drv&#34;: {
</span></span><span style="display:flex;"><span>    &#34;args&#34;: [
</span></span><span style="display:flex;"><span>      &#34;-e&#34;,
</span></span><span style="display:flex;"><span>      &#34;/nix/store/l622p70vy8k5sh7y5wizi5f2mic6ynpg-source-stdenv.sh&#34;,
</span></span><span style="display:flex;"><span>      &#34;/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh&#34;
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    &#34;builder&#34;: &#34;/nix/store/6ph0zypyfc09fw6hlc1ygjvk2hv4j9vd-bash-5.3p3/bin/bash&#34;,
</span></span><span style="display:flex;"><span>    &#34;env&#34;: {
</span></span><span style="display:flex;"><span>      &#34;NIX_MAIN_PROGRAM&#34;: &#34;i3&#34;,
</span></span><span style="display:flex;"><span>      &#34;__structuredAttrs&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;buildInputs&#34;: &#34;/nix/store/58q0dn2lbm2p04qmds0aymwdd1fr5j67-libxcb-1.17.0-dev /nix/store/3fcfw014z5i05ay1ag0hfr6p81mb1kzw-libxcb-keysyms-0.4.1-dev /nix/store/2cdrqvd3av1dmxna9xjqv1jccibpvg6m-libxcb-util-0.4.1-dev /nix/store/256alp82fhdgbxx475dp7mk8m29y53rh-libxcb-wm-0.4.2-dev /nix/store/nr44nfhj48abr3s6afqy1fjq4qmr23lz-xcb-util-xrm-1.3 /nix/store/ml4cfhhw6af6qq6g3dn7g5j5alrnii88-libxkbcommon-1.11.0-dev /nix/store/6hnzjg09fd5xkkrdj437wyaj952nlg45-libstartup-notification-0.12 /nix/store/9m0938zahq7kcfzzix4kkpm8d1iz3nmq-libx11-1.8.12-dev /nix/store/vz5gd0rv0m2kjca50gacz0zq9qh7i8xf-pcre2-10.46-dev /nix/store/334cvqpqc9f0plv0aks71g352w6hai0c-libev-4.33 /nix/store/6s3fw10c0441wv53bybjg50fh8ag1561-yajl-2.1.0-unstable-2024-02-01 /nix/store/d6aw2004h90dwlsfcsygzzj4pzm1s31a-libxcb-cursor-0.1.6-dev /nix/store/84mhqfj9amzyvxhp37yh3b0zd8sq0a7p-perl-5.40.0 /nix/store/l6bslkrp59gaknypf1jrs5vbb2xmcwym-pango-1.57.0-dev /nix/store/7s7by82nq8bahsh195qr0mnn9ac8ljmm-perl5.40.0-AnyEvent-I3-0.19 /nix/store/9ml0p4x1cx5k1lla91bxgramc0amsfkf-perl5.40.0-X11-XCB-0.20 /nix/store/67j1sx7qcn6f7qvq1kh3z8i5mpajgq3r-perl5.40.0-IPC-Run-20231003.0 /nix/store/859x84mz38bcq0r7hwksk4b5apcsmf2w-perl5.40.0-ExtUtils-PkgConfig-1.16 /nix/store/q1qydg6frfpq9jkhnymfsjzf71x9jswr-perl5.40.0-Inline-C-0.82&#34;,
</span></span><span style="display:flex;"><span>      &#34;builder&#34;: &#34;/nix/store/6ph0zypyfc09fw6hlc1ygjvk2hv4j9vd-bash-5.3p3/bin/bash&#34;,
</span></span><span style="display:flex;"><span>      &#34;checkPhase&#34;: &#34;runHook preCheck\n\ntest_failed=\n# \&#34;| cat\&#34; disables fancy progress reporting which makes the log unreadable.\n./complete-run.pl -p 1 --keep-xserver-output | cat || test_failed=\&#34;complete-run.pl returned $?\&#34;\nif [ -z \&#34;$test_failed\&#34; ]; then\n  # Apparently some old versions of `complete-run.pl` did not return a\n  # proper exit code, so check the log for signs of errors too.\n  grep -q &#39;^not ok&#39; latest/complete-run.log &amp;&amp; test_failed=\&#34;test log contains errors\&#34; ||:\nfi\nif [ -n \&#34;$test_failed\&#34; ]; then\n  echo \&#34;***** Error: $test_failed\&#34;\n  echo \&#34;===== Test log =====\&#34;\n  cat latest/complete-run.log\n  echo \&#34;===== End of test log =====\&#34;\n  false\nfi\n\nrunHook postCheck\n&#34;,
</span></span><span style="display:flex;"><span>      &#34;cmakeFlags&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;configureFlags&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;debug&#34;: &#34;/nix/store/20rgxn6fpywd229vka9dnjiaprypxirh-i3-4.24-debug&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsBuildBuild&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsBuildBuildPropagated&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsBuildTarget&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsBuildTargetPropagated&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsHostHost&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsHostHostPropagated&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsTargetTarget&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;depsTargetTargetPropagated&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;doCheck&#34;: &#34;1&#34;,
</span></span><span style="display:flex;"><span>      &#34;doInstallCheck&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;mesonFlags&#34;: &#34;-Ddocs=true -Dmans=true&#34;,
</span></span><span style="display:flex;"><span>      &#34;name&#34;: &#34;i3-4.24&#34;,
</span></span><span style="display:flex;"><span>      &#34;nativeBuildInputs&#34;: &#34;/nix/store/x06h0jfzv99c3dmb8pj8wbmy0v9wj6bd-pkg-config-wrapper-0.29.2 /nix/store/pcdnznc797nmf9svii18k3c5v22sqihs-make-shell-wrapper-hook /nix/store/nzg469dkg5dj7lv4p50pi8zmwzxx73hr-meson-1.9.1 /nix/store/rlcn0x0j22nbhhf8wfp8cwfxgh65l82r-ninja-1.13.1 /nix/store/hs4pgi40k5nbl0fpf0jx8i5f6zrdv63v-install-shell-files /nix/store/84mhqfj9amzyvxhp37yh3b0zd8sq0a7p-perl-5.40.0 /nix/store/xiqlw1h0i6a6v59skrg9a7rg3qpanqy7-asciidoc-10.2.1 /nix/store/300facd5m37fwqrypjcikn09vqs488zv-xmlto-0.0.29 /nix/store/yk7avh2szvm6bi5dwgzz4c2iciaipj2p-docbook-xml-4.5 /nix/store/d5qdxn0rjl9s7xfc1rca33gya0fhcvkm-docbook-xsl-nons-1.79.2 /nix/store/2y1r1cpza3lpk7v6y9mf75ak0pswilwi-find-xml-catalogs-hook /nix/store/r989dk196nl9frhnfsa1lb7knhbyjxw6-separate-debug-info.sh /nix/store/xlhipdkyqksxvp73cznnij5q6ilbbqd9-xorg-server-21.1.21-dev /nix/store/i8nxxmw5rzhxlx3n12s3lvplwwap6mpc-xvfb-run-1+g87f6705 /nix/store/a198i9cnhn6y5cajkdxg0hhcrmalazjr-xdotool-3.20211022.1 /nix/store/b4dnjyq2i4kjg8xswkjd7lwfcdps94j8-setxkbmap-1.3.4 /nix/store/cxdbw6iqj1a1r69wb55xl5nwi7abfllb-xrandr-1.5.3 /nix/store/5k4mv2a1qrciv12wywlkgpslc6swyv58-which-2.23&#34;,
</span></span><span style="display:flex;"><span>      &#34;out&#34;: &#34;/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24&#34;,
</span></span><span style="display:flex;"><span>      &#34;outputs&#34;: &#34;out debug&#34;,
</span></span><span style="display:flex;"><span>      &#34;patches&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;pname&#34;: &#34;i3&#34;,
</span></span><span style="display:flex;"><span>      &#34;postInstall&#34;: &#34;wrapProgram \&#34;$out/bin/i3-save-tree\&#34; --prefix PERL5LIB \&#34;:\&#34; \&#34;$PERL5LIB\&#34;\nfor program in $out/bin/i3-sensible-*; do\n  sed -i &#39;s/which/command -v/&#39; $program\ndone\n\ninstallManPage man/*.1\n&#34;,
</span></span><span style="display:flex;"><span>      &#34;postPatch&#34;: &#34;patchShebangs .\n\n# This testcase generates a Perl executable file with a shebang, and\n# patchShebangs can&#39;t replace a shebang in the middle of a file.\nif [ -f testcases/t/318-i3-dmenu-desktop.t ]; then\n  substituteInPlace testcases/t/318-i3-dmenu-desktop.t \\\n    --replace-fail \&#34;#!/usr/bin/env perl\&#34; \&#34;#!/nix/store/84mhqfj9amzyvxhp37yh3b0zd8sq0a7p-perl-5.40.0/bin/perl\&#34;\nfi\n&#34;,
</span></span><span style="display:flex;"><span>      &#34;propagatedBuildInputs&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;propagatedNativeBuildInputs&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;separateDebugInfo&#34;: &#34;1&#34;,
</span></span><span style="display:flex;"><span>      &#34;src&#34;: &#34;/nix/store/qx48i7zf9n69yla8gfbif6dskysk0l1w-source&#34;,
</span></span><span style="display:flex;"><span>      &#34;stdenv&#34;: &#34;/nix/store/43dbh9z6v997g6njz4yqmcrj26zic9ds-stdenv-linux&#34;,
</span></span><span style="display:flex;"><span>      &#34;strictDeps&#34;: &#34;&#34;,
</span></span><span style="display:flex;"><span>      &#34;system&#34;: &#34;x86_64-linux&#34;,
</span></span><span style="display:flex;"><span>      &#34;version&#34;: &#34;4.24&#34;
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    &#34;inputDrvs&#34;: {
</span></span><span style="display:flex;"><span>      &#34;/nix/store/0h97zzsaf4ggiiwi0rbdjl3fzjj8vhj0-meson-1.9.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/0r073sy0685h3gycpl8kpkgmv5p87rw4-libxcb-1.17.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/0rjr80q4lpigwjwaxw089wcrrag7p46m-xmlto-0.0.29.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/14wsbyw3j1h9blcxr16c9663w0piq0p2-bash-5.3p3.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/165y3ip2cqlnqd6qrgh6lzklv21xy11w-make-shell-wrapper-hook.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/1abxvpwsry6q5pijb2j91aryh2ilp929-pango-1.57.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/2sjcj6l2959dvd5vlicmkf1sdr0hwqx5-perl5.40.0-ExtUtils-PkgConfig-1.16.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/3jnvpbpi95g6zp8vjq1qafh20lz6kwi3-perl5.40.0-X11-XCB-0.20.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/45szhbhybqh4fkcpmx7sqpcrpwpadvgv-pkg-config-wrapper-0.29.2.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/4r5bd9g98fq40hjbfc7sbnp42jhnzg5h-yajl-2.1.0-unstable-2024-02-01.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/4yw0g3zqw4gn1szw8bqrvgmz5b6qm8s5-stdenv-linux.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/53gin0imc257fibkbyvl0jsi0pm1zvbl-docbook-xml-4.5.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/54q42ddy9jb24v4mbx0f19faqqsw5jga-libxkbcommon-1.11.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/56dg95jlnwp6kkifyqh94f548r5cha9b-xrandr-1.5.3.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/6srgz2k17vc6x85s3paccdbgg9rv0bia-asciidoc-10.2.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/7xpmbw1xzzwxcd1rnx6qid7zhqnzq3jh-setxkbmap-1.3.4.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/87b385i529h64dzrycf16ksv0jcbzs29-libev-4.33.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/9l94a5gr0wbhaq6zyl30wpqygp1cffrx-pcre2-10.46.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/b8hhyx6rpy47hkbq5wlhrvfrfv3yn7j8-xvfb-run-1+g87f6705.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/bxrnxv90lrpvq06rja47986h057rhwcc-libxcb-cursor-0.1.6.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/cgdz2idkz91w2k7hpb2dymv80938cz9w-libxcb-wm-0.4.2.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/ddvlvaj43mls902nay7ddjrg01d6c2la-perl-5.40.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/ddxlvkpjlg6ycayb6az23ldjdr21xlnf-which-2.23.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/ds5ss96inhkj9x2gbd7shinvbiid6v6b-xorg-server-21.1.21.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/f0yqdlwz2vwsx51wlgmi9pjqpdhbprkx-ninja-1.13.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/gm613dry4hkv26m7ml49fq60z8p0r0gf-perl5.40.0-IPC-Run-20231003.0.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/h3sjzf7hg9ghbh4hzdg6c4byfky2fjng-libx11-1.8.12.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/j5ji7yjwizrma9h72h2pqgi8ir6ah6q8-libstartup-notification-0.12.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/k2jxg4mck2f4pqlisp6slwhyd3pva8wz-source.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/n19ll9p9ivkni2y9l9i2rypyi5gi8z58-perl5.40.0-Inline-C-0.82.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/nm7v937f2z7srs54idjwc7sl6azc1slj-xdotool-3.20211022.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/qzg3b7p4gf4izfjbkc42bjyrvp8vz99k-xcb-util-xrm-1.3.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/rjmh0kp3w170bii9i57z5anlshzm2gll-install-shell-files.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/rrsm8jbqqf58k30cm2lxmgk43fkxsgqp-find-xml-catalogs-hook.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/s4wl1ny41k50rkxw0x0wdjf9l5mjqyv0-libxcb-util-0.4.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/vxckbgl5kwf5ikz0ma0fkavsnh683ry0-libxcb-keysyms-0.4.1.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;dev&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/xxb7x7j73p3sxf03hb1hzaz588avd3yw-docbook-xsl-nons-1.79.2.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;/nix/store/yik59jhh69af5fcvddmxlhfwya69pnzw-perl5.40.0-AnyEvent-I3-0.19.drv&#34;: {
</span></span><span style="display:flex;"><span>        &#34;dynamicOutputs&#34;: {},
</span></span><span style="display:flex;"><span>        &#34;outputs&#34;: [
</span></span><span style="display:flex;"><span>          &#34;out&#34;
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    &#34;inputSrcs&#34;: [
</span></span><span style="display:flex;"><span>      &#34;/nix/store/l622p70vy8k5sh7y5wizi5f2mic6ynpg-source-stdenv.sh&#34;,
</span></span><span style="display:flex;"><span>      &#34;/nix/store/r989dk196nl9frhnfsa1lb7knhbyjxw6-separate-debug-info.sh&#34;,
</span></span><span style="display:flex;"><span>      &#34;/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh&#34;
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    &#34;name&#34;: &#34;i3-4.24&#34;,
</span></span><span style="display:flex;"><span>    &#34;outputs&#34;: {
</span></span><span style="display:flex;"><span>      &#34;debug&#34;: {
</span></span><span style="display:flex;"><span>        &#34;path&#34;: &#34;/nix/store/20rgxn6fpywd229vka9dnjiaprypxirh-i3-4.24-debug&#34;
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      &#34;out&#34;: {
</span></span><span style="display:flex;"><span>        &#34;path&#34;: &#34;/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24&#34;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    &#34;system&#34;: &#34;x86_64-linux&#34;
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div></details>
<p>Unfortunately, I am not aware of a way to go from the derivation to the <code>.nix</code>
source, but at least one can check that a certain source results in an identical
derivation.</p>
<h3 id="developer-builds">Developer builds</h3>
<p>The versioning I have described so far is sufficient for most users, who will
not be interested in tracking intermediate versions of software, but only the
released versions.</p>
<p>But what about developers, or any kind of user who needs more precision?</p>
<p>When building i3 from git, it reports the git revision it was built from, using
<a href="https://manpages.debian.org/git-describe.1"><code>git-describe(1)</code></a>
:</p>
<pre tabindex="0"><code>~/i3/build % git describe
4.25-23-g98f23f54
~/i3/build % ninja
[110/110] Linking target i3
~/i3/build % ./i3 --version
i3 version 4.25-23-g98f23f54 © 2009 Michael Stapelberg and contributors
</code></pre><p>A modified working copy gets represented by a <code>+</code> after the revision:</p>
<pre tabindex="0"><code>~/i3/build % echo &#39;// dirty working copy&#39; &gt;&gt; ../src/main.c &amp;&amp; ninja
[104/104] Linking target i3bar
~/i3/build % ./i3 --version
i3 version 4.25-23-g98f23f54+ © 2009 Michael Stapelberg and contributors
</code></pre><p>Reporting the git revision (or VCS revision, generally speaking) is the most
useful choice.</p>
<p>This way, we catch the following common mistakes:</p>
<ul>
<li>People build from the wrong revision.</li>
<li>People build, but forget to install.</li>
<li>People install, but their session does not pick it up (wrong location?).</li>
</ul>
<h2 id="useful-stamp-vcs-rev">Most Useful: Stamp The VCS Revision</h2>
<p>As we have seen above, the single most useful piece of version information is
the VCS revision. We can fetch all other details (version numbers, dates,
authors, …) from the VCS repository.</p>
<p>Now, let’s demonstrate the best case scenario by looking at how Go does it!</p>
<h3 id="go-vcs">Go always stamps! 🥳</h3>
<p>Go has become my favorite programming language over the years, in big part
because of the good taste and style of the Go developers, and of course also
because of the high-quality tooling:</p>



  <div class="postlink">
      <div>
	<a href="https://michael.stapelberg.ch/posts/2017-08-19-golang_favorite/"><h3>Why Go is my favorite programming language</h3></a>
      </div>
      <div class="summary">
	
	
	
	
	<p>
	  <a href="https://michael.stapelberg.ch/posts/2017-08-19-golang_favorite/">
	  I strive to respect everybody’s personal preferences, so I usually steer clear of debates about which is the best programming language, text editor or operating system.
However, recently I was asked a couple of times why I like and use a lot of Go, so here is a coherent article to fill in the blanks of my ad-hoc in-person ramblings :-).

	  <span class="readmore"><a href="https://michael.stapelberg.ch/posts/2017-08-19-golang_favorite/">Read more →</a></span>
	  </a>
	</p>
      </div>
  </div>


<p>Therefore, I am pleased to say that Go implements the gold standard with regard
to software versioning: it stamps VCS buildinfo by default! 🥳 This was
introduced in <a href="https://go.dev/doc/go1.18#debug_buildinfo">Go 1.18 (March 2022)</a>:</p>
<blockquote>
<p>Additionally, the go command embeds information about the build, including
build and tool tags (set with -tags), compiler, assembler, and linker flags
(like -gcflags), whether cgo was enabled, and if it was, the values of the cgo
environment variables (like CGO_CFLAGS).</p>
<p>Both VCS and build information may be read together with module information
using <code>go version -m file</code> or
<a href="https://pkg.go.dev/runtime/debug#ReadBuildInfo">runtime/debug.ReadBuildInfo</a>
(for the currently running binary) or the new
<a href="https://pkg.go.dev/debug/buildinfo">debug/buildinfo</a> package.</p>
</blockquote>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Note:</strong> Before Go 1.18, the standard approach was to use <code>-ldflags -X main.version=$(git describe)</code> or similar explicit injection. This setup works
(and can still be seen in many places) but requires making changes to the
application code, whereas the Go 1.18+ stamping requires no extra steps.</div>
  </div>
</aside>

<p>What does this mean in practice? Here is a diagram for the common case: building
from git:</p>




<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-common-case-build-from-git.svg"><img
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-common-case-build-from-git.svg"
  alt="diagram showing going from git repository to binary by invoking go build / go install" title="diagram showing going from git repository to binary by invoking go build / go install"
  style="

border: 1px solid #000;

margin-right: 1rem"
  
  loading="lazy"></a>


<p>This covers most of my hobby projects!</p>
<p>Many tools I just <code>go install</code>, or <code>CGO_ENABLED=0 go install</code> if I want to
easily copy them around to other computers. Although, I am managing more and
more of my software in NixOS.</p>
<p>When I find a program that is not yet fully managed, I can use <code>gops</code> and the
<code>go</code> tool to identify it:</p>
<div style="font-size: 85%">
<pre tabindex="0"><code>root@ax52 ~ % nix run nixpkgs#gops
2573594 1       dcs-package-importer  go1.26.1 /nix/store/clby54zb003ibai8j70pwad629lhqfly-dcs-unstable/bin/dcs-package-importer
2573576 1       dcs-source-backend    go1.26.1 /nix/store/clby54zb003ibai8j70pwad629lhqfly-dcs-unstable/bin/dcs-source-backend
2573566 1       debiman               go1.25.5 /srv/man/bin/debiman
[…]
root@ax52 ~ % nix run nixpkgs#go -- version -m /srv/man/bin/debiman
/srv/man/bin/debiman: go1.25.5
  path	github.com/Debian/debiman/cmd/debiman
  mod	github.com/Debian/debiman	v0.0.0-20251230101540-ac8f5391b43b+dirty
  […]
  dep	pault.ag/go/debian	v0.18.0	h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
  dep	pault.ag/go/topsort	v0.1.1	h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4=
  build	-buildmode=exe
  build	-compiler=gc
  build	DefaultGODEBUG=containermaxprocs=0,decoratemappings=0,tlssha1=1,updatemaxprocs=0,x509sha256skid=0
  build	CGO_ENABLED=0
  build	GOARCH=amd64
  build	GOOS=linux
  build	GOAMD64=v1
  build	vcs=git
  build	vcs.revision=ac8f5391b43bc1a9dbdc99f6179e2fb7d7414a04
  build	vcs.time=2025-12-30T10:15:40Z
  build	vcs.modified=true
root@ax52 ~ %
</code></pre></div>
<p>It’s very cool that Go does the right thing by default!</p>
<p>Systems that consist of 100% Go software (like my <a href="https://gokrazy.org/">gokrazy Go appliance
platform</a>) are fully stamped! For example, the gokrazy web
interface shows me exactly which version and dependencies went into the
<code>gokrazy/rsync</code> build on my <a href="https://github.com/stapelberg/scan2drive">scan2drive
appliance</a>.</p>
<p>Despite being fully stamped, note that gokrazy only shows the module versions,
and no VCS buildinfo, because it currently suffers from the same gap as Nix:</p>















<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-03-29-gokrazy-scan2drive-rsync.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-03-29-gokrazy-scan2drive-rsync_hu_b9f20b08c5492d94.png 2x,https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-03-29-gokrazy-scan2drive-rsync_hu_ec0d408518026341.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-03-29-gokrazy-scan2drive-rsync_hu_16984c2d1be3db9c.png"
  alt="gokrazy scan2drive rsync" title="gokrazy scan2drive rsync"
  width="600"
  height="433"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h3 id="go-version-reporting">Go Version Reporting</h3>
<p>For the gokrazy packer, which follows a rolling release model (no version
numbers), I ended up with a few lines of Go code (see below) to display a git
revision, no matter if you installed the packer from a Go module or from a git
working copy.</p>
<p>The code either displays <code>vcs.revision</code> (the easy case; built from git) or
extracts the revision from the Go module version of the main module
(<a href="https://pkg.go.dev/runtime/debug#BuildInfo"><code>BuildInfo.Main.Version</code></a>):</p>
<p>What are the other cases? These examples illustrate the scenarios I usually deal
with:</p>
<table>
  <thead>
      <tr>
          <th>source (built from)</th>
          <th>buildinfo (stamped into program)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>directory (no git)</td>
          <td>module <code>(devel)</code></td>
      </tr>
      <tr>
          <td>Go module</td>
          <td>module <code>v0.3.1-0.20260105212325-5347ac5f5bcb</code></td>
      </tr>
      <tr>
          <td>directory (git)</td>
          <td>module <code>v0.0.0-20260131174001-ccb1d233f2a4+dirty</code></td>
      </tr>
      <tr>
          <td></td>
          <td><code>vcs.revision=ccb1d233f2a43e9118b9146b3c9a5ded1efb7551</code></td>
      </tr>
      <tr>
          <td></td>
          <td><code>vcs.time=2026-01-31T17:40:01Z</code></td>
      </tr>
      <tr>
          <td></td>
          <td><code>vcs.modified=true</code></td>
      </tr>
  </tbody>
</table>




<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-go-install-git-vs-module.svg"><img
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-go-install-git-vs-module.svg"
  alt="diagram showing the two cases with go build info stamping: building from a git checkout or installing from a Go module" title="diagram showing the two cases with go build info stamping: building from a git checkout or installing from a Go module"
  style="

border: 1px solid #000;

margin-right: 1rem"
  
  loading="lazy"></a>


<details>
<summary>Go code to programmatically read the version</summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">package</span><span style="color:#bbb"> </span>version<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">import</span><span style="color:#bbb"> </span>(<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;runtime/debug&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;strings&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">readParts</span>()<span style="color:#bbb"> </span>(revision<span style="color:#bbb"> </span><span style="color:#902000">string</span>,<span style="color:#bbb"> </span>modified,<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span><span style="color:#902000">bool</span>)<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>info,<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>debug.<span style="color:#06287e">ReadBuildInfo</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>!ok<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;&#34;</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>settings<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020">make</span>(<span style="color:#007020;font-weight:bold">map</span>[<span style="color:#902000">string</span>]<span style="color:#902000">string</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">for</span><span style="color:#bbb"> </span>_,<span style="color:#bbb"> </span>s<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">range</span><span style="color:#bbb"> </span>info.Settings<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>settings[s.Key]<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>s.Value<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// When built from a local VCS directory, we can use vcs.revision directly.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>rev,<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>settings[<span style="color:#4070a0">&#34;vcs.revision&#34;</span>];<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>rev,<span style="color:#bbb"> </span>settings[<span style="color:#4070a0">&#34;vcs.modified&#34;</span>]<span style="color:#bbb"> </span><span style="color:#666">==</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;true&#34;</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">true</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// When built as a Go module (not from a local VCS directory),</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// info.Main.Version is something like v0.0.0-20230107144322-7a5757f46310.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>v<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>info.Main.Version<span style="color:#bbb"> </span><span style="color:#60a0b0;font-style:italic">// for convenience</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>idx<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>strings.<span style="color:#06287e">LastIndexByte</span>(v,<span style="color:#bbb"> </span><span style="color:#4070a0">&#39;-&#39;</span>);<span style="color:#bbb"> </span>idx<span style="color:#bbb"> </span>&gt;<span style="color:#bbb"> </span><span style="color:#666">-</span><span style="color:#40a070">1</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>v[idx<span style="color:#666">+</span><span style="color:#40a070">1</span>:],<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">true</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;&lt;BUG&gt;&#34;</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">false</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">Read</span>()<span style="color:#bbb"> </span><span style="color:#902000">string</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>revision,<span style="color:#bbb"> </span>modified,<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">readParts</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>!ok<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;&lt;not okay&gt;&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>modifiedSuffix<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>modified<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>modifiedSuffix<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span><span style="color:#4070a0">&#34; (modified)&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;https://github.com/gokrazy/tools/commit/&#34;</span><span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span>revision<span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span>modifiedSuffix<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>}<span style="color:#bbb">
</span></span></span></code></pre></div></details>
<p>This is what it looks like in practice:</p>
<pre tabindex="0"><code>% go install github.com/gokrazy/tools/cmd/gok@latest
% gok --version
https://github.com/gokrazy/tools/commit/8ed49b4fafc7
</code></pre><p>But a version built from git has the full revision available (→ you can tell them apart):</p>
<pre tabindex="0"><code>% (cd ~gokrazy/../tools &amp;&amp; go install ./cmd/...)
% gok --version
https://github.com/gokrazy/tools/commit/ba6a8936f4a88ddcf20a3b8f625e323e65664aa6 (modified)
</code></pre><h2 id="vcs-rev-with-nixos">VCS rev with NixOS</h2>
<p>When packaging Go software with Nix, it’s easy to lose Go VCS revision stamping:</p>
<ol>
<li>Nix fetchers like <code>fetchFromGitHub</code> are implemented by fetching an archive
(<code>.tar.gz</code>) file from GitHub — the full <code>.git</code> repository is not transferred,
which is more efficient.</li>
<li>Even if a <code>.git</code> repository is present, Nix usually intentionally removes it
for reproducibility: <code>.git</code> directories contain packed objects that change
across <code>git gc</code> runs (for example), which would break reproducible builds
(different hash for the same source).</li>
</ol>
<p>So the fundamental tension here is between reproducibility and VCS stamping.</p>
<p>Luckily, there is a solution that works for both: I created the
<a href="https://github.com/stapelberg/nix"><code>stapelberg/nix/go-vcs-stamping</code> Nix overlay
module</a> that you can import to get working Go
VCS revision stamping by default for your <code>buildGoModule</code> Nix expressions!</p>




<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-vcs-rev-with-nix.svg"><img
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-vcs-rev-with-nix.svg"
  alt="diagram from Git repo to go build without and with my go-vcs-stamping overlay workaround" title="diagram from Git repo to go build without and with my go-vcs-stamping overlay workaround"
  style="

border: 1px solid #000;

margin-right: 1rem"
  
  loading="lazy"></a>


<h3 id="nix-go-build-detail">The Nix Go build situation in detail</h3>
<p><strong>Tip:</strong> If you are not a Nix user, feel free to skip over this section. I
included it in this article so that you have a full example of making VCS
stamping work in the most complicated environments.</p>
<hr>
<p>Packaging Go software in Nix is pleasantly straightforward.</p>
<p>For example, the Go Protobuf generator plugin <code>protoc-gen-go</code> is packaged in Nix
with &lt;30 lines: <a href="https://github.com/NixOS/nixpkgs/blob/e347ac28905f77edcd1e9855dedcfb61e517f265/pkgs/by-name/pr/protoc-gen-go/package.nix">official nixpkgs <code>protoc-gen-go</code>
package.nix</a>. You
call
<a href="https://nixos.org/manual/nixpkgs/stable/#ssec-language-go"><code>buildGoModule</code></a>,
supply as <code>src</code> the result from
<a href="https://nixos.org/manual/nixpkgs/stable/#fetchfromgithub"><code>fetchFromGitHub</code></a>
and add a few lines of metadata.</p>
<p>But getting developer builds fully stamped is not straightforward at all!</p>
<p>When packaging my own software, I want to package individual revisions
(developer builds), not just released versions. I use the same <code>buildGoModule</code>,
or <code>buildGoLatestModule</code> if I need the latest Go version. Instead of using
<code>fetchFromGitHub</code>, I provide my sources using Flakes, usually also from GitHub
or from another Git repository. For example, I package <code>gokrazy/bull</code> like so:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  pkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  pkgs-unstable<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  bullsrc<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#666">...</span>
</span></span><span style="display:flex;"><span>}:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># Use buildGoLatestModule to build with Go 1.26</span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># even before NixOS 26.05 Yarara is released</span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># (NixOS 25.11 contains Go 1.25).</span>
</span></span><span style="display:flex;"><span>pkgs-unstable<span style="color:#666">.</span>buildGoLatestModule {
</span></span><span style="display:flex;"><span>  pname <span style="color:#666">=</span> <span style="color:#4070a0">&#34;bull&#34;</span>;
</span></span><span style="display:flex;"><span>  version <span style="color:#666">=</span> <span style="color:#4070a0">&#34;unstable&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  src <span style="color:#666">=</span> bullsrc;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Needs changing whenever `go mod vendor` changes,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># i.e. whenever go.mod is updated to use different versions.</span>
</span></span><span style="display:flex;"><span>  vendorHash <span style="color:#666">=</span> <span style="color:#4070a0">&#34;sha256-sU5j2dji5bX2rp+qwwSFccXNpK2LCpWJq4Omz/jmaXU=&#34;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>bullsrc</code> comes from my <code>flake.nix</code>:</p>
<details>
<summary>Click here to expand the full <code>flake.nix</code></summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  inputs <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    nixpkgs<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-25.11&#34;</span>;
</span></span><span style="display:flex;"><span>    nixpkgs-unstable<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-unstable&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    disko <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nix-community/disko&#34;</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#60a0b0;font-style:italic"># Use the same version as nixpkgs</span>
</span></span><span style="display:flex;"><span>      inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    stapelbergnix<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/nix&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    zkjnastools<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/zkj-nas-tools&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    configfiles <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/configfiles&#34;</span>;
</span></span><span style="display:flex;"><span>      flake <span style="color:#666">=</span> <span style="color:#60add5">false</span>; <span style="color:#60a0b0;font-style:italic"># repo is not a flake</span>
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    bullsrc <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:gokrazy/bull&#34;</span>;
</span></span><span style="display:flex;"><span>      flake <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    sops-nix <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:Mic92/sops-nix&#34;</span>;
</span></span><span style="display:flex;"><span>      inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  outputs <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      nixpkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      nixpkgs-unstable<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      disko<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      stapelbergnix<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      zkjnastools<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      bullsrc<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      configfiles<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      sops-nix<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      <span style="color:#666">...</span>
</span></span><span style="display:flex;"><span>    }:
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>      system <span style="color:#666">=</span> <span style="color:#4070a0">&#34;x86_64-linux&#34;</span>;
</span></span><span style="display:flex;"><span>      pkgs <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      pkgs-unstable <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs-unstable {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      nixosConfigurations<span style="color:#666">.</span>keep <span style="color:#666">=</span> nixpkgs<span style="color:#666">.</span>lib<span style="color:#666">.</span>nixosSystem {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> pkgs;
</span></span><span style="display:flex;"><span>        specialArgs <span style="color:#666">=</span> { <span style="color:#007020;font-weight:bold">inherit</span> configfiles; };
</span></span><span style="display:flex;"><span>        modules <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>          disko<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>disko
</span></span><span style="display:flex;"><span>          sops-nix<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>sops
</span></span><span style="display:flex;"><span>          <span style="color:#235388">./configuration.nix</span>
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>userSettings
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>zshConfig
</span></span><span style="display:flex;"><span>          <span style="color:#60a0b0;font-style:italic"># Use systemd for network configuration</span>
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>systemdNetwork
</span></span><span style="display:flex;"><span>          <span style="color:#60a0b0;font-style:italic"># Use systemd-boot as bootloader</span>
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>systemdBoot
</span></span><span style="display:flex;"><span>          <span style="color:#60a0b0;font-style:italic"># Run prometheus node exporter in tailnet</span>
</span></span><span style="display:flex;"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>prometheusNode
</span></span><span style="display:flex;"><span>          zkjnastools<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>zkjbackup
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          {
</span></span><span style="display:flex;"><span>            nixpkgs<span style="color:#666">.</span>overlays <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>              (final: prev: {
</span></span><span style="display:flex;"><span>                bull <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> <span style="color:#235388">./bull-pkg.nix</span> {
</span></span><span style="display:flex;"><span>                  pkgs <span style="color:#666">=</span> final;
</span></span><span style="display:flex;"><span>                  pkgs-unstable <span style="color:#666">=</span> pkgs-unstable;
</span></span><span style="display:flex;"><span>                  <span style="color:#007020;font-weight:bold">inherit</span> bullsrc;
</span></span><span style="display:flex;"><span>                };
</span></span><span style="display:flex;"><span>              })
</span></span><span style="display:flex;"><span>            ];
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        ];
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      formatter<span style="color:#666">.</span><span style="color:#70a0d0">${</span>system<span style="color:#70a0d0">}</span> <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>nixfmt-tree;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></details>
<p>Go stamps all builds, but it does not have much to stamp here:</p>
<ul>
<li>We build from a directory, not a Go module, so the module version is <code>(devel)</code>.</li>
<li>The stamped buildinfo does not contain any <code>vcs</code> information.</li>
</ul>
<p>Here’s a full example of gokrazy/bull:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>% go version -m \
</span></span><span style="display:flex;"><span>  /nix/store/z3y90ck0fp1wwd4scljffhwxcrxjhb9j-bull-unstable/bin/bull
</span></span><span style="display:flex;"><span>/nix/store/z3y90ck0fp1wwd4scljffhwxcrxjhb9j-bull-unstable/bin/bull: go1.26.1
</span></span><span style="display:flex;"><span>        path    github.com/gokrazy/bull/cmd/bull
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        mod     github.com/gokrazy/bull (devel) 
</span></span><span style="display:flex;"><span>        dep     github.com/BurntSushi/toml      v1.4.1-0.20240526193622-a339e1f7089c    
</span></span><span style="display:flex;"><span>        dep     github.com/fsnotify/fsnotify    v1.8.0  
</span></span><span style="display:flex;"><span>        dep     github.com/google/renameio/v2   v2.0.2  
</span></span><span style="display:flex;"><span>        dep     github.com/yuin/goldmark        v1.7.8  
</span></span><span style="display:flex;"><span>        dep     go.abhg.dev/goldmark/wikilink   v0.5.0  
</span></span><span style="display:flex;"><span>        dep     golang.org/x/image      v0.23.0 
</span></span><span style="display:flex;"><span>        dep     golang.org/x/sync       v0.10.0 
</span></span><span style="display:flex;"><span>        dep     golang.org/x/sys        v0.28.0 
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   -buildmode=exe
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   -compiler=gc
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   -trimpath=true
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   CGO_ENABLED=0
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   GOARCH=amd64
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   GOOS=linux
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        build   GOAMD64=v1</span></span></code></pre></div>
<p>To fix VCS stamping, add my <code>goVcsStamping</code> overlay to your <code>nixosSystem.modules</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  nixpkgs<span style="color:#666">.</span>overlays <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    stapelbergnix<span style="color:#666">.</span>overlays<span style="color:#666">.</span>goVcsStamping
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>(If you are using <code>nixpkgs-unstable</code>, like I am, you need to apply the overlay in both places.)</p>
<p>After rebuilding, your Go binaries should newly be stamped with <code>vcs</code> buildinfo:</p>
<pre tabindex="0"><code>% go version -m /nix/store/z8mgsf10pkc6dgvi8pfnbb7cs23pqfkn-bull-unstable/bin/bull
[…]
  build   vcs=git
  build   vcs.revision=c0134ef21d37e4ca8346bdcb7ce492954516aed5
  build   vcs.time=2026-03-22T08:32:55Z
  build   vcs.modified=false
</code></pre><p>Nice! 🥳 But… how does it work? When does it apply? How do you know how to fix
your config?</p>
<p>I’ll show you <strong>the full diagram</strong> first, and then explain how to read it:</p>




<a href="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-nix-big-picture.svg"><img
  src="https://michael.stapelberg.ch/posts/2026-04-05-stamp-it-all-programs-must-report-their-version/2026-04-05-lea-nix-big-picture.svg"
  alt="a big diagram showing all the ways from .nix expression to a stamped binary or a binary where VCS info got lost" title="a big diagram showing all the ways from .nix expression to a stamped binary or a binary where VCS info got lost"
  style="

border: 1px solid #000;

margin-right: 1rem"
  
  loading="lazy"></a>


<p>There are 3 relevant parts of the Nix stack that you can end up in, depending on
what you write into your <code>.nix</code> files:</p>
<ol>
<li>Fetchers. These are what Flakes use, but also non-Flake use-cases.</li>
<li>Fixed-output derivations (FOD). This is how <code>pkgs.fetchgit</code> is implemented,
but the constant hash churn (updating the <code>sha256</code> line) inherent to FODs is
annoying.</li>
<li>Copiers. These just copy files into the Nix store and are not git-aware.</li>
</ol>
<p>For the purpose of VCS revision stamping, you should:</p>
<ul>
<li>Avoid the Copiers! If you use Flakes:
<ul>
<li>❌ do not use <code>url = &quot;/home/michael/dcs&quot;</code> as a Flake input</li>
<li>✅ use <code>url = &quot;git+file:///home/michael/dcs&quot;</code> instead for git awareness</li>
</ul>
</li>
<li>I avoid the fixed-output derivation (FOD) as well.
<ul>
<li>Fetching the git repository at build time is slow and inefficient.</li>
<li>Enabling <code>leaveDotGit</code>, which is needed for VCS revision stamping with this
approach, is even more inefficient because a new Git repository must be
constructed deterministically to keep the FOD reproducible.</li>
</ul>
</li>
</ul>
<p>Hence, we will stick to the left-most column: fetchers.</p>
<p>Unfortunately, by default, with fetchers, the VCS revision information, which is
stored in a Nix attrset (in-memory, during the build process), does not make it
into the Nix store, hence, when the Nix derivation is evaluated and Go compiles
the source code, Go does not see any VCS revision.</p>
<p>My <a href="https://github.com/stapelberg/nix"><code>stapelberg/nix/go-vcs-stamping</code> Nix overlay
module</a> fixes this, and enabling the overlay
is how you end up in the left-most lane of the above diagram: the happy path,
where your Go binaries are now stamped!</p>
<h3 id="nixos-buildinfo-overlay">My workaround: Nix git buildinfo overlay</h3>
<p>How does the <code>go-vcs-stamping</code> overlay work? It functions as an adapter between
Nix and Go:</p>
<ul>
<li>Nix tracks the VCS revision in the <code>.rev</code> in-memory attrset.</li>
<li>Go expects to find the VCS revision in a <code>.git</code> repository, accessed via
<code>.git/HEAD</code> file access and <a href="https://manpages.debian.org/git.1"><code>git(1)</code></a>
 commands.</li>
</ul>
<p>So the overlay implements 3 steps to get Go to stamp the correct info:</p>
<ol>
<li>It synthesizes a <code>.git/HEAD</code> file so that Go’s <code>vcs.FromDir()</code> detects a git
repository.</li>
<li>It injects a <code>git</code> command into the <code>PATH</code> that implements exactly the two
commands used by Go and fails loudly on anything else (in case Go updates its
implementation).</li>
<li>It sets <code>-buildvcs=true</code> in the <code>GOFLAGS</code> environment variable.</li>
</ol>
<p>For the full source, see
<a href="https://github.com/stapelberg/nix/blob/main/go-vcs-stamping.nix"><code>go-vcs-stamping.nix</code></a>.</p>
<h3 id="clean-fix">The clean fix</h3>
<p>See <a href="https://github.com/golang/go/issues/77020">Go issue #77020</a> and <a href="https://github.com/golang/go/issues/64162">Go issue
#64162</a> for a cleaner approach to
fixing this gap: allowing package managers to invoke the Go tool with the
correct VCS information injected.</p>
<p>This would allow Nix (or also gokrazy) to pass along buildinfo cleanly, without
the need for <a href="#nixos-buildinfo-overlay">workarounds like my <code>go-vcs-stamping</code>
adapter</a>.</p>
<p>At the time of writing, issue #77020 does not seem to have much traction and is
still open.</p>
<h2 id="conclusion-stamp-it-plumb-it-report-it">Conclusion: Stamp it! Plumb it! Report it!</h2>
<p>My argument is simple:</p>
<p><strong>Stamping the VCS revision is conceptually easy, but very important!</strong></p>
<p>For example, if the production system from the incident I mentioned had reported
its version, we would have saved multiple hours of mitigation time!</p>
<p>Unfortunately, many environments only identify the build output (useful, but
orthogonal), but do not plumb the VCS revision (much more useful!), or at least
not by default.</p>
<p>Your action plan to fix it is just 3 simple steps:</p>
<ol>
<li>Stamp it! Include the source VCS revision in your programs.
<ul>
<li>This is not a new idea: <a href="https://i3wm.org">i3</a> builds include their <a href="https://manpages.debian.org/git-describe.1"><code>git-describe(1)</code></a>
 revision since 2012!</li>
</ul>
</li>
<li>Plumb it! When building / packaging, ensure the VCS revision does not get lost.
<ul>
<li>My <a href="#vcs-rev-with-nixos">“VCS rev with NixOS”</a> case study section above
illustrates several reasons why the VCS rev could get lost, which paths
can work and how to fix the missing plumbing.</li>
</ul>
</li>
<li>Report it! Make your software print its VCS revision on every relevant
surface, for example:
<ul>
<li><strong>Executable programs:</strong> Report the VCS revision when run with <code>--version</code>
<ul>
<li>For Go programs, you can always use <code>go version -m</code></li>
</ul>
</li>
<li><strong>Services and batch jobs:</strong> Include the VCS revision in the startup logs.</li>
<li><strong>Outgoing HTTP requests:</strong> Include the VCS revision in the <code>User-Agent</code></li>
<li><strong>HTTP responses:</strong> Include the VCS revision in a header (internally)</li>
<li><strong>Remote Procedure Calls (RPCs):</strong> Include the revision in RPC metadata</li>
<li><strong>User Interfaces:</strong> Expose the revision somewhere visible for debugging.</li>
</ul>
</li>
</ol>
<p>Implementing “version observability” throughout your system is a one-day
high-ROI project.</p>
<p>With my Nix example, you saw how the VCS revision is available throughout the
stack, but can get lost in the middle. Hopefully my resources help you quickly
fix your stack(s), too:</p>
<ul>
<li><a href="https://github.com/stapelberg/nix">My <code>stapelberg/nix/go-vcs-stamping</code>
overlay</a> for Nix / NixOS</li>
<li><a href="https://github.com/stapelberg/stampit">My <code>stampit</code> repository</a> is a
community resource to collect examples (as markdown content) and includes a Go
module with a few helpers to make version reporting trivial.</li>
</ul>
<p><em><strong>Now go stamp your programs and data transfers! 🚀</strong></em></p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Coding Agent VMs on NixOS with microvm.nix]]></title>
    <link href="https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/"/>
    <id>https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/</id>
    <published>2026-02-01T09:00:00+01:00</published>
    <content type="html"><![CDATA[<p>I have come to appreciate <a href="https://en.wikipedia.org/wiki/AI-assisted_software_development">coding
agents</a> to be
valuable tools for working with computer program code in any capacity, such as
learning about any program’s architecture, diagnosing bugs or developing proofs
of concept. Depending on the use-case, reviewing each command the agent wants to
run can get tedious and time-consuming very quickly. To safely run a coding
agent without review, I wanted a Virtual Machine (VM) solution where the agent
has no access to my personal files and where it’s no big deal if the agent gets
compromised by malware: I can just throw away the VM and start over.</p>
<p>Instead of setting up a stateful VM and re-installing it when needed (ugh!), I
prefer the model of ephemeral VMs where nothing persists on disk, except for
what is explicitly shared with the host.</p>
<p>The <a href="https://github.com/microvm-nix/microvm.nix"><code>microvm.nix</code> project</a> makes it
easy to create such VMs on NixOS, and this article shows you how I like to set
up my VMs.</p>
<h2 id="see-also">See also</h2>
<p>If you haven’t heard of NixOS before, check out <a href="https://en.wikipedia.org/wiki/NixOS">the NixOS Wikipedia
page</a> and
<a href="https://nixos.org/">nixos.org</a>. I <a href="/talks/#2025">spoke about why I switched to Nix in
2025</a> and have published a <a href="/posts/tags/nix/">few blog posts about
Nix</a>.</p>
<p>For understanding the threat model of AI agents, read <a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/">Simon Willison’s “The
lethal trifecta for AI agents: private data, untrusted content, and external
communication” (June
2025)</a>. This
article’s approach to working with the threat model is to remove the “private
data” part from the equation.</p>
<p>If you want to learn about the whole field of sandboxing, check out <a href="https://www.luiscardoso.dev/blog/sandboxes-for-ai">Luis
Cardoso’s “A field guide to sandboxes for AI” (Jan
2026)</a>. I will not be
comparing different solutions in this article, I will just show you one possible
path.</p>
<p>And lastly, maybe you’re not in the mood to build/run sandboxing infrastructure
yourself. Good news: Sandboxing is a hot topic and there are many commercial
offerings popping up that address this need. For example, David Crawshaw and
Josh Bleecher Snyder (I know both from the Go community) recently launched
<a href="https://blog.exe.dev/meet-exe.dev">exe.dev</a>, an agent-friendly VM hosting
service. Another example is <a href="https://fly.io/blog/code-and-let-live/">Fly.io, who launched
Sprites</a>.</p>
<h2 id="setting-up-microvmnix">Setting up microvm.nix</h2>
<p>Let’s jump right in! The next sections walk you through how I set up my config.</p>
<h3 id="step-1-network-prep">Step 1: network prep</h3>
<p>First, I created a new <code>microbr</code> bridge which uses <code>192.168.33.1/24</code> as IP address range and NATs out of the <code>eno1</code> network interface. All <code>microvm*</code> interfaces will be added to that bridge:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>netdevs<span style="color:#666">.</span><span style="color:#4070a0">&#34;20-microbr&#34;</span><span style="color:#666">.</span>netdevConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>  Kind <span style="color:#666">=</span> <span style="color:#4070a0">&#34;bridge&#34;</span>;
</span></span><span style="display:flex;"><span>  Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microbr&#34;</span>;
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>networks<span style="color:#666">.</span><span style="color:#4070a0">&#34;20-microbr&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>  matchConfig<span style="color:#666">.</span>Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microbr&#34;</span>;
</span></span><span style="display:flex;"><span>  addresses <span style="color:#666">=</span> [ { Address <span style="color:#666">=</span> <span style="color:#4070a0">&#34;192.168.83.1/24&#34;</span>; } ];
</span></span><span style="display:flex;"><span>  networkConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    ConfigureWithoutCarrier <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>networks<span style="color:#666">.</span><span style="color:#4070a0">&#34;21-microvm-tap&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>  matchConfig<span style="color:#666">.</span>Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microvm*&#34;</span>;
</span></span><span style="display:flex;"><span>  networkConfig<span style="color:#666">.</span>Bridge <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microbr&#34;</span>;
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>networking<span style="color:#666">.</span>nat <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>  enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  internalInterfaces <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;microbr&#34;</span> ];
</span></span><span style="display:flex;"><span>  externalInterface <span style="color:#666">=</span> <span style="color:#4070a0">&#34;eno1&#34;</span>;
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><h3 id="step-2-flakenix">Step 2: <code>flake.nix</code></h3>
<p>Then, I added the <code>microvm</code> module as a new input to my <code>flake.nix</code> (check out
the <a href="https://microvm-nix.github.io/microvm.nix/">microvm.nix documentation</a> for
details) and enabled the <code>microvm.nixosModules.host</code> module on the NixOS
configuration for my PC (midna). I also created a new <code>microvm.nix</code> file, in
which I declare all my VMs. Here’s what my <code>flake.nix</code> looks like:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  inputs <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    nixpkgs <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-25.11&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#60a0b0;font-style:italic"># For more recent claude-code</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    nixpkgs-unstable <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-unstable&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    };
</span></span><span style="display:flex;"><span>    stapelbergnix <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/nix&#34;</span>;
</span></span><span style="display:flex;"><span>      inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>    zkjnastools <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/zkj-nas-tools&#34;</span>;
</span></span><span style="display:flex;"><span>      inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    microvm <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:microvm-nix/microvm.nix&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    };
</span></span><span style="display:flex;"><span>    home-manager <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nix-community/home-manager/release-25.11&#34;</span>;
</span></span><span style="display:flex;"><span>      inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>    configfiles <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/configfiles&#34;</span>;
</span></span><span style="display:flex;"><span>      flake <span style="color:#666">=</span> <span style="color:#60add5">false</span>; <span style="color:#60a0b0;font-style:italic"># repo is not a flake</span>
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  outputs <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      self<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      stapelbergnix<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      zkjnastools<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      nixpkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      nixpkgs-unstable<span style="color:#666">,</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      microvm<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      home-manager<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      configfiles<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>    }<span style="color:#666">@</span>inputs:
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>      system <span style="color:#666">=</span> <span style="color:#4070a0">&#34;x86_64-linux&#34;</span>;
</span></span><span style="display:flex;"><span>      pkgs <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      pkgs-unstable <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs-unstable {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      nixosConfigurations <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>        midna <span style="color:#666">=</span> nixpkgs<span style="color:#666">.</span>lib<span style="color:#666">.</span>nixosSystem {
</span></span><span style="display:flex;"><span>          system <span style="color:#666">=</span> <span style="color:#4070a0">&#34;x86_64-linux&#34;</span>;
</span></span><span style="display:flex;"><span>          specialArgs <span style="color:#666">=</span> { <span style="color:#007020;font-weight:bold">inherit</span> inputs; };
</span></span><span style="display:flex;"><span>          modules <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>            (<span style="color:#007020;font-weight:bold">import</span> <span style="color:#235388">./configuration.nix</span>)
</span></span><span style="display:flex;"><span>            stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>userSettings
</span></span><span style="display:flex;"><span>            <span style="color:#60a0b0;font-style:italic"># Use systemd for network configuration</span>
</span></span><span style="display:flex;"><span>            stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>systemdNetwork
</span></span><span style="display:flex;"><span>            <span style="color:#60a0b0;font-style:italic"># Use systemd-boot as bootloader</span>
</span></span><span style="display:flex;"><span>            stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>systemdBoot
</span></span><span style="display:flex;"><span>            <span style="color:#60a0b0;font-style:italic"># Run prometheus node exporter in tailnet</span>
</span></span><span style="display:flex;"><span>            stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>prometheusNode
</span></span><span style="display:flex;"><span>            zkjnastools<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>zkjbackup
</span></span><span style="display:flex; background-color:#d8d8d8"><span>            microvm<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>host
</span></span><span style="display:flex; background-color:#d8d8d8"><span>            <span style="color:#235388">./microvm.nix</span>
</span></span><span style="display:flex;"><span>          ];
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h3 id="step-3-microvmnix">Step 3: <code>microvm.nix</code></h3>
<p>The following <code>microvm.nix</code> declares two microvms, one for Emacs (about which I wanted to learn more) and one for Go Protobuf, a code base I am familiar with and can use to understand Claude’s capabilities:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  config<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  lib<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  pkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  inputs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#666">...</span>
</span></span><span style="display:flex;"><span>}:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>  <span style="color:#007020;font-weight:bold">inherit</span> (inputs)
</span></span><span style="display:flex;"><span>    nixpkgs-unstable
</span></span><span style="display:flex;"><span>    stapelbergnix
</span></span><span style="display:flex;"><span>    microvm
</span></span><span style="display:flex;"><span>    configfiles
</span></span><span style="display:flex;"><span>    home-manager
</span></span><span style="display:flex;"><span>    ;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  microvmBase <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> <span style="color:#235388">./microvm-base.nix</span>;
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  microvm<span style="color:#666">.</span>vms<span style="color:#666">.</span>emacsvm <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    autostart <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>    config <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      imports <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>        stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>userSettings
</span></span><span style="display:flex;"><span>        microvm<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>microvm
</span></span><span style="display:flex;"><span>        (microvmBase {
</span></span><span style="display:flex;"><span>          hostName <span style="color:#666">=</span> <span style="color:#4070a0">&#34;emacsvm&#34;</span>;
</span></span><span style="display:flex;"><span>          ipAddress <span style="color:#666">=</span> <span style="color:#4070a0">&#34;192.168.83.6&#34;</span>;
</span></span><span style="display:flex;"><span>          tapId <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microvm4&#34;</span>;
</span></span><span style="display:flex;"><span>          mac <span style="color:#666">=</span> <span style="color:#4070a0">&#34;02:00:00:00:00:05&#34;</span>;
</span></span><span style="display:flex;"><span>          workspace <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael/microvm/emacs&#34;</span>;
</span></span><span style="display:flex;"><span>          <span style="color:#007020;font-weight:bold">inherit</span>
</span></span><span style="display:flex;"><span>            nixpkgs-unstable
</span></span><span style="display:flex;"><span>            configfiles
</span></span><span style="display:flex;"><span>            home-manager
</span></span><span style="display:flex;"><span>            stapelbergnix
</span></span><span style="display:flex;"><span>            ;
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>        <span style="color:#235388">./microvms/emacs.nix</span>
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  microvm<span style="color:#666">.</span>vms<span style="color:#666">.</span>goprotobufvm <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    autostart <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>    config <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      imports <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>        stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>userSettings
</span></span><span style="display:flex;"><span>        microvm<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>microvm
</span></span><span style="display:flex;"><span>        (microvmBase {
</span></span><span style="display:flex;"><span>          hostName <span style="color:#666">=</span> <span style="color:#4070a0">&#34;goprotobufvm&#34;</span>;
</span></span><span style="display:flex;"><span>          ipAddress <span style="color:#666">=</span> <span style="color:#4070a0">&#34;192.168.83.7&#34;</span>;
</span></span><span style="display:flex;"><span>          tapId <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microvm5&#34;</span>;
</span></span><span style="display:flex;"><span>          mac <span style="color:#666">=</span> <span style="color:#4070a0">&#34;02:00:00:00:00:06&#34;</span>;
</span></span><span style="display:flex;"><span>          workspace <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael/microvm/goprotobuf&#34;</span>;
</span></span><span style="display:flex;"><span>          <span style="color:#007020;font-weight:bold">inherit</span>
</span></span><span style="display:flex;"><span>            nixpkgs-unstable
</span></span><span style="display:flex;"><span>            configfiles
</span></span><span style="display:flex;"><span>            home-manager
</span></span><span style="display:flex;"><span>            stapelbergnix
</span></span><span style="display:flex;"><span>            ;
</span></span><span style="display:flex;"><span>          extraZshInit <span style="color:#666">=</span> <span style="color:#4070a0">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">            export GOPATH=$HOME/go
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">            export PATH=$GOPATH/bin:$PATH
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>        <span style="color:#235388">./microvms/goprotobuf.nix</span>
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="step-4-microvm-basenix">Step 4: <code>microvm-base.nix</code></h3>
<p>The <code>microvm-base.nix</code> module takes these parameters and declares:</p>
<ul>
<li>Network settings: I like using <a href="https://manpages.debian.org/systemd-networkd.8"><code>systemd-networkd(8)</code></a>
 and <a href="https://manpages.debian.org/systemd-resolved.8"><code>systemd-resolved(8)</code></a>
.</li>
<li>Shared directories for:
<ul>
<li>the workspace directory, e.g. <code>~/microvm/emacs</code></li>
<li>the host’s Nix store, so the VM can access software from cache (often)</li>
<li>this VM’s SSH host keys</li>
<li><code>~/claude-microvm</code>, which is a separate state directory, used only on the microvms.</li>
</ul>
</li>
<li>an 8 GB disk overlay (var.img), stored in <code>/var/lib/microvms/&lt;name&gt;</code></li>
<li><code>cloud-hypervisor</code> (QEMU also works well!) as the hypervisor, with 8 vCPUs and 4 GB RAM.</li>
<li>A workaround for systemd trying to unmount <code>/nix/store</code> (which causes a deadlock).</li>
</ul>
<details>
<summary>Expand full <code>microvm-base.nix</code> code</summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  hostName<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  ipAddress<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  tapId<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  mac<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  workspace<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  nixpkgs-unstable<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  configfiles<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  home-manager<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  stapelbergnix<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  extraZshInit <span style="color:#666">?</span> <span style="color:#4070a0">&#34;&#34;</span><span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>}:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  config<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  lib<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  pkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#666">...</span>
</span></span><span style="display:flex;"><span>}:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>  system <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>stdenv<span style="color:#666">.</span>hostPlatform<span style="color:#666">.</span>system;
</span></span><span style="display:flex;"><span>  pkgsUnstable <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs-unstable {
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>    config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  imports <span style="color:#666">=</span> [ home-manager<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>home-manager ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># home-manager configuration</span>
</span></span><span style="display:flex;"><span>  home-manager<span style="color:#666">.</span>useGlobalPkgs <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  home-manager<span style="color:#666">.</span>useUserPackages <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  home-manager<span style="color:#666">.</span>extraSpecialArgs <span style="color:#666">=</span> { <span style="color:#007020;font-weight:bold">inherit</span> configfiles stapelbergnix; };
</span></span><span style="display:flex;"><span>  home-manager<span style="color:#666">.</span>users<span style="color:#666">.</span>michael <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    imports <span style="color:#666">=</span> [ <span style="color:#235388">./microvm-home.nix</span> ];
</span></span><span style="display:flex;"><span>    microvm<span style="color:#666">.</span>extraZshInit <span style="color:#666">=</span> extraZshInit;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Claude Code CLI (from nixpkgs-unstable, unfree)</span>
</span></span><span style="display:flex;"><span>  environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    pkgsUnstable<span style="color:#666">.</span>claude-code
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>  networking<span style="color:#666">.</span>hostName <span style="color:#666">=</span> hostName;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  system<span style="color:#666">.</span>stateVersion <span style="color:#666">=</span> <span style="color:#4070a0">&#34;25.11&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  services<span style="color:#666">.</span>openssh<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># To match midna (host)</span>
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>groups<span style="color:#666">.</span>michael <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    gid <span style="color:#666">=</span> <span style="color:#40a070">1000</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>michael <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    group <span style="color:#666">=</span> <span style="color:#4070a0">&#34;michael&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  services<span style="color:#666">.</span>resolved<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  networking<span style="color:#666">.</span>useDHCP <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>  networking<span style="color:#666">.</span>useNetworkd <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  networking<span style="color:#666">.</span>tempAddresses <span style="color:#666">=</span> <span style="color:#4070a0">&#34;disabled&#34;</span>;
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>networks<span style="color:#666">.</span><span style="color:#4070a0">&#34;10-e&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    matchConfig<span style="color:#666">.</span>Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;e*&#34;</span>;
</span></span><span style="display:flex;"><span>    addresses <span style="color:#666">=</span> [ { Address <span style="color:#666">=</span> <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>ipAddress<span style="color:#70a0d0">}</span><span style="color:#4070a0">/24&#34;</span>; } ];
</span></span><span style="display:flex;"><span>    routes <span style="color:#666">=</span> [ { Gateway <span style="color:#666">=</span> <span style="color:#4070a0">&#34;192.168.83.1&#34;</span>; } ];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  networking<span style="color:#666">.</span>nameservers <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#34;8.8.8.8&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#34;1.1.1.1&#34;</span>
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Disable firewall for faster boot and less hassle;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># we are behind a layer of NAT anyway.</span>
</span></span><span style="display:flex;"><span>  networking<span style="color:#666">.</span>firewall<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>settings<span style="color:#666">.</span>Manager <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># fast shutdowns/reboots! https://mas.to/@zekjur/113109742103219075</span>
</span></span><span style="display:flex;"><span>    DefaultTimeoutStopSec <span style="color:#666">=</span> <span style="color:#4070a0">&#34;5s&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Fix for microvm shutdown hang (issue #170):</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Without this, systemd tries to unmount /nix/store during shutdown,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># but umount lives in /nix/store, causing a deadlock.</span>
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>mounts <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      what <span style="color:#666">=</span> <span style="color:#4070a0">&#34;store&#34;</span>;
</span></span><span style="display:flex;"><span>      where <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/nix/store&#34;</span>;
</span></span><span style="display:flex;"><span>      overrideStrategy <span style="color:#666">=</span> <span style="color:#4070a0">&#34;asDropin&#34;</span>;
</span></span><span style="display:flex;"><span>      unitConfig<span style="color:#666">.</span>DefaultDependencies <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Use SSH host keys mounted from outside the VM (remain identical).</span>
</span></span><span style="display:flex;"><span>  services<span style="color:#666">.</span>openssh<span style="color:#666">.</span>hostKeys <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      path <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/etc/ssh/host-keys/ssh_host_ed25519_key&#34;</span>;
</span></span><span style="display:flex;"><span>      type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;ed25519&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  microvm <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># Enable writable nix store overlay so nix-daemon works.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># This is required for home-manager activation.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># Uses tmpfs by default (ephemeral), which is fine since we</span>
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># don&#39;t build anything in the VM.</span>
</span></span><span style="display:flex;"><span>    writableStoreOverlay <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/nix/.rw-store&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    volumes <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        mountPoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/var&#34;</span>;
</span></span><span style="display:flex;"><span>        image <span style="color:#666">=</span> <span style="color:#4070a0">&#34;var.img&#34;</span>;
</span></span><span style="display:flex;"><span>        size <span style="color:#666">=</span> <span style="color:#40a070">8192</span>; <span style="color:#60a0b0;font-style:italic"># MB</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    shares <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#60a0b0;font-style:italic"># use proto = &#34;virtiofs&#34; for MicroVMs that are started by systemd</span>
</span></span><span style="display:flex;"><span>        proto <span style="color:#666">=</span> <span style="color:#4070a0">&#34;virtiofs&#34;</span>;
</span></span><span style="display:flex;"><span>        tag <span style="color:#666">=</span> <span style="color:#4070a0">&#34;ro-store&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#60a0b0;font-style:italic"># a host&#39;s /nix/store will be picked up so that no</span>
</span></span><span style="display:flex;"><span>        <span style="color:#60a0b0;font-style:italic"># squashfs/erofs will be built for it.</span>
</span></span><span style="display:flex;"><span>        source <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/nix/store&#34;</span>;
</span></span><span style="display:flex;"><span>        mountPoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/nix/.ro-store&#34;</span>;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        proto <span style="color:#666">=</span> <span style="color:#4070a0">&#34;virtiofs&#34;</span>;
</span></span><span style="display:flex;"><span>        tag <span style="color:#666">=</span> <span style="color:#4070a0">&#34;ssh-keys&#34;</span>;
</span></span><span style="display:flex;"><span>        source <span style="color:#666">=</span> <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>workspace<span style="color:#70a0d0">}</span><span style="color:#4070a0">/ssh-host-keys&#34;</span>;
</span></span><span style="display:flex;"><span>        mountPoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/etc/ssh/host-keys&#34;</span>;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        proto <span style="color:#666">=</span> <span style="color:#4070a0">&#34;virtiofs&#34;</span>;
</span></span><span style="display:flex;"><span>        tag <span style="color:#666">=</span> <span style="color:#4070a0">&#34;claude-credentials&#34;</span>;
</span></span><span style="display:flex;"><span>        source <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael/claude-microvm&#34;</span>;
</span></span><span style="display:flex;"><span>        mountPoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael/claude-microvm&#34;</span>;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        proto <span style="color:#666">=</span> <span style="color:#4070a0">&#34;virtiofs&#34;</span>;
</span></span><span style="display:flex;"><span>        tag <span style="color:#666">=</span> <span style="color:#4070a0">&#34;workspace&#34;</span>;
</span></span><span style="display:flex;"><span>        source <span style="color:#666">=</span> workspace;
</span></span><span style="display:flex;"><span>        mountPoint <span style="color:#666">=</span> workspace;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    interfaces <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;tap&#34;</span>;
</span></span><span style="display:flex;"><span>        id <span style="color:#666">=</span> tapId;
</span></span><span style="display:flex;"><span>        mac <span style="color:#666">=</span> mac;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    hypervisor <span style="color:#666">=</span> <span style="color:#4070a0">&#34;cloud-hypervisor&#34;</span>;
</span></span><span style="display:flex;"><span>    vcpu <span style="color:#666">=</span> <span style="color:#40a070">8</span>;
</span></span><span style="display:flex;"><span>    mem <span style="color:#666">=</span> <span style="color:#40a070">4096</span>;
</span></span><span style="display:flex;"><span>    socket <span style="color:#666">=</span> <span style="color:#4070a0">&#34;control.socket&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></details>
<h3 id="step-5-microvm-homenix">Step 5: <code>microvm-home.nix</code></h3>
<p><code>microvm-base.nix</code> in turn pulls in <code>microvm-home.nix</code>, which sets up home-manager to:</p>
<ul>
<li>Set up Zsh with my configuration</li>
<li>Set up Emacs with my configuration</li>
<li>Set up Claude Code in shared directory <code>~/claude-microvm</code>.</li>
</ul>
<details>
<summary>Expand full <code>microvm-home.nix</code> code</summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  config<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  pkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  lib<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  configfiles<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  stapelbergnix<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#666">...</span>
</span></span><span style="display:flex;"><span>}:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  options<span style="color:#666">.</span>microvm <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    extraZshInit <span style="color:#666">=</span> lib<span style="color:#666">.</span>mkOption {
</span></span><span style="display:flex;"><span>      type <span style="color:#666">=</span> lib<span style="color:#666">.</span>types<span style="color:#666">.</span>lines;
</span></span><span style="display:flex;"><span>      default <span style="color:#666">=</span> <span style="color:#4070a0">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span>      description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;Extra lines to add to zsh initContent&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  config <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    home<span style="color:#666">.</span>username <span style="color:#666">=</span> <span style="color:#4070a0">&#34;michael&#34;</span>;
</span></span><span style="display:flex;"><span>    home<span style="color:#666">.</span>homeDirectory <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    programs<span style="color:#666">.</span>zsh <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>      history <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>        size <span style="color:#666">=</span> <span style="color:#40a070">4000</span>;
</span></span><span style="display:flex;"><span>        save <span style="color:#666">=</span> <span style="color:#40a070">10000000</span>;
</span></span><span style="display:flex;"><span>        ignoreDups <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>        share <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>        append <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      initContent <span style="color:#666">=</span> <span style="color:#4070a0">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        </span><span style="color:#70a0d0">${</span><span style="color:#007020">builtins</span><span style="color:#666">.</span>readFile <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>configfiles<span style="color:#70a0d0">}</span><span style="color:#4070a0">/zshrc&#34;</span><span style="color:#70a0d0">}</span><span style="color:#4070a0">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        export CLAUDE_CONFIG_DIR=/home/michael/claude-microvm
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        </span><span style="color:#70a0d0">${</span>config<span style="color:#666">.</span>microvm<span style="color:#666">.</span>extraZshInit<span style="color:#70a0d0">}</span><span style="color:#4070a0">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">      &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    programs<span style="color:#666">.</span>emacs <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>      package <span style="color:#666">=</span> stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>emacsWithPackages { <span style="color:#007020;font-weight:bold">inherit</span> pkgs; };
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    home<span style="color:#666">.</span>file<span style="color:#666">.</span><span style="color:#4070a0">&#34;.config/emacs&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      source <span style="color:#666">=</span> <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>configfiles<span style="color:#70a0d0">}</span><span style="color:#4070a0">/config/emacs&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    home<span style="color:#666">.</span>stateVersion <span style="color:#666">=</span> <span style="color:#4070a0">&#34;25.11&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    programs<span style="color:#666">.</span>home-manager<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></details>
<h3 id="step-6-goprotobufnix">Step 6: <code>goprotobuf.nix</code></h3>
<p>The <code>goprotobuf.nix</code> makes available a bunch of required and convenient packages:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># Project-specific configuration for goprotobufvm</span>
</span></span><span style="display:flex;"><span>{ pkgs<span style="color:#666">,</span> <span style="color:#666">...</span> }:
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Development environment for Go Protobuf</span>
</span></span><span style="display:flex;"><span>  environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># Go toolchain</span>
</span></span><span style="display:flex;"><span>    go
</span></span><span style="display:flex;"><span>    gopls
</span></span><span style="display:flex;"><span>    delve
</span></span><span style="display:flex;"><span>    protobuf
</span></span><span style="display:flex;"><span>    gnumake
</span></span><span style="display:flex;"><span>    gcc
</span></span><span style="display:flex;"><span>    git
</span></span><span style="display:flex;"><span>    ripgrep
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="running-the-vm">Running the VM</h3>
<p>Let’s create the workspace directory and create an SSH host key:</p>
<pre tabindex="0"><code>mkdir -p ~/microvm/emacs/ssh-host-keys
ssh-keygen -t ed25519 -N &#34;&#34; \
  -f ~/microvm/emacs/ssh-host-keys/ssh_host_ed25519_key
</code></pre><p>Now we can start the VM:</p>
<pre tabindex="0"><code>sudo systemctl start microvm@emacsvm
</code></pre><p>It boots and responds to pings within a few seconds.</p>
<p>Then, SSH into the VM (perhaps in a <a href="https://manpages.debian.org/tmux.1"><code>tmux(1)</code></a>
 session) and run Claude
(or your Coding Agent of choice) without permission prompts in the shared
workspace directory:</p>
<pre tabindex="0"><code>% ssh 192.168.83.2
emacsvm% cd microvm/emacs
emacsvm% claude --dangerously-skip-permissions
</code></pre><p>This is what running Claude in such a setup looks like:</p>















<a href="https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured_hu_51eebb097010a828.png 2x,https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured_hu_9f1cbedec722fa16.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured_hu_d06e7aa7176833b3.png"
  alt="Claude Code in “bypass permissions” mode" title="Claude Code in “bypass permissions” mode"
  width="600"
  height="479"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h2 id="creating-vms-with-claude">Creating VMs with Claude</h2>
<p>After going through the process of setting up a MicroVM once, it becomes tedious.</p>
<p>I was curious if <a href="https://code.claude.com/docs/en/skills">Claude Skills</a> could
help with a task like this. Skills are markdown files that instruct Claude to do
certain steps in certain situations.</p>
<p>I created <code>.claude/skills/create-microvm/SKILL.md</code> as follows:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span>name: create-microvm
</span></span><span style="display:flex;"><span>description: Creates a new microvm Virtual Machine on midna for running Claude in, with source code repositories and build dependencies available inside the microvm. Use when the user asks to create a new microvm.
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Inspect the existing structure at ~/machines/midna (NixOS configuration using Flakes), which includes several MicroVMs in the ~/machines/midna/microvms/ directory.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Then, create a similar structure for the microvm the user asked to create. Be sure to consider:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">1.</span> Create a new subdirectory for this microvm, named NAME (the microvm name).
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">2.</span> Create an entry in microvm.nix similar to an existing microvm&#39;s, but:
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">3.</span> Change hostname to NAME
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">4.</span> Change IP address (e.g., 192.168.83.3): find used ones and chose next free
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">5.</span> Change workspace share to /home/michael/microvm/NAME
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">6.</span> Include build dependencies for the new microvm based on user request
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">7.</span> Create ssh-host-keys directory
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">8.</span> Generate SSH host keys
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">9.</span> Clone/download source code repositories into this microvm based on user request
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Once the microvm is defined, update ~/machines/midna to pull it in. To verify, build the config (but do not deploy).
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>You need to git-commit changes to the microvm before ~/machines/midna can see them.
</span></span></code></pre></div><p>When using this skill with Claude Code (tested version: v2.0.76 and v2.1.15), with the <a href="https://www.anthropic.com/news/claude-opus-4-5">Opus 4.5 model</a>, I can send a prompt like this:</p>
<blockquote>
<p>please set up a microvm for Debian Code Search (dcs). see ~/dcs for the source code (but clone from <a href="https://github.com/Debian/dcs">https://github.com/Debian/dcs</a>)</p>
</blockquote>
<p>Now Claude churns for a few minutes, possibly asking a clarification question
before that. Afterwards, Claude reports back with:</p>
<blockquote>
<p>The dcsvm microvm has been set up successfully. Here&rsquo;s what was created:</p>
<p>Configuration:</p>
<ul>
<li>/home/michael/machines/midna/microvms/dcs.nix - Project-specific packages (Go toolchain, protobuf tools, uglify-js, yuicompressor,
zopfli)</li>
<li>Updated /home/michael/machines/midna/microvm.nix to include dcsvm with:
<ul>
<li>IP: 192.168.83.8</li>
<li>tapId: microvm6</li>
<li>mac: 02:00:00:00:00:07</li>
</ul>
</li>
</ul>
<p>Workspace:</p>
<ul>
<li>/home/michael/microvm/dcs/ - Workspace directory</li>
<li>/home/michael/microvm/dcs/ssh-host-keys/ - SSH host keys (ed25519)</li>
<li>/home/michael/microvm/dcs/dcs/ - Cloned repository from <a href="https://github.com/Debian/dcs">https://github.com/Debian/dcs</a></li>
</ul>
<p>Build verified - The configuration builds successfully.</p>
<p>To start the microvm after deploying:
<code>systemctl start microvm@dcsvm</code></p>
<p>To SSH into it:
<code>ssh michael@192.168.83.8</code></p>
</blockquote>
<p>Wonderful! In my experience, Claude always got the VM creation correct.</p>
<h2 id="replicating-this-setup-with-claude-code">Replicating this setup with Claude Code</h2>
<p>In fact, you can go one step further: Instead of just asking Claude to create
new MicroVMs, you can also ask Claude to replicate this entire setup into your
NixOS configuration!</p>
<p>Try a prompt like this:</p>
<blockquote>
<p>read
<a href="https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/">https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/</a>
— I want the exact same setup in my midna NixOS configuration please!</p>
</blockquote>
<h2 id="conclusion">Conclusion</h2>
<p>NixOS has a reputation of being hard to adopt, but once you are using NixOS, you
can do powerful things like spinning up ephemeral MicroVMs for a new project
within minutes.</p>
<p>The maintenance effort is minimal: When I update my personal PC, my MicroVM
configurations start using the new software versions, too. Customization is easy
if needed.</p>
<p>This actually mirrors my experience with Coding Agents: I don’t feel like
they’re <em>automatically</em> making existing tasks more efficient, I feel that they
make things possible that were previously out of reach (similar to <a href="https://en.wikipedia.org/wiki/Jevons_paradox">Jevons
paradox</a>).</p>
<p>It was fascinating (and scary!) to experience the quality increase of Coding
Agents during 2025. At the beginning of 2025 I thought that LLMs are an
overhyped toy, and felt it was almost insulting when people showed me text or
code produced by these models. But almost every new frontier model release got
significantly better, and by now I have been positively surprised by Claude
Code’s capabilities and quality many times. It has produced code that handles
legitimate edge cases I would not have considered.</p>
<p>With this article, I showed one possible way to run Coding Agents safely (or any
workload that shouldn’t access your private data, really) that you can adjust in
many ways for your needs.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Can I finally start using Wayland in 2026?]]></title>
    <link href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/"/>
    <id>https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/</id>
    <published>2026-01-04T08:55:00+01:00</published>
    <content type="html"><![CDATA[<p>Wayland is the successor to the X server (X11, Xorg) to implement the graphics
stack on Linux. The <a href="https://en.wikipedia.org/wiki/Wayland_(protocol)">Wayland</a>
project was actually started in 2008, a year before I created the <a href="https://i3wm.org/">i3 tiling
window manager for X11</a> in 2009 — but for the last 18 years
(!), Wayland was never usable on my computers. I don’t want to be stuck on
deprecated software, so I try to start using Wayland each year, and this
articles outlines what keeps me from migrating to Wayland in 2026.</p>
<h2 id="historical-context">Historical context</h2>
<p>For the first few years, Wayland rarely even started on my machines. When I was
lucky enough for something to show up, I could start some toy demo apps in the
demo compositor Weston.</p>
<p>Around 2014, GNOME started supporting Wayland. KDE followed a few years later.
Major applications (like Firefox, Chrome or Emacs) have been slower to adopt
Wayland and needed users to opt into experimental implementations via custom
flags or environment variables, until very recently, or — in some cases, like
<code>geeqie</code> — still as of today.</p>
<p>Unfortunately, the driver support situation remained poor for many years.  With
nVidia graphics cards, which <a href="/posts/2017-12-11-dell-up3218k/">are the only cards that support my 8K
monitor</a>, Wayland would either not work at all
or exhibit heavy graphics glitches and crashes.</p>
<p>In the 2020s, more and more distributions announced looking to switch to Wayland
by default or even <a href="https://www.phoronix.com/news/Fedora-40-Eyes-No-X11-Session">drop their X11
sessions</a>, and RHEL
is <a href="https://www.redhat.com/en/blog/rhel-10-plans-wayland-and-xorg-server">winding down their contributions to the X
server</a>.</p>
<p>Modern Linux distributions like <a href="https://asahilinux.org/">Asahi Linux</a> (for
Macs, with their own GPU driver!) clearly consider Wayland their primary desktop
stack, and only support X11 on a best-effort basis.</p>
<p>So the pressure to switch to Wayland is mounting! Is it ready now? What’s
missing?</p>
<h2 id="making-wayland-start">Making Wayland start</h2>
<h3 id="wayland-hardware">Hardware</h3>
<p>I’m testing with my lab PC, which is a slightly upgraded version of <a href="/posts/2022-01-15-high-end-linux-pc/">my 2022
high-end Linux PC</a>.</p>
<p>I describe my setup in more details in <a href="/posts/2020-05-23-desk-setup/">stapelberg uses this: my 2020 desk
setup</a>.</p>
<p>Most importantly for this article, I use a <a href="/posts/2017-12-11-dell-up3218k/">Dell 8K 32&quot;
monitor</a> (resolution: 7680x4320!), which, in my
experience, is only compatible with nVidia graphics cards (I try other cards
sometimes).</p>
<p>Hence, both the lab PC and my main PC contain an nVidia GPU:</p>
<ul>
<li>The lab PC contains a nVidia GeForce RTX 4070 Ti.</li>
<li>The main PC contains a nVidia GeForce RTX 3060 Ti.</li>
</ul>
<p>(In case you’re wondering why I use the older card in my PC: I had a crash once
where I suspected the GPU, so I switched back from the 4070 to my older 3060.)</p>
<h3 id="nvidia-driver-support">nVidia driver support</h3>
<p>For many years, nVidia drivers were entirely unsupported under Wayland.</p>
<p>Apparently, nVidia refused to support the API that Wayland was using, insisting
that their EGLStreams approach was superior. Luckily, with nVidia driver 495
(late 2021), they added support for GBM (Generic Buffer Manager).</p>
<p>But, even with GBM support, while you could now start many Wayland sessions, the
session wouldn’t run smoothly: You would see severe graphics glitches and
artifacts, preventing you from getting any work done.</p>
<p>The solution for the glitches was <em>explicit sync</em> support: because the nVidia
driver does not support <em>implicit sync</em> (like AMD or Intel), Wayland (and
wlroots, and sway) needed to get <a href="https://zamundaaa.github.io/wayland/2024/04/05/explicit-sync.html">explicit sync
support</a>.</p>
<p>Sway 1.11 (June 2025) and wlroots 0.19.0 are the first version with explicit
sync support.</p>
<h3 id="tile">Not working: TILE support for 8K monitor</h3>
<p>With the nVidia driver now working <em>per se</em> with Wayland, unfortunately that’s
still not good enough to use Wayland in my setup: my <a href="/posts/2017-12-11-dell-up3218k/">Dell UP3218K
monitor</a> requires two DisplayPort 1.4
connections with MST (Multi Stream Transport) and <code>TILE</code> support. This
combination worked just fine under X11 for the last 8+ years.</p>
<p>While GNOME successfully configures the monitor with its native resolution of
7680x4320@60, the monitor incorrectly shows up as two separate monitors in sway.</p>
<p>The reason behind this behavior is that <a href="https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/1580">wlroots does not support the <code>TILE</code>
property (issue #1580 from
2019)</a>. Luckily,
in 2023, contributor <code>EBADBEEF</code> sent <a href="https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4154">draft merge request
!4154</a>,
which adds support for the <code>TILE</code> property.</p>
<p>But, even with the <code>TILE</code> patch, my monitor would not work correctly: The right
half of the monitor would just stay black. The full picture is visible when
taking a screenshot with <code>grim</code>, so it seems like an output issue. I had a few
exchanges about this with <code>EBADBEEF</code> starting in August 2025 (thanks for taking
a look!), but we couldn’t figure out the issue.</p>
<p>A quarter later, I had made good experiences regarding debugging complex issues
with the coding assistant <a href="https://claude.com/product/claude-code">Claude Code</a>
(Opus 4.5 at the time of writing), so I decided to give it another try. Over two
days, I ran a number of tests to narrow down the issue, letting Claude analyze
source code (of sway, wlroots, Xorg, mesa, …) and produce test programs that I
could run manually.</p>
<p>Ultimately, I ended up with a minimal reproducer program (independent of
Wayland) that shows how the <code>SRC_X</code> DRM property does not work on nVidia (but
does work on Intel, for example!): I posted a <a href="https://forums.developer.nvidia.com/t/bug-right-half-right-tile-of-my-8k-monitor-is-black-on-wlroots-based-wayland-compositors/355579">bug report with a video in the
nVidia
forum</a>
and hope an nVidia engineer will take a look!</p>
<p>Crucially, with the bug now identified, I had Claude implement a workaround:
copy the right half of the screen (at <code>SRC_X=3840</code>) to another buffer, and then
display <em>that buffer</em>, but with <code>SRC_X=0</code>.</p>
<p>With <a href="https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4154#note_3249071">that
patch</a>
applied, for the first time, I can use Sway on my 8K monitor! 🥳</p>
<hr>
<p>By the way, when I mentioned that GNOME successfully configures the native
resolution, that doesn’t mean the monitor is usable with GNOME! While GNOME
supports tiled displays, the updates of individual tiles are not synchronized,
so you see heavy tearing in the middle of the screen, much worse than anything I
have ever observed under X11. GNOME/mutter <a href="https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4822">merge request
!4822</a> should
hopefully address this.</p>
<h3 id="software-nixos">Software: NixOS</h3>
<p>During 2025, I <a href="/posts/tags/nix/">switched all my computers to NixOS</a>. Its
declarative approach is really nice for doing such tests, because you can
reliably restore your system to an earlier version.</p>
<p>To make a Wayland/sway session available on my NixOS 25.11 installation, I added
the following lines to my NixOS configuration file (<code>configuration.nix</code>):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># GDM display manager (can launch both X11/i3 and Wayland/Sway sessions)</span>
</span></span><span style="display:flex;"><span>services<span style="color:#666">.</span>displayManager<span style="color:#666">.</span>gdm<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>services<span style="color:#666">.</span>displayManager<span style="color:#666">.</span>gdm<span style="color:#666">.</span>autoSuspend <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># enable GNOME (for testing)</span>
</span></span><span style="display:flex;"><span>services<span style="color:#666">.</span>desktopManager<span style="color:#666">.</span>gnome<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>programs<span style="color:#666">.</span>sway <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>  enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  wrapperFeatures<span style="color:#666">.</span>gtk <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  extraOptions <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;--unsupported-gpu&#34;</span> ];
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>I also added the following Wayland-specific programs to <code>environment.systemPackages</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># …</span>
</span></span><span style="display:flex;"><span>  foot          <span style="color:#60a0b0;font-style:italic"># terminal emulator</span>
</span></span><span style="display:flex;"><span>  wtype         <span style="color:#60a0b0;font-style:italic"># replacement for xdotool type</span>
</span></span><span style="display:flex;"><span>  fuzzel        <span style="color:#60a0b0;font-style:italic"># fuzzy matching program starter</span>
</span></span><span style="display:flex;"><span>  wayland-utils <span style="color:#60a0b0;font-style:italic"># for wayland-info(1)</span>
</span></span><span style="display:flex;"><span>  gammastep     <span style="color:#60a0b0;font-style:italic"># redshift replacement</span>
</span></span><span style="display:flex;"><span>];
</span></span></code></pre></div><p>Note that activating this configuration kills your running X11 session, if any.</p>
<p>Just to be sure, I rebooted the entire machine after changing the configuration.</p>
<h2 id="results">Experiment results</h2>
<p>With this setup, I spent about one full work day in a Wayland session. Trying to
actually get some work done uncovers issues that might not show in casual
testing. Most of the day was spent trying to fix Wayland issues 😅. The
following sections explain what I have learned/observed.</p>
<h3 id="sway">Desktop: i3 → sway</h3>
<p>Many years ago, when Wayland became more popular, people asked on the i3 issue
tracker if i3 would be ported to Wayland. I said no: How could I port a program
to an environment that doesn’t even run on any of my computers? But also, I knew
that with working a full-time job, I wouldn’t have time to be an early adopter
and shape Wayland development.</p>
<p>This attitude resulted in Drew DeVault starting the
<a href="https://en.wikipedia.org/wiki/Sway_(window_manager)">Sway</a> project around 2016,
which aims to be a Wayland version of i3. I don’t see Sway as
competition. Rather, I thought it was amazing that people liked the i3 project
so much that they would go through the trouble of creating a similar program for
other environments! What a nice compliment! 😊</p>
<p>Sway aims to be compatible with i3 configuration files, and it mostly is.</p>
<p>If you’re curious, here is what I changed from the Sway defaults, mostly moving
key bindings around for the <a href="https://neo-layout.org/">NEO keyboard layout</a> I
use, and configuring <code>input</code>/<code>output</code> blocks that I formerly configured in <a href="https://github.com/stapelberg/configfiles/blob/c896b138b2f50e1badf1ee862678adb820d58473/xsession">my
<code>~/.xsession</code>
file</a>:</p>
<details>
<summary>my changes to the default Sway config</summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#a00000">--- /home/michael/src/sway/config.in	2025-09-24 19:08:38.876573260 +0200
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+++ /home/michael/.config/sway/config	2025-12-31 15:50:38.616697542 +0100
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span><span style="color:#800080;font-weight:bold">@@ -9,19 +9,76 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span> # Logo key. Use Mod1 for Alt.
</span></span><span style="display:flex;"><span> set $mod Mod4
</span></span><span style="display:flex;"><span> # Home row direction keys, like vim
</span></span><span style="display:flex;"><span><span style="color:#a00000">-set $left h
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-set $down j
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-set $up k
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-set $right l
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+set $left n
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+set $down r
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+set $up t
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+set $right d
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> # Your preferred terminal emulator
</span></span><span style="display:flex;"><span> set $term foot
</span></span><span style="display:flex;"><span> # Your preferred application launcher
</span></span><span style="display:flex;"><span><span style="color:#a00000">-set $menu wmenu-run
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+set $menu fuzzel
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+font pango:Bitstream Vera Sans Mono 8
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+titlebar_padding 4 2
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+# Make Xwayland windows recognizeable:
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+for_window [shell=&#34;xwayland&#34;] title_format &#34;%title [Xwayland]&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+workspace_layout stacking
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+# Open two terminal windows side-by-side on new workspaces:
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+# https://github.com/stapelberg/workspace-populate-for-i3
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+exec ~/go/bin/workspace-populate-for-i3
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+exec gammastep -l 47.31:8.50 -b 0.9
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+input * {
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+   xkb_layout &#34;de&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+   xkb_variant &#34;neo&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	repeat_delay 250
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	repeat_rate 30
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+}
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+input * {
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	accel_profile adaptive
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+	pointer_accel 0.2
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+}
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> 
</span></span><span style="display:flex;"><span> ### Output configuration
</span></span><span style="display:flex;"><span> #
</span></span><span style="display:flex;"><span><span style="color:#a00000">-# Default wallpaper (more resolutions are available in @datadir@/backgrounds/sway/)
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-output * bg @datadir@/backgrounds/sway/Sway_Wallpaper_Blue_1920x1080.png fill
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+output * bg /dev/null fill #333333
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+output * scale 3
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> #
</span></span><span style="display:flex;"><span> # Example configuration:
</span></span><span style="display:flex;"><span> #
</span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -33,14 +90,41 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span> #
</span></span><span style="display:flex;"><span> # Example configuration:
</span></span><span style="display:flex;"><span> #
</span></span><span style="display:flex;"><span><span style="color:#a00000">-# exec swayidle -w \
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-#          timeout 300 &#39;swaylock -f -c 000000&#39; \
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-#          timeout 600 &#39;swaymsg &#34;output * power off&#34;&#39; resume &#39;swaymsg &#34;output * power on&#34;&#39; \
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-#          before-sleep &#39;swaylock -f -c 000000&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+exec swayidle -w \
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+         before-sleep &#39;~/swaylock.sh&#39; \
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+         lock &#39;~/swaylock.sh&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> #
</span></span><span style="display:flex;"><span> # This will lock your screen after 300 seconds of inactivity, then turn off
</span></span><span style="display:flex;"><span> # your displays after another 300 seconds, and turn your screens back on when
</span></span><span style="display:flex;"><span> # resumed. It will also lock your screen before your computer goes to sleep.
</span></span><span style="display:flex;"><span><span style="color:#00a000">+bindsym $mod+l exec loginctl lock-session
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+  # Notifications
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+  bindsym $mod+period exec dunstctl close
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> 
</span></span><span style="display:flex;"><span> ### Input configuration
</span></span><span style="display:flex;"><span> #
</span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -63,11 +147,13 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span>     # Start a terminal
</span></span><span style="display:flex;"><span>     bindsym $mod+Return exec $term
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>     # Kill focused window
</span></span><span style="display:flex;"><span><span style="color:#a00000">-    bindsym $mod+Shift+q kill
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+    bindsym $mod+Shift+x kill
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> 
</span></span><span style="display:flex;"><span>     # Start your launcher
</span></span><span style="display:flex;"><span><span style="color:#a00000">-    bindsym $mod+d exec $menu
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+    bindsym $mod+a exec $menu
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> 
</span></span><span style="display:flex;"><span>     # Drag floating windows by holding down $mod and left mouse button.
</span></span><span style="display:flex;"><span>     # Resize them with right mouse button + $mod.
</span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -142,12 +228,11 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span>     bindsym $mod+v splitv
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>     # Switch the current container between different layout styles
</span></span><span style="display:flex;"><span><span style="color:#a00000">-    bindsym $mod+s layout stacking
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+    bindsym $mod+i layout stacking
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>     bindsym $mod+w layout tabbed
</span></span><span style="display:flex;"><span><span style="color:#a00000">-    bindsym $mod+e layout toggle split
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span> 
</span></span><span style="display:flex;"><span>     # Make the current focus fullscreen
</span></span><span style="display:flex;"><span><span style="color:#a00000">-    bindsym $mod+f fullscreen
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+    bindsym $mod+e fullscreen
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> 
</span></span><span style="display:flex;"><span>     # Toggle the current focus between tiling and floating mode
</span></span><span style="display:flex;"><span>     bindsym $mod+Shift+space floating toggle
</span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -156,7 +241,7 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span>     bindsym $mod+space focus mode_toggle
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>     # Move focus to the parent container
</span></span><span style="display:flex;"><span><span style="color:#a00000">-    bindsym $mod+a focus parent
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+    bindsym $mod+u focus parent
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> #
</span></span><span style="display:flex;"><span> # Scratchpad:
</span></span><span style="display:flex;"><span> #
</span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -192,37 +277,25 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span>     bindsym Return mode &#34;default&#34;
</span></span><span style="display:flex;"><span>     bindsym Escape mode &#34;default&#34;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span><span style="color:#a00000">-bindsym $mod+r mode &#34;resize&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+#bindsym $mod+r mode &#34;resize&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> 
</span></span><span style="display:flex;"><span> #
</span></span><span style="display:flex;"><span> # Status Bar:
</span></span><span style="display:flex;"><span> #
</span></span><span style="display:flex;"><span> # Read `man 5 sway-bar` for more information about this section.
</span></span><span style="display:flex;"><span> bar {
</span></span><span style="display:flex;"><span><span style="color:#a00000">-    position top
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span> 
</span></span><span style="display:flex;"><span>     # When the status_command prints a new line to stdout, swaybar updates.
</span></span><span style="display:flex;"><span>     # The default just shows the current date and time.
</span></span><span style="display:flex;"><span><span style="color:#a00000">-    status_command while date +&#39;%Y-%m-%d %X&#39;; do sleep 1; done
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+    status_command i3status
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span> }
</span></span><span style="display:flex;"><span> 
</span></span></code></pre></div></details>
<p>I encountered the following issues with Sway:</p>
<ol>
<li>
<p>I don’t know how I can configure the same libinput settings that I had
before.  See <a href="xinput-list-props-mx-ergo.txt"><code>xinput-list-props-mx-ergo.txt</code></a>
for what I have on X11. Sway’s available <code>accel_profile</code> settings do not seem
to match what I used before.</p>
</li>
<li>
<p>The mouse cursor / pointer seems laggy, somehow?! It seems to take longer to
react when I move the trackball, and it also seems to move less smoothly
across the screen.</p>
<p><a href="https://github.com/emersion">Simon Ser</a> suspects that this might be because
hardware cursor support might not work with the nVidia drivers currently.</p>
</li>
<li>
<p>No Xwayland scaling: programs started via Xwayland are blurry (by default) or
double-scaled (when setting <code>Xft.dpi: 288</code>). This is a Sway-specific
limitation: <a href="https://pointieststick.com/2022/06/17/this-week-in-kde-non-blurry-xwayland-apps/">KDE fixed this in
2022</a>. From
<a href="https://github.com/swaywm/sway/issues/2966">Sway issue #2966</a>, I can tell
that Sway developers do not seem to like this approach for some reason, but
that’s <em>very unfortunate</em> for my migration: The backwards compatibility
option of running older programs through Xwayland is effectively unavailable
to me.</p>
</li>
<li>
<p>Sometimes, keyboard shortcuts seem to be executed twice! Like, when I focused
the first of five Chrome windows in a stack and moved that window to another
workspace, <em>two windows</em> would be moved instead of one. I also see messages
like this one (not exactly correlated with the double-shortcut problem,
though):</p>
<pre tabindex="0"><code>[ERROR] [wlr] [libinput] event0  - https: kinT (kint36): client bug: event
processing lagging behind by 32ms, your system is too slow
</code></pre><p>…and that seems wrong to me. My <a href="/posts/2025-05-15-my-2025-high-end-linux-pc/">high-end Linux
PC</a> certainly isn’t slow by any
measure.</p>
</li>
</ol>
<h3 id="gtk-font-size">GTK: Font size</h3>
<p>When I first started GTK programs like GIMP or Emacs, I noticed all fonts were
way too large! Apparently, I still had some scaling-related settings that I
needed to reset like so:</p>
<pre tabindex="0"><code>gsettings reset org.gnome.desktop.interface scaling-factor
gsettings reset org.gnome.desktop.interface text-scaling-factor
</code></pre><p>Debugging tip: Display GNOME settings using <code>dconf dump /</code> (stored in
<code>~/.config/dconf</code>).</p>
<h3 id="gtk-backend">GTK: Backend</h3>
<p>Some programs like <code>geeqie</code> apparently need an explicit <code>export GDK_BACKEND=wayland</code> environment variable, otherwise they run in
Xwayland. Weird.</p>
<h3 id="font-rendering">Font rendering</h3>
<p>I also noticed that font rendering is different between X11 and Wayland! The
difference is visible in Chrome browser tab titles and the URL bar, for example:</p>















<a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/2026-01-03-chrome-wayland-x11.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/2026-01-03-chrome-wayland-x11_hu_9fc3abb5389d56fe.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/2026-01-03-chrome-wayland-x11_hu_e67075ba45299853.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/2026-01-03-chrome-wayland-x11_hu_8822a57b2d4633df.png"
  
  width="600"
  height="219"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>At first I thought that maybe Wayland defaults to different font-antialiasing
and font-hinting settings, but I tried experimenting with the following settings
(which default to <code>font-antialiasing=grayscale</code> and <code>font-hinting=slight</code>), but
couldn’t get things to render like they did before:</p>
<pre tabindex="0"><code>gsettings set org.gnome.desktop.interface font-antialiasing &#39;rgba&#39;
gsettings set org.gnome.desktop.interface font-hinting &#39;full&#39;
</code></pre><p><strong>Update</strong>: Thanks to
<a href="https://fosstodon.org/@whynothugo/115836011150423009">Hugo</a> for pointing out
that under Wayland, GTK3 ignores the <code>~/.config/gtk-3.0/settings.ini</code>
configuration file and uses dconf exclusively! Setting the following dconf
setting makes the font rendering match:</p>
<pre tabindex="0"><code>gsettings set org.gnome.desktop.interface font-name &#39;Cantarell 11&#39;
</code></pre><h3 id="swaylock">Screen locker: swaylock</h3>
<p>The obvious replacement for <a href="http://i3wm.org/i3lock/"><code>i3lock</code></a> is
<a href="https://github.com/swaywm/swaylock"><code>swaylock</code></a>.</p>
<p>I quickly ran into a difference in architecture between the two programs:</p>
<ul>
<li>
<p>i3lock shows a screen locker window. When you kill i3lock, the screen is
unlocked.</p>
</li>
<li>
<p>When you kill swaylock, you end up in a <em>Red Screen Of Death</em>.</p>
<p>To get out of this state, you need to restart swaylock and unlock. You can
unlock from the command line by sending <code>SIGUSR1</code> to the <code>swaylock</code> process.</p>
</li>
</ul>
<p>This was very surprising to me, but is by (Wayland) design! See <a href="https://github.com/swaywm/sway/issues/7046">Sway issue
#7046</a> for details, and this quote from
the <a href="https://wayland.app/protocols/ext-session-lock-v1"><code>ext-session-lock-v1</code> Wayland protocol</a>:</p>
<blockquote>
<p>&ldquo;The compositor must stop rendering and provide input to normal
clients. Instead the compositor must blank all outputs with an opaque color
such that their normal content is fully hidden.&rdquo;</p>
</blockquote>
<p>OK, so when you start <code>swaylock</code> via SSH for testing, remember to always unlock
instead of just cancelling <code>swaylock</code> with Ctrl+C. And hope it never crashes.</p>
<p>I used to start <code>i3lock</code> via a wrapper script, which turns off the monitor
(input wakes it up):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#007020">#!/bin/sh
</span></span></span><span style="display:flex;"><span><span style="color:#007020"></span><span style="color:#60a0b0;font-style:italic"># Turns on DPMS, mutes all output, locks the screen.</span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># Reverts all settings on unlock, or when killed.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>revert<span style="color:#666">()</span> <span style="color:#666">{</span>
</span></span><span style="display:flex;"><span>    xset dpms <span style="color:#40a070">0</span> <span style="color:#40a070">0</span> <span style="color:#40a070">0</span>
</span></span><span style="display:flex;"><span>    pactl set-sink-mute @DEFAULT_SINK@ <span style="color:#40a070">0</span>
</span></span><span style="display:flex;"><span><span style="color:#666">}</span>
</span></span><span style="display:flex;"><span><span style="color:#007020">trap</span> revert SIGHUP SIGINT SIGTERM
</span></span><span style="display:flex;"><span>xset +dpms dpms <span style="color:#40a070">15</span> <span style="color:#40a070">15</span> <span style="color:#40a070">15</span>
</span></span><span style="display:flex;"><span><span style="color:#666">(</span>sleep <span style="color:#40a070">1</span> <span style="color:#666">&amp;&amp;</span> xset dpms force off<span style="color:#666">)</span> &amp;
</span></span><span style="display:flex;"><span>pactl set-sink-mute @DEFAULT_SINK@ <span style="color:#40a070">1</span>
</span></span><span style="display:flex;"><span>i3lock --raw 3840x2160:rgb --image ~/i3lock-wallpaper-3840x2160.rgb -n 
</span></span><span style="display:flex;"><span>revert
</span></span></code></pre></div><p>With Wayland, the DPMS behavior has to be implemented differently, with <code>swayidle</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#007020">#!/bin/sh
</span></span></span><span style="display:flex;"><span><span style="color:#007020"></span><span style="color:#60a0b0;font-style:italic"># Turns on DPMS, mutes all output, locks the screen.</span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># Reverts all settings on unlock, or when killed.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>swayidle -w <span style="color:#4070a0;font-weight:bold">\
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-weight:bold"></span>  timeout <span style="color:#40a070">5</span> <span style="color:#4070a0">&#39;swaymsg &#34;output * dpms off&#34;&#39;</span> <span style="color:#4070a0;font-weight:bold">\
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-weight:bold"></span>  resume <span style="color:#4070a0">&#39;swaymsg &#34;output * dpms on&#34;&#39;</span> &amp;
</span></span><span style="display:flex;"><span><span style="color:#bb60d5">swayidle</span><span style="color:#666">=</span><span style="color:#bb60d5">$!</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>revert<span style="color:#666">()</span> <span style="color:#666">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#007020">kill</span> <span style="color:#bb60d5">$swayidle</span>
</span></span><span style="display:flex;"><span>    pactl set-sink-mute @DEFAULT_SINK@ <span style="color:#40a070">0</span>
</span></span><span style="display:flex;"><span><span style="color:#666">}</span>
</span></span><span style="display:flex;"><span><span style="color:#007020">trap</span> revert SIGHUP SIGINT SIGTERM
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>pactl set-sink-mute @DEFAULT_SINK@ <span style="color:#40a070">1</span>
</span></span><span style="display:flex;"><span>swaylock --image ~/i3lock-wallpaper-3840x2160.jpg
</span></span><span style="display:flex;"><span>revert
</span></span></code></pre></div><h3 id="i3-ipc">i3 IPC automation</h3>
<p>The i3 window manager can be extended via its <a href="https://i3wm.org/docs/ipc.html">IPC interface (interprocess
communication)</a>.</p>
<p>I use a few small tools that use this interface.</p>
<p>I noticed the following issues when using these tools with Sway:</p>
<ol>
<li>
<p>Tools using the <a href="https://pkg.go.dev/go.i3wm.org/i3/v4"><code>go.i3wm.org/i3/v4</code> Go
package</a> need a <a href="https://github.com/RasmusLindroth/i3keys/blob/99e368e4cbb4f82f4e9461c3fd43054add0c3c37/i3parse/config.go#L67">special socket path
hook
currently</a>. We
should probably include transparent handling in the package to ease the
transition.</p>
</li>
<li>
<p>Tools started with <code>exec</code> from the Sway config unexpectedly keep running even
when you exit Sway (<code>swaymsg exit</code>) and log into a new session!</p>
</li>
<li>
<p>My
<a href="https://github.com/stapelberg/workspace-populate-for-i3">workspace-populate-for-i3</a>
did not work:</p>
<ul>
<li>Sway does not implement i3’s <a href="https://i3wm.org/docs/layout-saving.html">layout
saving/restoring</a> because Drew
decided in 2017 that <a href="https://github.com/swaywm/sway/issues/1005#issuecomment-331526652">the feature is “too complicated and hacky for too
little
benefit”</a>. Too
bad. I have a couple of layouts I liked that I’ll need to replicate
differently.</li>
<li>Sway does not match workspace nodes with <code>[con_id]</code> criteria. There’s
<a href="https://github.com/swaywm/sway/pull/8980">pull request #8980</a> (posted
independently, five days ago) to fix that.</li>
</ul>
</li>
<li>
<p>My <a href="https://github.com/stapelberg/wsmgr-for-i3">wsmgr-for-i3</a> worked
partially:</p>
<ul>
<li>Restoring workspaces (<code>wsmgr restore</code>) worked.</li>
<li>Sway’s <a href="https://i3wm.org/docs/userguide.html#_renaming_workspaces"><code>rename workspace</code>
command</a>
implementation does not seem to pick up workspace numbers from the target
name.</li>
</ul>
</li>
</ol>
<h3 id="foot">Terminal: foot</h3>
<p>On X11, I use the <a href="https://wiki.archlinux.org/title/Rxvt-unicode">rxvt-unicode</a>
(URxvt) terminal emulator. It has a couple of quality-of-life features that I
don’t want to lose, aside from being fast and coming with a minimal look:</p>
<ul>
<li>Backwards search through your scrollback (= command output)</li>
<li>Opening URLs in your scrollback using keyboard shortcuts</li>
<li>Opening a new terminal window in the same working directory</li>
<li>Updating the terminal title from your shell</li>
</ul>
<p>In earlier experiments, I tried Alacritty or Kitty, but wasn’t happy with
either.</p>
<p>Thanks to <a href="https://anarc.at/software/desktop/wayland/#terminal-xterm-foot">anarcat’s blog post “Wayland: i3 to Sway
migration”</a>, I
discovered the <a href="https://codeberg.org/dnkl/foot"><code>foot</code> terminal emulator</a>, which
looks like a really nice option!</p>
<p>I started a <a href="https://github.com/stapelberg/configfiles/commit/7cc2c08dca5dd195ce47166c57deb44e7d68909d"><code>foot.ini</code> config
file</a>
to match my URxvt config, but later I noticed that at least some colors don’t
seem to match (some text lines with green/red background looked different). I’m
not sure why and have not yet looked into it any further.</p>
<p>I noticed the following issues using <code>foot</code>:</p>
<ul>
<li>
<p>Pressing Ctrl+Enter (which I seem to do by mistake quite a bit) results in
escape sequences, whereas URxvt just treats Ctrl+Enter like Enter.</p>
<p>This can be worked around in your shell (Zsh, in my case), see <a href="https://codeberg.org/dnkl/foot/issues/628">foot issue
#628</a> for details.</p>
</li>
<li>
<p>Double-clicking on part of a URL with the mouse selects the URL (as expected),
but without the <code>https:</code> scheme prefix! Annoying when you do want to use the
mouse.</p>
<p>I can hold Ctrl to work around this, which will make <code>foot</code> select everything
under the pointer up to, and until, the next space characters.</p>
</li>
<li>
<p>Starting <a href="https://manpages.debian.org/screen.1"><code>screen(1)</code></a>
 in <code>foot</code> results in not having
color support for programs running inside the <code>screen</code> session. Probably a
terminfo-related problem somehow…? I can also reproduce this issue with GNOME
terminal. But with URxvt or <a href="https://en.wikipedia.org/wiki/Xterm">xterm</a>, it
works.</p>
</li>
<li>
<p>Selecting text highlights the text within the line, but not the entire line.
This is different from other terminal emulators I am used to, but I don’t see
an option to change it.</p>
<p>Here’s a screenshot showing <code>foot</code> after triple-clicking on the right of
“kthreadd”:</p>

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  <a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-triple-long.png"><img
    srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-triple-long_hu_7510c893c9d05252.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-triple-long_hu_ff181a615a3b5243.png 3x"
    src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-triple-long_hu_ac615569c350b15a.png"
    alt="triple-click in foot on a top(1) output line highlights the whole line" title="triple-click in foot on a top(1) output line highlights the whole line"
    width="600"
    height="188"
    style="
  
  border: 1px solid #000;
  
  "
    
    loading="lazy"></a>
  
  

<p>But triple-clicking on an echo output line highlights only the contents, not
the whole line:</p>

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  <a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-triple-short.png"><img
    srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-triple-short_hu_48f0e7085e53631f.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-triple-short_hu_200619f664efe8fd.png 3x"
    src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-triple-short_hu_d228c42ae1be0723.png"
    alt="triple-click in foot on an echo output line highlights only the contents, not the whole line" title="triple-click in foot on an echo output line highlights only the contents, not the whole line"
    width="600"
    height="188"
    style="
  
  border: 1px solid #000;
  
  "
    
    loading="lazy"></a>
  
  

</li>
</ul>
<h3 id="emacs">Text editor: Emacs</h3>
<p>I find Emacs’s Wayland support rather disappointing. The standard version of
Emacs only supports X11, so on Sway, it starts in Xwayland. Because Sway does
not support scaling with Xwayland, Emacs shows up blurry (top/background
window):</p>















<a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/emacs-blurry.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/emacs-blurry_hu_7d0c5381426a6840.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/emacs-blurry_hu_451ca8625cf37945.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/emacs-blurry_hu_1f4eb4e05ba1b280.png"
  alt="Emacs being blurry in Xwayland" title="Emacs being blurry in Xwayland"
  width="600"
  height="178"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>Native Wayland support (bottom/foreground window) is only available in the
<code>pgtk</code> Emacs version (<code>emacs-pgtk</code> on NixOS). <code>pgtk</code> used to be a separate
branch, but was merged in Emacs 29 (July 2023). There seem to be issues
with <code>pgtk</code> on X11 (you get a warning when starting Emacs-pgtk on X11), so there
have to be two separate versions for now…</p>
<p>Unfortunately, the <code>pgtk</code> text rendering looks different than native X11 text
rendering! The line height and letter spacing seems different:</p>















<a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/2026-01-01-emacs-pgtk-text.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/2026-01-01-emacs-pgtk-text_hu_7e85de1866cbc649.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/2026-01-01-emacs-pgtk-text_hu_a6e6e5e89fa8ab95.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/2026-01-01-emacs-pgtk-text_hu_3f7d94e78f26ace1.png"
  alt="Different text rendering in Emacs (pgtk vs. X11)" title="Different text rendering in Emacs (pgtk vs. X11)"
  width="600"
  height="217"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>I’m not sure why it’s different! Does anybody know how to make it match the old
behavior?</p>
<p>Aside from the different text rendering, the other major issue for me is input
latency: Emacs-pgtk feels significantly slower (less responsive) than
Emacs. This was reported on Reddit multiple times (<a href="https://www.reddit.com/r/emacs/comments/1k9ihp7/emacs_sluggish_ui_with_pgtk_wayland_4k_fractional/">thread
1</a>,
<a href="https://www.reddit.com/r/emacs/comments/1acdieh/pgtk_emacs_high_input_lag_at_large_frame_sizes_on/">thread
2</a>)
and <a href="https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71591">Emacs bug #71591</a>, but
there doesn’t seem to be any solution.</p>
<p>I’ll also need a solution for running Emacs remotely. Thus far, I use X11
forwarding over SSH (which works fine and with low latency over fiber
connections). I should probably check out waypipe, but have not yet had a
chance.</p>
<h3 id="chrome">Browser: Chrome</h3>
<p>When starting Chrome and checking the <code>chrome://gpu</code> debug page, things look
good:</p>















<a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/chrome-gpu-wayland.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/chrome-gpu-wayland_hu_c5d62f89e8e4bd7.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/chrome-gpu-wayland_hu_ffe2cb2ff688e13c.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/chrome-gpu-wayland_hu_546fe7df2c265248.png"
  alt="chrome://gpu on Sway" title="chrome://gpu on Sway"
  width="600"
  height="619"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>But rather quickly, after moving and resizing browser windows, the GPU process
dies with messages like the following and, for example, WebGL is no longer
hardware accelerated:</p>
<pre tabindex="0"><code>ERROR:ui/ozone/platform/wayland/gpu/gbm_pixmap_wayland.cc:95] Cannot create bo with format=RGBA_8888 and usage=Scanout|Rendering|Texturing
ERROR:ui/gfx/linux/gbm_wrapper.cc:405] Failed to create BO with modifiers: Invalid argument (22)
ERROR:ui/ozone/platform/wayland/gpu/gbm_pixmap_wayland.cc:95] Cannot create bo with format=RGBA_8888 and usage=Texturing
ERROR:gpu/command_buffer/service/shared_image/shared_image_factory.cc:981] CreateSharedImage: could not create backing.
ERROR:gpu/command_buffer/service/shared_image/shared_image_manager.cc:397] SharedImageManager::ProduceSkia: Trying to Produce a Skia representation from a non-existent mailbox.
ERROR:components/viz/service/gl/exit_code.cc:13] Restarting GPU process due to unrecoverable error. Context was lost.
R:gpu/ipc/client/command_buffer_proxy_impl.cc:321] GPU state invalid after WaitForGetOffsetInRange.
ERROR:content/browser/gpu/gpu_process_host.cc:1005] GPU process exited unexpectedly: exit_code=8704
</code></pre><p>Of course, using a browser without hardware acceleration is very frustrating,
especially at high resolutions. Starting Chrome with <code>--disable-gpu-compositing</code>
seems to work around the GPU process exiting, but Chrome still does not feel as
smooth as on X11.</p>
<p>Another big issue for me is that Sway does not open Chrome windows on the
workspace on which I closed them. Support for tracking and restoring the
<code>_NET_WM_DESKTOP</code> EWMH atom was added to i3 in <a href="https://github.com/i3/i3/commit/328035fb7e98630862ae8b43088631f62b807c77">January
2016</a>
and to Chrome in <a href="https://chromium.googlesource.com/chromium/src.git/+/06405c5944436b431f26037fdc93340842c51de5%5E%21/">May
2016</a>
and Firefox in <a href="https://hg-edge.mozilla.org/integration/autoland/rev/323e2a212629">March
2020</a>.</p>
<p>I typically have 5+ workspaces and even more Chrome windows at any given point,
so having to sort through 10+ Chrome windows every day (when I boot my work
computer) is <strong>very annoying</strong>.</p>
<p><a href="https://github.com/emersion">Simon Ser</a> said that this would be addressed with
a new Wayland protocol (<a href="https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/18"><code>xdg-session-management</code>, merge request
!18</a>).</p>
<h3 id="screensharing">Screensharing</h3>
<p>I work remotely a lot, so screen sharing is a table-stakes feature for me.  I
use screen sharing in my browser almost every day, in different scenarios and
with different requirements.</p>
<p>In X11, I am used to the following experience with Chrome. I click the “Window”
tab and see previews of my windows. When I select the window and confirm, its
contents get shared:</p>















<a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-x11.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-x11_hu_128aa894a5a9f6a0.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-x11_hu_b26a1a182eda0744.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-x11_hu_5eadab053d491b69.png"
  alt="screensharing behavior in Chrome (X11)" title="screensharing behavior in Chrome (X11)"
  width="600"
  height="338"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>To get screen sharing to work in Wayland/sway, you need to install
<code>xdg-desktop-portal</code> and <code>xdg-desktop-portal-wlr</code> (the latter is specific to
wlroots, which sway uses).</p>
<p>With these packages set up, this is the behavior I see:</p>
<ul>
<li>I can share a Chrome tab.</li>
<li>I can share the entire monitor.</li>
<li>I <em>cannot</em> share a specific window (the entire monitor shows up as a single
window).</li>
</ul>
<p>This is <a href="https://github.com/emersion/xdg-desktop-portal-wlr/issues/107">a limitation of <code>xdg-desktop-portal-wlr</code> (and
others)</a>, which
should be addressed with the upcoming Sway 1.12 release.</p>
<p>I changed my NixOS configuration to use sway and wlroots from git to try it
out. When I click on the “Window” tab, I see a chooser in which I need to select
a window:</p>















<a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select1-featured.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select1-featured_hu_6528a3bb6276452b.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select1-featured_hu_df6e12cd6d0a51ab.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select1-featured_hu_4b360882013550cc.png"
  alt="screensharing behavior in Sway" title="screensharing behavior in Sway"
  width="600"
  height="338"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>After selecting the window, I see <em>only that window’s contents</em> previewed in
Chrome:</p>















<a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select2.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select2_hu_319a311d3fbd9aac.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select2_hu_683c5a6911153f97.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select2_hu_a0d8fb229067ea48.png"
  alt="screensharing behavior in Sway" title="screensharing behavior in Sway"
  width="600"
  height="338"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>After confirming, I get another chooser and need to select the window
again. Notably, there is no connection between the previewed window and the
chosen window in this second step — if I chose a different window, that’s what
will be shared:</p>















<a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select3.png"><img
  srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select3_hu_481526ad70aa37a6.png 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select3_hu_f4e29500e188ce3f.png 3x"
  src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/screenshare-select3_hu_de76f21970bd6130.png"
  alt="screensharing behavior in Sway" title="screensharing behavior in Sway"
  width="600"
  height="338"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>Now that window is screenshared (so the feature now works; nice!), but
unfortunately in low resolution, meaning the text is blurry for my co-workers.</p>
<p>I reported this as <a href="https://github.com/emersion/xdg-desktop-portal-wlr/issues/364">xdg-desktop-portal-wlr issue
#364</a> and it
seems like the issue is that the wrong scale factor is applied. The patch
provided in the issue works for me.</p>
<p>But, on a high level, the whole flow seems wrong: I shouldn’t see a chooser when
clicking on Chrome’s “Window” tab. I should see previews of all windows. I
should be able to select the window in Chrome, not with a separate chooser.</p>
<h3 id="scaling-glitches">Scaling Glitches</h3>
<p>I also noticed a very annoying glitch when output scaling is enabled: the
contents of (some!) windows would “jump around” as I was switching between
windows (in a tabbed or stacked container) or between workspaces.</p>
<p>I first noticed this in the <code>foot</code> terminal emulator, where the behavior is as follows:</p>
<ol>
<li>Switch focus to another <code>foot</code> terminal by changing workspaces, or by
switching focus within a stacked or tabbed container.</li>
<li>The new <code>foot</code> terminal shows up with its text contents slightly offset.</li>
<li>Within a few milliseconds, <code>foot</code>’s text jumps to the correct position.</li>
</ol>
<p>I captured the following frame with my iPhone just as the content was moving a
few pixels, shortly after switching focus to this window:</p>















<a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-move.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-move_hu_a4fdc817f2e959e3.jpg 2x,https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-move_hu_4017ffc8ad6950e4.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/foot-move_hu_ba49c2e51c18f85f.jpg"
  alt="foot content moving around" title="foot content moving around"
  width="600"
  height="338"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>Later, I also noticed that Chrome windows briefly <a href="https://github.com/emersion/xdg-desktop-portal-wlr/issues/364#issuecomment-3702287572">show up blurry after
switching</a>.</p>
<p>My guess is that because Sway sets the scale factor to 1 for invisible windows,
when switching focus you see a scale-1 content buffer until the application
provided its scale-3 content buffer.</p>
<h3 id="notifications-dunst">Notifications: dunst</h3>
<p>dunst supports Wayland natively. I tried dunst 1.13 and did not notice any
issues.</p>
<h3 id="picker-rofi">Picker: rofi</h3>
<p>rofi works on Wayland since v2.0.0 (2025-09-01).</p>
<p>I use rofi with <a href="https://github.com/fdw/rofimoji">rofimoji</a> as my Emoji
picker. For text input, instead of <code>xdotool</code>, <code>wtype</code> seems to work. I didn’t
notice any issues.</p>
<h3 id="screenshots-grim">Screenshots: grim?</h3>
<p>Instead of my usual choice <a href="https://manpages.debian.org/maim.1"><code>maim(1)</code></a>
, I tried <a href="https://manpages.debian.org/grim.1"><code>grim(1)</code></a>
, but unfortunately <code>grim</code>’s <code>-T</code> flag to select the
window to capture is rather cumbersome to use (and captures in 1x scale).</p>
<p>Does anyone have any suggestions for a good alternative?</p>
<h2 id="conclusion">Conclusion</h2>
<p>Finally I made some progress on getting a Wayland session to work in my
environment!</p>
<p>Before giving my verdict on this Wayland/sway experiment, let me explain that my
experience on X11/i3 is really good. I don’t see any tearing or other artifacts
or glitches in my day-to-day computer usage. I don’t use a compositor, so my
input latency is really good: I once measured it to approximately 763 μs in
Emacs on X11 with my custom-built keyboard (plus output latency), see <a href="/posts/2018-04-17-kinx-latency-measurement/">kinX:
latency measurement (2018)</a>.</p>
<p>So from my perspective, switching from this existing, flawlessly working stack
(for me) to Sway only brings downsides. I observe new graphical glitches that I
didn’t have before. The programs I spend most time in (Chrome and Emacs) run
noticeably worse. Because of the different implementations, or because I need to
switch programs entirely, I encounter a ton of new bugs.</p>
<p>For the first time, an on-par Wayland experience seems within reach, but
realistically it will require weeks or even months of work still. In my
experience, debugging sessions quickly take hours as I need to switch graphics
cards and rewire monitors to narrow down bugs. I don’t have the time to
contribute much to fixing these numerous issues unfortunately, so I’ll keep
using X11/i3 for now.</p>
<p>For me, a Wayland/Sway session will be ready as my daily driver when:</p>
<ul>
<li>Sway no longer triggers some key bindings twice some times (“ghost key
presses”)</li>
<li>I no longer see glitches when switching between windows or workspaces in Sway.</li>
<li>Chrome is continuously hardware-accelerated.</li>
<li>Chrome windows are restored to their previous workspace when starting.</li>
<li>Emacs either:
<ul>
<li>Runs via Xwayland and Sway makes scaling work.</li>
<li>Or if its <code>pgtk</code> variant fixes its input latency issues
and can be made to render text the same as before somehow.</li>
</ul>
</li>
</ul>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Self-hosting my photos with Immich]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/"/>
    <id>https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/</id>
    <published>2025-11-29T08:22:05+01:00</published>
    <content type="html"><![CDATA[<p>For every cloud service I use, I want to have a local copy of my data for backup
purposes and independence. Unfortunately, the <code>gphotos-sync</code> tool <a href="https://github.com/gilesknap/gphotos-sync-discussion/discussions/1">stopped
working in March
2025</a> when
Google restricted the OAuth scopes, so I needed an alternative for my existing
Google Photos setup. In this post, I describe how I have set up
<a href="https://immich.app/">Immich</a>, a self-hostable photo manager.</p>
<p>Here is the end result: a few (live) photos from <a href="/posts/2025-09-21-nixcon-2025-trip-report/">NixCon
2025</a>:</p>















<a href="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/2025-11-19-immich-screenshot-featured.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/2025-11-19-immich-screenshot-featured_hu_6928fc2a893484f1.jpg 2x,https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/2025-11-19-immich-screenshot-featured_hu_7c70567581178dd5.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/2025-11-19-immich-screenshot-featured_hu_744c6f048556917c.jpg"
  alt="screenshot of Immich in a web browser" title="screenshot of Immich in a web browser"
  width="600"
  height="504"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h2 id="step-1-hardware">Step 1. Hardware</h2>
<p>I am running Immich on my <a href="/posts/2024-07-02-ryzen-7-mini-pc-low-power-proxmox-hypervisor/">Ryzen 7 Mini PC (ASRock DeskMini
X600)</a>, which
consumes less than 10 W of power in idle and has plenty of resources for VMs (64
GB RAM, 1 TB disk). You can read more about it in my blog post from July 2024:</p>



  <div class="postlink">
      <div>
	<a href="https://michael.stapelberg.ch/posts/2024-07-02-ryzen-7-mini-pc-low-power-proxmox-hypervisor/"><h3>Ryzen 7 Mini-PC makes a power-efficient VM host</h3></a>
      </div>
      <div class="summary">
	
	
	
	
	
	<a href="https://michael.stapelberg.ch/posts/2024-07-02-ryzen-7-mini-pc-low-power-proxmox-hypervisor/" style="min-width: 200px; margin-right: 1.5rem; margin-bottom: 1.5rem"><img src="https://michael.stapelberg.ch/posts/2024-07-02-ryzen-7-mini-pc-low-power-proxmox-hypervisor/240630-server-featured_hu_6c2736729d115f81.jpg" width="200" height="134"/></a>
	
	<p>
	  <a href="https://michael.stapelberg.ch/posts/2024-07-02-ryzen-7-mini-pc-low-power-proxmox-hypervisor/">
	  When I saw the first reviews of the ASRock DeskMini X600 barebone, I was immediately interested in building a home-lab hypervisor (VM host) with it. Apparently, the DeskMini X600 uses less than 10W of power but supports latest-generation AMD CPUs like the Ryzen 7 8700G!

	  <span class="readmore"><a href="https://michael.stapelberg.ch/posts/2024-07-02-ryzen-7-mini-pc-low-power-proxmox-hypervisor/">Read more →</a></span>
	  </a>
	</p>
      </div>
  </div>


<p>I installed <a href="https://proxmox.com/en/">Proxmox</a>, an Open Source virtualization
platform, to divide this mini server into VMs, but you could of course also
install Immich directly on any server.</p>
<h2 id="step-2-install-immich">Step 2. Install Immich</h2>
<p>I created a VM (named “photos”) with 500 GB of disk space, 4 CPU cores and 4 GB of RAM.</p>
<p>For the initial import, you could assign more CPU and RAM, but for normal usage, that’s enough.</p>
<p>I <a href="/posts/2025-06-01-nixos-installation-declarative/">(declaratively) installed
NixOS</a> on that VM as described in this blog post:</p>



  <div class="postlink">
      <div>
	<a href="https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/"><h3>How I like to install NixOS (declaratively)</h3></a>
      </div>
      <div class="summary">
	
	
	
	
	
	<a href="https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/" style="min-width: 200px; margin-right: 1.5rem; margin-bottom: 1.5rem"><img src="https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/nix-snowflake-rainbow-featured_hu_2c3edabdd6900fed.png" width="200" height="200"/></a>
	
	<p>
	  <a href="https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/">
	  For one of my network storage PC builds, I was looking for an alternative to Flatcar Container Linux and tried out NixOS again (after an almost 10 year break). There are many ways to install NixOS, and in this article I will outline how I like to install NixOS on physical hardware or virtual machines: over the network and fully declaratively.

	  <span class="readmore"><a href="https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/">Read more →</a></span>
	  </a>
	</p>
      </div>
  </div>


<p>Afterwards, I enabled Immich, with this exact configuration:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>services<span style="color:#666">.</span>immich <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>  enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>At this point, Immich is available on <code>localhost</code>, but not over the network,
because NixOS enables a firewall by default. I could enable the
<code>services.immich.openFirewall</code> option, but I actually want Immich to only be
available via my Tailscale VPN, for which I don’t need to open firewall access —
instead, I use <code>tailscale serve</code> to forward traffic to <code>localhost:2283</code>:</p>
<pre tabindex="0"><code>photos# tailscale serve --bg http://localhost:2283
</code></pre><p>Because I have <a href="https://tailscale.com/kb/1081/magicdns">Tailscale’s MagicDNS</a>
and <a href="https://tailscale.com/kb/1153/enabling-https">TLS certificate provisioning</a>
enabled, that means I can now open <a href="https://photos.example.ts.net">https://photos.example.ts.net</a> in my browser
on my PC, laptop or phone.</p>
<h2 id="step-2-initial-photos-import">Step 2. Initial photos import</h2>
<p>At first, I tried importing my photos using the official Immich CLI:</p>
<pre tabindex="0"><code>% nix run nixpkgs#immich-cli -- login https://photos.example.ts.net secret
% nix run nixpkgs#immich-cli -- upload --recursive /home/michael/lib/photo/gphotos-takeout
</code></pre><p>Unfortunately, the upload was not running reliably and had to be restarted
manually a few times after running into a timeout. Later I realized that this
was because the Immich server runs background jobs like thumbnail creation,
metadata extraction or face detection, and these background jobs slow down the
upload to the extent that the upload can fail with a timeout.</p>
<p>The other issue was that even after the upload was done, I realized that Google
Takeout archives for Google Photos contain metadata in separate JSON files next
to the original image files:</p>















<a href="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/2025-11-19-google-photos-takeout.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/2025-11-19-google-photos-takeout_hu_c2ca8fd1cf90122a.jpg 2x,https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/2025-11-19-google-photos-takeout_hu_7169d5fe6b33d18d.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/2025-11-19-google-photos-takeout_hu_8ac7b224e95d405d.jpg"
  alt="Takeout: Google Photos formats" title="Takeout: Google Photos formats"
  width="600"
  height="720"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>Unfortunately, these files are not considered by <code>immich-cli</code>.</p>
<p>Luckily, there is a great third-party tool called
<a href="https://github.com/simulot/immich-go">immich-go</a>, which solves both of these
issues! It pauses background tasks before uploading and restarts them
afterwards, which works much better, and it does its best to understand Google
Takeout archives.</p>
<p>I ran <code>immich-go</code> as follows and it worked beautifully:</p>
<pre tabindex="0"><code>% immich-go \
  upload \
  from-google-photos \
  --server=https://photos.example.ts.net \
  --api-key=secret \
  ~/Downloads/takeout-*.zip
</code></pre><h2 id="step-3-install-the-immich-iphone-app">Step 3. Install the Immich iPhone App</h2>
<p>My main source of new photos is my phone, so I installed the Immich app on my
iPhone, logged into my Immich server via its Tailscale URL and enabled automatic
backup of new photos via the icon at the top right.</p>
<p>I am not 100% sure whether these settings are correct, but it seems like camera
photos generally go into Live Photos, and Recent should cover other files…?!</p>
<p>If anyone knows, please send an explanation (or a link!) and I will update the article.</p>















<a href="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/IMG_5893.PNG"><img
  srcset="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/IMG_5893_hu_a062fb5b5187dc76.PNG 2x,https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/IMG_5893_hu_9575452ad065a609.PNG 3x"
  src="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/IMG_5893_hu_8ba00d1bf6088e0a.PNG"
  
  width="600"
  height="1304"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>I also strongly recommend to disable notifications for Immich, because otherwise
you get notifications whenever it uploads images in the background. These
notifications are not required for background upload to work, as <a href="https://www.reddit.com/r/immich/comments/1nnk8i9/comment/nfoffbb/">an Immich
developer confirmed on
Reddit</a>. Open
<em>Settings</em> → <em>Apps</em> → <em>Immich</em> → <em>Notifications</em> and un-tick the permission checkbox:</p>















<a href="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/IMG_5894.PNG"><img
  srcset="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/IMG_5894_hu_eae3457253ed621d.PNG 2x,https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/IMG_5894_hu_b720b6d63d234471.PNG 3x"
  src="https://michael.stapelberg.ch/posts/2025-11-29-self-hosting-photos-with-immich/IMG_5894_hu_3ee31e31d3c5d235.PNG"
  
  width="600"
  height="667"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h2 id="step-4-backup">Step 4. Backup</h2>
<p><a href="https://docs.immich.app/administration/backup-and-restore">Immich’s documentation on
backups</a> contains
some good recommendations. The Immich developers recommend backing up the entire
contents of <code>UPLOAD_LOCATION</code>, which is <code>/var/lib/immich</code> on NixOS. The
<code>backups</code> subdirectory contains SQL dumps, whereas the 3 directories <code>upload</code>,
<code>library</code> and <code>profile</code> contain all user-uploaded data.</p>
<p>Hence, I have set up a systemd timer that runs <code>rsync</code> to copy <code>/var/lib/immich</code>
onto my PC, which is enrolled in a <a href="https://www.backblaze.com/blog/the-3-2-1-backup-strategy/">3-2-1 backup
scheme</a>.</p>
<h2 id="whats-missing">What’s missing?</h2>
<p>Immich (currently?) does not contain photo editing features, so to rotate or
crop an image, I download the image and use <a href="https://www.gimp.org/">GIMP</a>.</p>
<p>To share images, I still upload them to Google Photos (depending on who I share
them with).</p>
<h2 id="why-immich-instead-of">Why Immich instead of…?</h2>
<p>The two most promising options in the space of self-hosted image management
tools seem to be <a href="https://immich.app/">Immich</a> and <a href="http://ente.io/">Ente</a>.</p>
<p>I got the impression that Immich is more popular in my bubble, and Ente made the
impression on me that its scope is far larger than what I am looking for:</p>
<blockquote>
<p>Ente is a service that provides a fully open source, end-to-end encrypted
platform for you to store your data in the cloud without needing to trust the
service provider. On top of this platform, we have built two apps so far: Ente
Photos (an alternative to Apple and Google Photos) and Ente Auth (a 2FA
alternative to the deprecated Authy).</p>
</blockquote>
<p>I don’t need an end-to-end encrypted platform. I already have encryption on the
transit layer (Tailscale) and disk layer (LUKS), no need for more complexity.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Immich is a delightful app! It’s very fast and generally seems to work well.</p>
<p>The initial import is smooth, but only if you use the right tool. Ideally, the
official <code>immich-cli</code> could be improved. Or maybe <code>immich-go</code> could be made the
official one.</p>
<p>I think the auto backup is too hard to configure on an iPhone, so that could
also be improved.</p>
<p>But aside from these initial stumbling blocks, I have no complaints.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[My impressions of the MacBook Pro M4]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-10-31-macbook-pro-m4-impressions/"/>
    <id>https://michael.stapelberg.ch/posts/2025-10-31-macbook-pro-m4-impressions/</id>
    <published>2025-10-31T11:04:59+01:00</published>
    <content type="html"><![CDATA[<p>I have been using a MacBook Pro M4 as my portable computer for the last half a
year and wanted to share a few short impressions. As always, I am not a
professional laptop reviewer, so in this article you won’t find benchmarks, just
subjective thoughts!</p>
<p>Back in 2021, I wrote about the <a href="/posts/2021-11-28-macbook-air-m1/">MacBook Air
M1</a>, which was the first computer I used that
contained Apple’s own ARM-based CPU. Having a silent laptop with long battery
life was a game-changer, so I wanted to keep those properties.</p>
<p>When the US government announced tariffs, I figured I would replace my 4-year
old MacBook Air M1 with a more recent model that should last a few more
years. Ultimately, Apple’s prices remained stable, so, in retrospect, I could
have stayed with the M1 for a few more years. Oh well.</p>
<h2 id="the-nano-textured-display">The nano-textured display</h2>
<p>I went to the Apple Store to compare the different options in
person. Specifically, I was curious about the display and whether the increased
weight and form factor of the MacBook Pro (compared to a MacBook Air) would be
acceptable. Another downside of the Pro model is that it comes with a fan, and I
really like absolutely quiet computers. Online, I read from other MacBook Pro
owners that the fan mostly stays off.</p>
<p>In general, I would have preferred to go with a MacBook Air because it has
enough compute power for my needs and I like the case better (no ventilation
slots), but unfortunately only the MacBook Pro line has the better displays.</p>
<p>Why aren’t all displays nano-textured? The employee at the Apple Store presented
the trade-off as follows: The nano texture display is great at reducing
reflections, at the expense of also making the picture slightly less vibrant.</p>
<p>I could immediately see the difference when placing two laptops side by side:
The bright Apple Store lights showed up very prominently on the normal display
(left), and were almost not visible at all on the nano texture display (right):</p>















<a href="https://michael.stapelberg.ch/posts/2025-10-31-macbook-pro-m4-impressions/2025-10-30-macbooks-displays.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-10-31-macbook-pro-m4-impressions/2025-10-30-macbooks-displays_hu_8c87fe476c5344b6.jpg 2x,https://michael.stapelberg.ch/posts/2025-10-31-macbook-pro-m4-impressions/2025-10-30-macbooks-displays_hu_243730d5507ecc98.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-10-31-macbook-pro-m4-impressions/2025-10-30-macbooks-displays_hu_4cb7e6e8fae8437.jpg"
  alt="MacBook Air (left) vs. MacBook Pro (right)" title="MacBook Air (left) vs. MacBook Pro (right)"
  width="600"
  height="409"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>Personally, I did not perceive a big difference in “vibrancy”, so my choice was
clear: I’ll pick the MacBook Pro over the MacBook Air (despite the weight) for
the nano texture display!</p>
<p>After using the laptop in a number of situations, I am very happy with this
choice. In normal scenarios, I notice no reflections at all (where my previous
laptop did show reflections!). This includes using the laptop on a train (next
to the window), or using the laptop outside in daylight.</p>
<h2 id="specs-m4-or-m4-pro">Specs: M4 or M4 Pro?</h2>
<p>(When I chose the new laptop, Apple’s M4 chips were current. By now, they have
released the first devices with M5 chips.)</p>
<p>I decided to go with the MacBook Pro with M4 chip instead of the M4 <strong>Pro</strong> chip
because I don’t need the extra compute, and the M4 needs less cooling — the M4
Pro apparently runs hotter. This increases the chance of the fan staying off.</p>
<p>Here are the specs I ended up with:</p>
<ul>
<li>14&quot; Liquid Retina XDR Display with nano texture</li>
<li>Apple M4 Chip (10 core CPU, 10 core GPU)</li>
<li>32 GB RAM (this is the maximum!), 2 TB SSD (enough for this computer)</li>
</ul>
<h2 id="impressions">Impressions</h2>
<p>One thing I noticed is that the MacBook Pro M4 sometimes gets warm, even when it
is connected to power, but is suspended to RAM (and has been fully charged for
hours). I’m not sure why.</p>
<p>Luckily, the fan indeed stays silent. I think I might have heard it spin up once
in half a year or so?</p>
<p>The battery life is amazing! The previous MacBook Air M1 had amazing all-day
battery life already, and this MacBook Pro M4 lasts even longer. For example,
watching videos on a train ride (with VLC) for 3 hours consumed only 10% of
battery life. I generally never even carry the charger.</p>
<p>Because of that, Apple’s re-introduction of MagSafe, a magnetic power connector
(so you don’t damage the laptop when you trip over it), is nice-to-have but
doesn’t really make much of a difference anymore. In fact, it might be better to
pack a USB-C cable when traveling, as that makes you more flexible in how you
use the charger.</p>
<h2 id="120-hz-display">120 Hz display</h2>
<p>I was curious whether the 120 Hz display would make a difference in practice. I
mostly notice the increased refresh rate when there are animations, but not,
for example, when scrolling.</p>
<p>One surprising discovery (but obvious in retrospect) is that even non-animations
can become faster. For example, when running a Go web server on <code>localhost</code>, I
noticed that navigating between pages by clicking links felt faster on the 120
Hz display!</p>
<p>The following illustration shows why that is, using a page load that takes 6ms
of processing time. There are three cases (the illustration shows an average
case and the worst case):</p>
<ol>
<li>Best case: Page load finishes <em>just before</em> the next frame is displayed: no delay.</li>
<li>Worst case: Page load finishes <em>just after</em> a frame is displayed: one frame of delay.</li>
<li>Most page loads are somewhere in between. We’ll have 0.x to 1.0 frames of delay</li>
</ol>




<a href="https://michael.stapelberg.ch/posts/2025-10-31-macbook-pro-m4-impressions/2025-10-31-delay-60-vs-120.svg"><img
  src="https://michael.stapelberg.ch/posts/2025-10-31-macbook-pro-m4-impressions/2025-10-31-delay-60-vs-120.svg"
  alt="delay" title="delay"
  style="

border: 1px solid #000;

margin-right: 1rem"
  
  loading="lazy"></a>


<p>As you can see, the waiting time becomes shorter when going from 60 Hz (one
frame every 16.6ms) to 120 Hz (one frame every 8.3ms). So if you’re working with
a system that has &lt;8ms response times, you might observe actions completing (up
to) twice as fast!</p>
<p>I don’t notice going back to 60 Hz displays on computers. However, on phones,
where a lot more animations are a key part of the user experience, I think 120
Hz displays are more interesting.</p>
<h2 id="conclusion">Conclusion</h2>
<p>My ideal MacBook would probably be a MacBook Air, but with the nano-texture display! :)</p>
<p>I still don’t like macOS and would prefer to run Linux on this laptop. But
<a href="https://asahilinux.org/">Asahi Linux</a> still needs some work before it’s usable
for me (I need external display output, and M4 support). This doesn’t bother me
too much, though, as I don’t use this computer for serious work.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[NixCon 2025 Trip Report 🐝]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/"/>
    <id>https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/</id>
    <published>2025-09-21T09:34:00+02:00</published>
    <content type="html"><![CDATA[<p>I liked the NixOS meetup earlier this year, and at the end of the meetup they
told everyone about NixCon 2025, which would be happening in Switzerland this
year, at the very same location, the <a href="https://www.ost.ch/">University Of Applied Sciences
OST</a> in Rapperswil, so I decided to go! In this trip
report, I want to give you a rough impression of how I experienced this awesome
conference :)</p>
<p><em>The bee in the title is a NixCon inside joke ;)</em></p>
<h2 id="friday">Friday</h2>
<p>I arrived at about 09:30 on a rainy Friday morning, meaning I hurried from the
train station into OST building 1 to show my ticket QR code and pick up my
conference badge and custom name tag that I pre-ordered. The custom ones have
your name engraved and come with a strong magnet to attach them to your clothes:</p>















<a href="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5775.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5775_hu_2ab24eefb66f32dc.jpg 2x,https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5775_hu_5badb8e34d0d757f.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5775_hu_f608afe3d2f3ee1.jpg"
  alt="regular and custom name tag" title="regular and custom name tag"
  width="600"
  height="450"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>After grabbing a bite to eat, I headed to the main lecture hall for the opening
session. <a href="https://www.ost.ch/en/person/farhad-d-mehta-8699">Prof. Dr. Farhad
Mehta</a> from OST, as well as
the entire NixCon orga team, welcomed the 450 registered attendees to the 10th
NixCon! I recognized many familiar faces from the Nix meetup, but many hands
went up when the audience was asked for whom it was the first time at NixCon, or
in Switzerland in general.</p>
<p>I want to thank Prof. Mehta in particular for making possible such meetups and
events! 👏</p>
<p>If you work at a university, school or other organisation that has access to
rooms, consider offering to host a meetup (on a regular basis, or even just
once)! Locations are always hard to find, so offering a space is a great
contribution to Open Source.</p>
<h3 id="what-if-github-actions-were-local-first-and-built-using-nix">“What if GitHub Actions were local-first and built using Nix?”</h3>
<p>The first technical talk of the day was “What if GitHub Actions were local-first
and built using Nix?” by Domen Kožar, the person behind
<a href="https://cachix.org">cachix.org</a>, which is a hosted Nix cache. The talk pitched
<a href="https://cloud.devenv.sh/">cloud.devenv.sh</a>, which is a Nix-based CI solution
(like GitHub Actions) using <a href="https://devenv.sh/">devenv</a>.</p>
<p>By using this solution, you solve the problem that you can’t easily / completely
run GitHub Actions locally (yes, we all know about
<a href="https://github.com/nektos/act">act</a>), and you get to (?) write Nix configs
instead of YAML configs.</p>
<p>The solution seems nice, but I found the talk a little unstructured because the
presenter jumped around between slides so much. One crucial question was left
unanswered: How do you integrate this custom solution with your GitHub projects?
To me, diverging from the default way of configuring GitHub Actions does not
seem worth it for my projects. YMMV.</p>
<p><a href="https://media.ccc.de/v/nixcon2025-56408-what-if-github-actions">→ watch the recording (46 minutes) on
media.ccc.de</a></p>
<h3 id="rewriting-the-hydra-queue-runner-in-rust">“Rewriting the Hydra Queue Runner in Rust”</h3>
<p>Next up: “Rewriting the Hydra Queue Runner in Rust” by Simon Hauser from
<a href="https://helsinki-systems.de/">Helsinki Systems</a>, a small German software
company. <a href="https://github.com/NixOS/hydra">Hydra</a> is the component in the
NixOS infrastructure which schedules builds: when nixpkgs changes, this is
the component that runs the build whose result ends up on
<a href="https://cache.nixos.org">cache.nixos.org</a> (the Debian equivalent is
<a href="https://wiki.debian.org/buildd">buildd</a>).</p>
<p>Simon explained that bottlenecks in the current queue runner result in
stranding of infrastructure: the project has machines available that it
cannot use fully. He outlined how they replaced a crufty SSH-based automation
with a well-designed gRPC protocol. I got the impression that a group of
people was involved in developing and reviewing this design, which is a great
sign for a healthy project.</p>
<p>One thing that was unfortunately missing from the talk were metrics. It would
have been great to see a few graphs that illustrate just how much better the
rewritten queue runner is.</p>
<p>Currently, the new queue runner is already used for Nix Community builds, but
not yet in production for NixOS itself. Hopefully soon, though!</p>
<p><a href="https://media.ccc.de/v/nixcon2025-56409-rewriting-the-hydra-que">→ watch the recording (27 minutes) on
media.ccc.de</a></p>
<h3 id="you-cant-spell-devshell-without-hell">“You can&rsquo;t spell &ldquo;devshell&rdquo; without &ldquo;hell&rdquo;”</h3>
<p>This talk was presented by Zach Mitchell from <a href="https://flox.dev/">Flox</a>, which
is a Nix-based dev environment solution. Thus far, I use <code>nix-shell</code> or <code>nix develop</code> (see <a href="/posts/2025-07-27-dev-shells-with-nix-4-quick-examples/">Development shells with Nix: four quick
examples</a>), so I was
curious what I’d learn from this talk.</p>
<p>Zach explained that both, <code>nix-shell</code> and <code>nix develop</code> were originally
written to debug Nix package builds, not to provide general-purpose
development environments. For users, this manifests as not being able to use
your favorite shell — <code>nix develop</code> only supports Bash. One might read about
<code>nix develop -c exec &lt;shell&gt;</code>, but that’s wrong, because then the shell’s RC
files run <strong>after</strong> Nix setup, possibly destroying parts of the setup.</p>
<p>One interesting thing I learnt is that the Nix garbage collector scans
<code>/proc</code> to avoid removing Nix store paths that are still needed by running
processes.</p>
<p>Zach mentioned <a href="https://github.com/zmitchell/proctrace">https://github.com/zmitchell/proctrace</a>, which is a
bpftrace-based profiler that tracks forks/execs and generates gantt chart
syntax of the timing. Sounds cool, but is unfortunately broken right now…?
Too bad.</p>
<p><a href="https://media.ccc.de/v/nixcon2025-56410-you-cant-spell-devshell">→ watch the recording (45 minutes) on
media.ccc.de</a></p>
<h3 id="the-nix-binary-cache-and-aws">“The Nix Binary Cache and AWS”</h3>
<p>In this fireside chat, Tarus Balog shared how he ended up at AWS after 20 years
of Open Source, and how his team wants to give back to the community. One
specific way in which they’re doing that is by hosting cache.nixos.org.</p>
<p><a href="https://media.ccc.de/v/nixcon2025-56403-the-nix-binary-cache-an">→ watch the recording (24 minutes) on
media.ccc.de</a></p>
<h3 id="nix-based-development-environments-at-shopify-reprise">“Nix-based development environments at Shopify (reprise)”</h3>
<p>Josh Heinrichs from Shopify shared how they adopted Nix (again!), and I think
real-world enterprise adoption stories like these are very interesting.</p>
<p>In summary, Shopify had a <code>dev</code> command (since 2016), which offered declarative
configuration and then dispatched to <code>apt</code> (Linux) or <code>homebrew</code> (macOS). In the
first attempt to move to Nix, the effort didn’t reach stable footing (some folks
couldn’t use it yet) and then a company-wide shift to cloud development
happened, where the easier solution was to “just use ubuntu”.</p>
<p>A few years in, folks are apparently not so happy with the cloud development
environments and one day, Shopify CEO <a href="https://en.wikipedia.org/wiki/Tobias_L%C3%BCtke">Tobias
Lütke</a> finds
<a href="https://devenv.sh">devenv</a>, which is a Nix-based solution that is remarkably
similar to Shopify’s <code>dev</code>. So Tobi adopts devenv for one of their services and
becomes supportive of using Nix. This time around, they spend a lot more time on
a successful rollout within the organization, meaning incremental adoption,
getting all stakeholders on board, etc.</p>
<p>The takeaway is that one specific, well-supported use-case can be the adoption
driver. And once you have your development environments on a Nix-based solution,
you can more easily adopt other parts of the ecosystem as well.</p>
<p><a href="https://media.ccc.de/v/nixcon2025-56413-nix-based-development-e">→ watch the recording (19 minutes) on
media.ccc.de</a></p>
<h3 id="my-first-nix-aha-a-newcomers-perspective">“My first Nix Aha!: A Newcomer’s Perspective”</h3>
<p>In a similar spirit to the Shopify talk, Kavisha Kumar from ASML shared how she
got into Nix after seeing a colleague use <code>nix-shell</code> to obtain a clean
development shell.</p>
<p>Kavisha spent a lot of time at ASML to teach others about why and how to use
Nix. She shared a number of nice metaphors that explained Nix concepts through
the subject area of video gaming.</p>
<p>I think many people are excited about Nix, but have trouble conveying that
excitement to others. Kavisha showed us a good way that worked for her.</p>
<p><a href="https://media.ccc.de/v/nixcon2025-56414-my-first-nix-aha-a-newc">→ watch the recording (19 minutes) on
media.ccc.de</a></p>
<h3 id="lightning-talks">Lightning Talks</h3>
<p>The rest of the day was filled with lightning talks.</p>
<p>Cole Mickens from <a href="https://determinate.systems/">Determinate Systems</a> explained
what features they are currently shipping in their downstream distribution
“Determinate Nix” (features will be upstreamed): lazy trees (a performance
optimization for evaluating Flakes), parallel evaluation (brings evaluation
times down from 16s to 7s) and a native Linux builder for mac. Next up are Flake
Schemas, which I haven’t read about yet.</p>
<p>Yvan Sraka from <a href="https://numtide.com/">Numtide</a>, a Nix and DevOps consultancy,
showed how he manages Linux machines for friends and family with NixOS. He has
his own configuration layer on top of NixOS and only uses the system as a
base. Most actual programs are used through AppImage, Flatpaks,
<a href="https://github.com/Mic92/envfs">envfs</a> and
<a href="https://github.com/nix-community/nix-ld">nix-ld</a>. The latter two are solutions
to use FHS based programs (those that expect <code>/usr/bin</code> and other standard
locations to be present) on non-FHS systems like NixOS. I had heard of nix-ld
before, but not of envfs.</p>
<p>Jacek Galowicz from <a href="https://nixcademy.com">Nixcademy</a> showed how to use
systemd-sysupdate and systemd-repart to implement A/B style updates with NixOS
and systemd. It’s great to see that this technique is more and more mainstream,
as I am also using A/B style updating successfully in
<a href="https://gokrazy.org/">gokrazy</a>.</p>
<h2 id="saturday">Saturday</h2>
<p>The weather on Saturday was a lot better, so I made sure to get a seat with a
view of Lake Zürich:</p>















<a href="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5754.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5754_hu_707d7fbf62a50ff9.jpg 2x,https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5754_hu_c44ab49e54db14bc.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5754_hu_91ced09efd5ff807.jpg"
  alt="lake view!" title="lake view!"
  width="600"
  height="450"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h3 id="the-bikes-have-been-shed-the-official-nix-formatter">“The bikes have been shed: The official Nix formatter”</h3>
<p>In this talk, Silvan Mosberger from Tweag (and one of the main NixCon
organizers!), explains how the official formatting tool for .nix files came to
be.</p>
<p>I was delighted to hear <code>gofmt</code>, the official Go formatter, being mentioned as a
source of inspiration. Just like in other language ecosystems, introducing
uniform formatting eliminates time-consuming back-and-forth in code review over
adhering to coding style. Unfortunately, the formatting folks did not replicate
one key aspect to gofmt’s success: gofmt has no options. As the famous Go
proverb goes:</p>
<blockquote>
<p>Gofmt&rsquo;s style is no one&rsquo;s favorite, yet gofmt is everyone&rsquo;s favorite!</p>
</blockquote>
<p>Meaning that it’s more important that everyone uses the same style, compared to
everyone being able to express their personal style preferences.</p>
<p><a href="https://media.ccc.de/v/nixcon2025-56375-the-bikes-have-been-she">→ watch the recording (20 minutes) on
media.ccc.de</a></p>
<h3 id="mastering-nixos-integration-tests-advanced-techniques-for-fast-and-robust-multi-vm-tests">“Mastering NixOS Integration Tests: Advanced Techniques for Fast and Robust Multi-VM Tests”</h3>
<p>In this two-hour workshop, Jacek Galowicz from
<a href="https://nixcademy.com">Nixcademy</a>, who is not only a Nix teacher, but also
happens to be the maintainer of the NixOS integration test driver, shows us how
to write complex integration tests with a few lines of Nix and Python.</p>
<p>Jacek showed an integration test example: a Bittorrent service, consisting of
tracker, clients, firewalls and multiple networks! Nixpkgs contains over 1000
such integration tests, and running one on your laptop is easy.</p>
<p>The various ways to debug your tests seem pretty cool: using vsock instead of
port forwardings, and enabling a debug hook that will make a failed test hang
and wait to be debugged.</p>
<p>I thought this was a great overview and Jacek is an engaging teacher. I would
recommend booking his classes!</p>
<h3 id="when-not-to-nix-working-with-external-config-and-sops-nix">“When Not to Nix: Working with External Config and SOPS Nix”</h3>
<p>Ryota spoke about when to use Nix and when not to use Nix. For example, you
could manage your dotfiles (config files) with Nix, or you could decide not
to. Having recently migrated more and more machines and configurations to Nix, I
found myself agreeing with this talk: It’s important to understand what you’ll
get out of declaratively or statefully managed configs, and when which approach
is better.</p>
<p><a href="https://media.ccc.de/v/nixcon2025-56381-when-not-to-nix-working">→ watch the recording (19 minutes) on
media.ccc.de</a></p>
<h3 id="lightning-talks-1">Lightning Talks</h3>
<p>The rest of the day I spent in lightning talks, some of which were sponsored
talk slots. I learnt about, in no particular order:</p>
<ul>
<li><a href="https://www.cloudhypervisor.org/">Cloud Hypervisor</a>, a KVM based hypervisor like qemu, but written in Rust.</li>
<li><a href="https://nixbuild.net/">nixbuild.net</a>, a pay-as-you-go offering for extra
build capacity you can rent. On Sunday I heard someone say that their company
is using nixbuild.net and it’s very smooth.</li>
<li><a href="https://nix-ci.com/">NixCI</a>, a Nix-based hosted CI. So, the cloud.devenv.sh
service we heard about on Friday is a competitor to this service.</li>
<li><a href="https://flox.dev/nixinthewild/">Nix in the Wild</a> is an effort by Flox where
they do 45-60 minute interviews about Nix success stories. This might help you
convince folks in your organization.</li>
<li><a href="https://clan.lol">clan</a> is a fleet management solution.</li>
<li><a href="https://novacustom.com/">NovaCustom</a>, a one-person laptop/PC company. The
laptops come with coreboot and work with NixOS.</li>
<li>ExpressVPN is migrating their internal server setup (TrustedServer) from
Debian to NixOS! Deploying weekly in 105+ countries.</li>
<li>Cyberus, a German company, is offering NixOS LTS releases, compliant with the
EU Cyber Resilience Act obligations.</li>
<li>David’s <a href="https://github.com/dnr/styx">styx</a> project is a more
bandwidth-efficient download mechanism for NixOS updates. This uses
<a href="https://en.wikipedia.org/wiki/EROFS">EROFS</a>, which seems like an interesting
alternative to SquashFS images.</li>
</ul>
<p>After all the talks, we met outside for a group picture followed by barbecue at
the lake:</p>















<a href="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/NixCon2025_arik-grahl.de_CC-BY-SA-4.0-3200w-featured.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/NixCon2025_arik-grahl.de_CC-BY-SA-4.0-3200w-featured_hu_f49e046e26d2953f.jpg 2x,https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/NixCon2025_arik-grahl.de_CC-BY-SA-4.0-3200w-featured_hu_5c0748c0810ca3da.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/NixCon2025_arik-grahl.de_CC-BY-SA-4.0-3200w-featured_hu_8ba18aa31755731f.jpg"
  alt="NixCon 2025 group picture" title="NixCon 2025 group picture"
  width="600"
  height="400"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p><em>NixCon 2025 by Arik Grahl. Licensed under CC BY-SA 4.0.</em></p>
<h2 id="sunday">Sunday</h2>
<p>Before the conference, I wasn’t sure if I would even bother showing up for
Sunday (Hack day), but on Sunday, I was like “of course!”, and it was a great
decision!</p>
<p>Many people were still around and were working on their projects. It felt like
the answer to any Nix question was just one chat message away — there was
expertise and helping hands from many parts of the project.</p>
<p>I ended up meeting a couple of people I only knew from online interactions
before, and we also talked a lot about meetups. Now, I am invited to multiple
meetups to give a talk :D</p>
<h2 id="conclusion">Conclusion</h2>
<p>This was a wonderful conference! The orga team and all contributors did a great
job!</p>
<p>As always, the OST in Rapperswil is a great venue for Open Source events.</p>
<p>Ticket sales and talk submission / scheduling were done using the
<a href="http://pretix.eu/">Pretix</a> and <a href="https://pretalx.com/p/about/">Pretalx</a> Open
Source systems, which makes me proud to have contributed to Pretix.</p>
<p>The selection of talks was great: Some deeply technical, some covering only the
human side of things, and many somewhere in between. I got the impression that
all the presenters I saw genuinely cared about their topic, so the overall
energy was very good!</p>
<p>(You can watch the talk recordings at <a href="https://media.ccc.de/c/nixcon2025">media.ccc.de: NixCon
2025</a>.)</p>
<p>Also outside of the talks, I had many friendly interactions and interesting
conversations. There is a lot of interest and adoption of Nix, which is great to
see!</p>
<p>The production level of the conference was <em>very high</em> for such a
volunteer-driven event. For example, the very cool sounding break music between
talks was created specifically for NixCon: <a href="https://tonstrstudio.bandcamp.com/album/lava">“Lava” by
tonstr.studio</a>. Similarly, the
welcome bag contained dark Swiss chocolate, specifically made for NixCon (see
picture below). I don’t even like dark chocolate, but this one was delicious!</p>
<p>Thanks again to all helpers, and I look forward to coming back soon!</p>















<a href="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5755.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5755_hu_9e2fa5817975ea55.jpg 2x,https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5755_hu_8c2c2412f4af86af.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-09-21-nixcon-2025-trip-report/IMG_5755_hu_8736ee28b37b1d95.jpg"
  alt="NixCon 2025 Swiss chocolate" title="NixCon 2025 Swiss chocolate"
  width="600"
  height="800"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Bye Intel, hi AMD! I’m done after 2 dead Intels]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/"/>
    <id>https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/</id>
    <published>2025-09-07T08:33:00+02:00</published>
    <content type="html"><![CDATA[<p>The Intel 285K CPU in my <a href="/posts/2025-05-15-my-2025-high-end-linux-pc/">high-end 2025 Linux
PC</a> died <strong>again</strong>! 😡 Notably,
this was the replacement CPU for the original 285K that <a href="/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/">died in
March</a>, and
after reading through the reviews of Intel CPUs on my electronics store of
choice, many of which (!) mention CPU replacements, I am getting the impression
that Intel’s current CPUs just are not stable 😞. Therefore, I am giving up on
Intel for the coming years and have bought an AMD Ryzen 9950X3D CPU instead.</p>
<h2 id="what-happened-or-the-batch-job-of-death">What happened? Or: the batch job of death</h2>
<p>On the 9th of July, I set out to experiment with
<a href="https://layout-parser.github.io/">layout-parser</a> and
<a href="https://en.wikipedia.org/wiki/Tesseract_(software)">tesseract</a> in order to
convert a collection of scanned paper documents from images into text.</p>
<p>I expected that offloading this task to the GPU would result in a drastic
speed-up, so I attempted to build layout-parser with
<a href="https://en.wikipedia.org/wiki/CUDA">CUDA</a>. Usually, it’s not required to
compile software yourself on <a href="https://nixos.org/">NixOS</a>, but CUDA is non-free,
so the default NixOS cache does not compile software with CUDA. (Tip: Enable the
<a href="https://nix-community.org/cache/">Nix Community Cache</a>, which contains prebuilt
CUDA packages, too!)</p>
<p>This lengthy compilation attempt failed with a weird symptom: I left for work,
and after a while, my PC was no longer reachable over the network, but fans kept
spinning at 100%! 😳 At first, <a href="https://mas.to/@zekjur/114822353514097399">I suspected a Linux
bug</a>, but now I am thinking this was
the first sign of the CPU being unreliable.</p>
<p>When the CUDA build failed, I ran the batch job without GPU offloading
instead. It took about 4 hours and consumed roughly 300W constantly. You can see
it on this CPU usage graph (screenshot of a <a href="https://grafana.com/">Grafana</a>
dashboard showing metrics collected by <a href="https://prometheus.io/">Prometheus</a>):</p>















<a href="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-cpu.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-cpu_hu_9a0c4beab08cd5e4.jpg 2x,https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-cpu_hu_e2bc3f9689ff66b8.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-cpu_hu_d1235bb0543c21b1.jpg"
  alt="CPU usage (measured with Prometheus)" title="CPU usage (measured with Prometheus)"
  width="600"
  height="195"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>


















<a href="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-temp.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-temp_hu_c6884625e3a8a3cf.jpg 2x,https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-temp_hu_8a99527dfded461e.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-temp_hu_1197124e615ec242.jpg"
  alt="CPU temperature (measured with Prometheus)" title="CPU temperature (measured with Prometheus)"
  width="600"
  height="196"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>On the evening of the 9th, the computer still seemed to work fine.</p>
<p>But the next day, when I wanted to wake up my PC from suspend-to-RAM as usual,
it wouldn’t wake up. Worse, even after removing the power cord and waiting a few
seconds, there was no reaction to pressing the power button.</p>
<p>Later, I diagnosed the problem to either the mainboard and/or the CPU. The Power
Supply, RAM and disk all work with different hardware. I ended up returning both
the CPU and the mainboard, as I couldn’t further diagnose which of the two is
broken.</p>
<p>To be clear: I am not saying the batch job killed the CPU. The computer was
acting strangely in the morning already. But the batch job might have been what
really sealed the deal.</p>
<h2 id="no-it-wasnt-the-heat-wave">No, it wasn’t the heat wave</h2>
<p><a href="https://www.tomshardware.com/pc-components/cpus/firefox-dev-says-intel-raptor-lake-crashes-are-increasing-with-rising-temperatures-in-record-european-heat-wave-mozilla-staffs-tracking-overwhelmed-by-intel-crash-reports-team-disables-the-function">Tom’s Hardware recently
reported</a>
that “Intel Raptor Lake crashes are increasing with rising temperatures in
record European heat wave”, which prompted some folks to blame Europe’s general
lack of Air Conditioning.</p>
<p>But in this case, I actually <strong>did air-condition the room</strong> about half-way
through the job (at about 16:00), when I noticed the room was getting
hot. Here’s the temperature graph:</p>















<a href="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-roomtemp.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-roomtemp_hu_1c38592935786593.jpg 2x,https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-roomtemp_hu_6501c7c4a99a9259.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-07-19-roomtemp_hu_f4d3a15f7c3ae485.jpg"
  alt="temperature graph (measured with HomeMatic sensors)" title="temperature graph (measured with HomeMatic sensors)"
  width="600"
  height="215"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>I would say that 25 to 28 degrees celsius are normal temperatures for computers.</p>
<p>I also double-checked if the CPU temperature of about 100 degrees celsius is too
high, but no: <a href="https://www.tomshardware.com/pc-components/cooling/intel-core-ultra-9-285k-cooling-testing-how-much-does-it-take-to-keep-arrow-lake-cool-in-msis-mpg-gungnir-300r-airflow-pc-case/2">this Tom’s Hardware
article</a>
shows even higher temperatures, and Intel specifies a maximum of 110
degrees. So, running at “only” 100 degrees for a few hours should be fine.</p>
<p>Lastly, even if Intel CPUs were prone to <em>crashing</em> under high heat, they should
<em>never die</em>.</p>
<h2 id="which-amd-cpu-to-buy">Which AMD CPU to buy?</h2>
<p>I wanted the fastest AMD CPU (for desktops, not for servers), which currently is
the Ryzen 9 9950X, but there is also the Ryzen 9 9950X<strong>3D</strong>, a variant with 3D
V-Cache. Depending on the use-case, the variant with or without 3D V-Cache is
faster, see <a href="https://www.phoronix.com/review/amd-ryzen-9-9950x3d-linux/10">the comparison on
Phoronix</a>.</p>
<p>Ultimately, I decided for the 9950X3D model, not just because it performs better
in many of the benchmarks, but also because Linux 6.13 and newer <a href="https://www.phoronix.com/review/amd-3d-vcache-optimizer-9950x3d">let you
control whether to prefer the CPU cores with larger V-Cache or higher
frequency</a>,
which sounds like an interesting capability: By changing this setting, maybe one
can see how sensitive certain workloads are to extra cache.</p>
<p>Aside from the CPU, I also needed a new mainboard (for AMD’s socket AM5), but I
kept all the other components. I ended up selecting the <a href="https://www.asus.com/ch-en/motherboards-components/motherboards/tuf-gaming/tuf-gaming-x870-plus-wifi/">ASUS TUF
X870+</a>
mainboard. I usually look for low power usage in a mainboard, so I made sure to
go with an X870 mainboard instead of an X870E one, because the X870E has two
chipsets (both of which consume power and need cooling)! Given the context of
this hardware replacement, I also like the TUF line’s focus on endurance…</p>
<h2 id="performance">Performance</h2>
<p>The performance of the AMD 9950X3D seems to be slightly better than the Intel
285K:</p>
<table>
  <thead>
      <tr>
          <th>Workload</th>
          <th><a href="/posts/2022-01-15-high-end-linux-pc/">12900K (2022)</a></th>
          <th><a href="/posts/2025-05-15-my-2025-high-end-linux-pc/">285K (2025)</a></th>
          <th>9950X3D (2025)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://go.dev/dl/">build Go 1.24.3</a></td>
          <td>≈35s</td>
          <td>≈26s</td>
          <td>≈24s</td>
      </tr>
      <tr>
          <td><a href="https://github.com/gokrazy/rsync/tree/0c5ac23ecf8b337dd5672c2ae9f945defa5d0b7f">gokrazy/rsync tests</a></td>
          <td>≈0.5s</td>
          <td>≈0.4s</td>
          <td>≈0.5s</td>
      </tr>
      <tr>
          <td><a href="https://github.com/gokrazy/kernel/tree/699ad7a064b8702dbe91b801ea21c2da2f0e9737">gokrazy Linux compile</a></td>
          <td>3m 13s</td>
          <td>2m 7s</td>
          <td>1m 56s</td>
      </tr>
  </tbody>
</table>
<p>In case you’re curious, the commands used for each workload are:</p>
<ol>
<li><code>cd src; ./make.bash</code></li>
<li><code>make test</code></li>
<li><code>gokr-rebuild-kernel -cross=arm64</code></li>
</ol>
<p>(I have not included the gokrazy UEFI integration tests because I think there is
an unrelated difference that prevents comparison of my old results with how the
test runs currently.)</p>
<h2 id="power-consumption">Power consumption</h2>
<p>In my <a href="/posts/2025-05-15-my-2025-high-end-linux-pc/">high-end 2025 Linux PC</a> I
explained that I chose the Intel 285K CPU for its lower idle power consumption,
and some folks were skeptical if AMD CPUs are really worse in that regard.</p>
<p>Having switched between 3 different PCs, but with identical peripherals, I can
now answer the question of how the top CPUs differ in power consumption!</p>
<p>I picked a few representative point-in-time power values from a couple of days
of usage:</p>
<table>
  <thead>
      <tr>
          <th>CPU</th>
          <th>Mainboard</th>
          <th>idle power</th>
          <th>idle power with monitor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Intel 12900k</td>
          <td>ASUS PRIME Z690-A</td>
          <td>40W</td>
          <td>60W</td>
      </tr>
      <tr>
          <td>Intel 285k</td>
          <td>ASUS PRIME Z890-P</td>
          <td>46W</td>
          <td>65W</td>
      </tr>
      <tr>
          <td>AMD 9950X3D</td>
          <td>ASUS TUF GAMING X870-PLUS WIFI</td>
          <td>55W</td>
          <td>80W</td>
      </tr>
  </tbody>
</table>
<p>Looking at two typical evenings, here is the power consumption of the Intel 285K
(measured using a <a href="https://mystrom.com/de/produkt/mystrom-wifi-switch-eu/">myStrom WiFi switch smart
plug</a>, which comes with
a REST API):</p>















<a href="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-09-01-power-285k.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-09-01-power-285k_hu_7236808eb271f880.jpg 2x,https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-09-01-power-285k_hu_f394efdd55708e71.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-09-01-power-285k_hu_b51bdfb7439fc3e4.jpg"
  alt="Power consumption of the Intel 285K-based PC" title="Power consumption of the Intel 285K-based PC"
  width="600"
  height="250"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>…and here is the same PC setup, but with the AMD 9950X3D:</p>















<a href="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-09-01-power-9950x3d.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-09-01-power-9950x3d_hu_15d057b3b8ad161d.jpg 2x,https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-09-01-power-9950x3d_hu_58469d76dc5fb4e.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-09-07-bye-intel-hi-amd-9950x3d/2025-09-01-power-9950x3d_hu_54a6909b22e1209f.jpg"
  alt="Power consumption of the AMD 9950X3D-based PC" title="Power consumption of the AMD 9950X3D-based PC"
  width="600"
  height="254"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>I get the general impression that the AMD CPU has higher power consumption in
all regards: the baseline is higher, the spikes are higher (peak consumption)
and it spikes more often / for longer.</p>
<p>Looking at my energy meter statistics, I usually ended up at about 9.x kWh per
day for a two-person household, cooking with induction.</p>
<p>After switching my PC from Intel to AMD, I end up at 10-11 kWh per day.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I started buying Intel CPUs because they allowed me to build high-performance
computers that ran Linux flawlessly and produced little noise. This formula
worked for me over many years:</p>
<ul>
<li>Back in 2008, I <a href="/posts/2008-04-02-startschwierigkeiten_2008/">bought a mobile Intel CPU in a desktop case (article in
German)</a>.</li>
<li>Then, in 2012, I could just <a href="/posts/2012-06-24-buying_linux_computer_2012/">buy a regular Intel CPU (i7-2600K) for my Linux
PC</a>, because they had gotten so
much better in terms of power saving.</li>
<li>Over the years, I bought an i7-8700K, and later an i9-9900K.</li>
<li>The last time this formula worked out for me was <a href="/posts/2022-01-15-high-end-linux-pc/">with my 2022 high-end Linux
PC</a>.</li>
</ul>
<p>On the one hand, I’m a little sad that this era has ended. On the other hand, I
have had a soft spot for AMD since I had one of their K6 CPUs in one of my early
PCs and in fact, I have never stopped buying AMD CPUs (e.g. for my <a href="/posts/2024-07-02-ryzen-7-mini-pc-low-power-proxmox-hypervisor/">Ryzen
7-based Mini
Server</a>).</p>
<p>Maybe AMD could further improve their idle power usage in upcoming models? And,
if Intel survives for long enough, maybe they succeed at stabilizing their CPU
designs again? I certainly would love to see some competition in the CPU market.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Secret Management on NixOS with sops-nix]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-08-24-secret-management-with-sops-nix/"/>
    <id>https://michael.stapelberg.ch/posts/2025-08-24-secret-management-with-sops-nix/</id>
    <published>2025-08-24T09:56:00+02:00</published>
    <content type="html"><![CDATA[<p>Passwords and secrets like cryptographic key files are everywhere in
computing. When configuring a Linux system, sooner or later you will need to put
a password somewhere — for example, when I <a href="/posts/2025-07-13-nixos-nas-network-storage-config/">migrated my existing Linux Network
Storage (NAS) setup to
NixOS</a>, I needed to specify
the desired Samba passwords in my NixOS config (or manage them manually, outside
of NixOS). For personal computers, this is fine, but if the goal is to share
system configurations (for example in a Git repository), we need a different
solution: Secret Management.</p>
<h2 id="what-is-secret-management">What is Secret Management?</h2>
<p>The basic idea behind Secret Management systems is to <em>encrypt</em> the secrets at
rest, meaning if somebody clones the git repository containing your NixOS system
configurations, they cannot access (and therefore, also not deploy) the
encrypted secrets.</p>
<p>Conceptually, we need to:</p>
<ol>
<li>Encrypt the secrets such that the target system can decrypt them.</li>
<li>Encrypt the secrets such that other people working on this config can decrypt
them.</li>
<li>Have the target system decrypt secrets at runtime.</li>
<li>Tell our software where to access the decrypted secrets.</li>
</ol>
<h2 id="sops-nix-setup">sops-nix setup</h2>
<p>In this article, I will show how to accomplish the above using sops-nix. Here’s
a quick overview of the three different building blocks we will use:</p>
<ul>
<li><a href="https://getsops.io/">sops</a> is a tool to version-control secrets in git, in
their encrypted form.
<ul>
<li>sops makes it easy to re-encrypt these secrets when adding/removing authorized keys.</li>
<li>sops is very flexible and can work with tons of other tools/providers.</li>
</ul>
</li>
<li><a href="https://github.com/Mic92/sops-nix">sops-nix</a> provides a way to integrate sops
with Nix/NixOS</li>
<li>Using sops with <a href="https://manpages.debian.org/age.1"><code>age(1)</code></a>
 allows us to use our
existing SSH private key (humans) or SSH host private key (machines) instead
of managing a separate set of key files.</li>
</ul>
<p>You might wonder why I chose sops-nix over
<a href="https://github.com/ryantm/agenix">agenix</a>, the other contender? The
instructions for setting up sops-nix made more sense to me when I first looked
at it, and I wanted to have the option to use sops in other ways, not just with
age. If you’re curious about agenix, <a href="https://www.splitbrain.org/blog/2025-07/27-agenix">check out Andreas Gohr’s blog post about
agenix</a>.</p>
<h3 id="step-1-preparation">Step 1. Preparation</h3>
<p>I ran the following instructions on an <a href="/posts/2025-07-27-dev-shells-with-nix-4-quick-examples/#setup">Arch Linux machine on which I installed
the Nix tool and enabled Nix
Flakes</a>. Follow
the link for instructions also for other systems like Debian or Fedora.</p>
<h3 id="step-2-obtain-an-age-identity-from-your-personal-ssh-key">Step 2. Obtain an age identity from your personal SSH key</h3>
<p>I don’t want to manage an extra key file, so I’ll use <code>ssh-to-age</code> to derive a
key from my SSH private key file, which I already take good care of to back up:</p>
<pre tabindex="0"><code>midna % mkdir -p $HOME/.config/sops/age/
midna % read -s SSH_TO_AGE_PASSPHRASE; export SSH_TO_AGE_PASSPHRASE
midna % nix run nixpkgs#ssh-to-age -- \
  -private-key \
  -i $HOME/.ssh/id_ed25519 \
  -o $HOME/.config/sops/age/keys.txt
</code></pre><p>(The <code>SSH_TO_AGE_PASSPHRASE</code> option is documented in the <a href="https://github.com/Mic92/ssh-to-age/blob/main/README.md#usage">ssh-to-age
README</a>.)</p>
<p>To display the age recipient (public key) of this age identity (private key), I
used:</p>
<pre tabindex="0"><code>midna % nix shell nixpkgs#age
midna 2 % age-keygen -y $HOME/.config/sops/age/keys.txt
age10e9tt2qwq90y5hvl35dau0sm5cm4qvegtw2a70v7sz5fy99de42s9d5nkf
</code></pre><h3 id="step-3-obtain-an-age-recipient-for-the-remote-machine">Step 3. Obtain an age recipient for the remote machine</h3>
<p>Similarly, I will derive an age recipient from the SSH host key of the remote
system:</p>
<pre tabindex="0"><code>batchn % cat /etc/ssh/ssh_host_ed25519_key.pub | nix run nixpkgs#ssh-to-age
age1wnwfnrqhewjh39pmtyc8zhqw606znskt4h5p9s3pve4apd67gapqj6tr0k
</code></pre><h3 id="step-4-configure-sops-for-your-git-repository">Step 4. Configure sops for your git repository</h3>
<p>In my git repository (nix-configs), I have one subdirectory per NixOS system,
i.e. <a href="https://manpages.debian.org/tree.1"><code>tree(1)</code></a>
 shows:</p>
<pre tabindex="0"><code>├── batchn
│   ├── configuration.nix
│   ├── disk-config.nix
│   ├── flake.lock
│   ├── flake.nix
│   ├── hardware-configuration.nix
│   ├── Makefile
│   ├── secrets
│   │   └── example.yaml
├── wiki
│   ├── configuration.nix
│   ├── disk-config.nix
│   ├── flake.lock
│   ├── flake.nix
│   ├── hardware-configuration.nix
│   ├── Makefile
…
</code></pre><p>In the root of the git repository (next to the <code>batchn</code> directory), I create
<code>.sops.yaml</code> like so:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">keys</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span>- <span style="color:#007020">&amp;admin_michael</span><span style="color:#bbb"> </span>age10e9tt2qwq90y5hvl35dau0sm5cm4qvegtw2a70v7sz5fy99de42s9d5nkf<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span>- <span style="color:#007020">&amp;server_batchn</span><span style="color:#bbb"> </span>age1wnwfnrqhewjh39pmtyc8zhqw606znskt4h5p9s3pve4apd67gapqj6tr0k<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#60a0b0;font-style:italic"># …more server keys go here…</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#062873;font-weight:bold">creation_rules</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span>- <span style="color:#062873;font-weight:bold">path_regex</span>:<span style="color:#bbb"> </span>batchn/secrets/[^/]+\.(yaml|json|env|ini)$<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">key_groups</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span>- <span style="color:#062873;font-weight:bold">age</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">      </span>- <span style="color:#007020">*admin_michael</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">      </span>- <span style="color:#007020">*server_batchn</span><span style="color:#bbb">
</span></span></span></code></pre></div><p>The more systems I manage, the more <code>keys</code> and <code>creation_rules</code> I will need to
configure.</p>
<p>The creation rules tell sops which keys to use when encrypting a file. In my
setups, I typically use only a single file per system, but I could imagine
splitting out some secrets into a separate file if I wanted to collaborate with
someone on just one aspect of the system.</p>
<h3 id="step-5-manage-some-secrets-with-sops">Step 5. Manage some secrets with sops</h3>
<p>Now that we told sops which recipients to encrypt for, we can decrypt and edit
<code>secrets/example.yaml</code> in our configured editor by running:</p>
<pre tabindex="0"><code>midna ~/nix-configs/batchn % nix run nixpkgs#sops secrets/example.yaml
</code></pre><p>The simplest key file contains just one key, for example:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">api-key</span>:<span style="color:#bbb"> </span>hello world :)<span style="color:#bbb">
</span></span></span></code></pre></div><p>After saving and exiting your editor, sops will update the encrypted
secrets/example.yaml.</p>
<h3 id="step-6-configure-sops-in-nixos">Step 6. Configure sops in NixOS</h3>
<p>Now, we need to reference the encrypted file in NixOS and enable <code>sops-nix</code>
integration to make the decrypted secrets available on the system.</p>
<p>In <code>flake.nix</code>, I added <code>sops-nix</code> to the <code>inputs</code> section and added the NixOS
module. I show the entire diff because the places where the lines go are just as
important as what the lines say:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#a00000">--- c/batchn/flake.nix
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+++ i/batchn/flake.nix
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span><span style="color:#800080;font-weight:bold">@@ -1,85 +1,93 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span> {
</span></span><span style="display:flex;"><span>   inputs = {
</span></span><span style="display:flex;"><span>     nixpkgs.url = &#34;github:nixos/nixpkgs/nixos-25.05&#34;;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>     disko.url = &#34;github:nix-community/disko&#34;;
</span></span><span style="display:flex;"><span>     # Use the same version as nixpkgs
</span></span><span style="display:flex;"><span>     disko.inputs.nixpkgs.follows = &#34;nixpkgs&#34;;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>     stapelbergnix.url = &#34;github:stapelberg/nix&#34;;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>     zkjnastools.url = &#34;github:stapelberg/zkj-nas-tools&#34;;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#00a000">+    sops-nix = {
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+      url = &#34;github:Mic92/sops-nix&#34;;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+      inputs.nixpkgs.follows = &#34;nixpkgs&#34;;
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+    };
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>   };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>   outputs =
</span></span><span style="display:flex;"><span>     {
</span></span><span style="display:flex;"><span>       nixpkgs,
</span></span><span style="display:flex;"><span>       disko,
</span></span><span style="display:flex;"><span>       stapelbergnix,
</span></span><span style="display:flex;"><span>       zkjnastools,
</span></span><span style="display:flex;"><span><span style="color:#00a000">+      sops-nix,
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>       ...
</span></span><span style="display:flex;"><span>     }:
</span></span><span style="display:flex;"><span>     let
</span></span><span style="display:flex;"><span>       system = &#34;x86_64-linux&#34;;
</span></span><span style="display:flex;"><span>       pkgs = import nixpkgs {
</span></span><span style="display:flex;"><span>         inherit system;
</span></span><span style="display:flex;"><span>         config.allowUnfree = false;
</span></span><span style="display:flex;"><span>       };
</span></span><span style="display:flex;"><span>     in
</span></span><span style="display:flex;"><span>     {
</span></span><span style="display:flex;"><span>       nixosConfigurations.batchn = nixpkgs.lib.nixosSystem {
</span></span><span style="display:flex;"><span>         inherit system;
</span></span><span style="display:flex;"><span>         inherit pkgs;
</span></span><span style="display:flex;"><span>         modules = [
</span></span><span style="display:flex;"><span>           disko.nixosModules.disko
</span></span><span style="display:flex;"><span>           ./configuration.nix
</span></span><span style="display:flex;"><span>           stapelbergnix.lib.userSettings
</span></span><span style="display:flex;"><span>           # Use systemd for network configuration
</span></span><span style="display:flex;"><span>           stapelbergnix.lib.systemdNetwork
</span></span><span style="display:flex;"><span>           # Use systemd-boot as bootloader
</span></span><span style="display:flex;"><span>           stapelbergnix.lib.systemdBoot
</span></span><span style="display:flex;"><span>           # Run prometheus node exporter in tailnet
</span></span><span style="display:flex;"><span>           stapelbergnix.lib.prometheusNode
</span></span><span style="display:flex;"><span>           zkjnastools.nixosModules.zkjbackup
</span></span><span style="display:flex;"><span><span style="color:#00a000">+          sops-nix.nixosModules.sops
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>         ];
</span></span><span style="display:flex;"><span>       };
</span></span><span style="display:flex;"><span>       formatter.${system} = pkgs.nixfmt-tree;
</span></span><span style="display:flex;"><span>     };
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>Then, in <code>configuration.nix</code>, we tell <code>sops-nix</code> to use the SSH host key as
identity, where sops will find our secrets and which secrets <code>sops-nix</code> should
realize on the remote system:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  sops<span style="color:#666">.</span>age<span style="color:#666">.</span>sshKeyPaths <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;/etc/ssh/ssh_host_ed25519_key&#34;</span> ];
</span></span><span style="display:flex;"><span>  sops<span style="color:#666">.</span>defaultSopsFile <span style="color:#666">=</span> <span style="color:#235388">./secrets/example.yaml</span>;
</span></span><span style="display:flex;"><span>  sops<span style="color:#666">.</span>secrets<span style="color:#666">.</span><span style="color:#4070a0">&#34;api-key&#34;</span> <span style="color:#666">=</span> { };
</span></span></code></pre></div><p>After deploying, we can access the secret on the running system:</p>
<pre tabindex="0"><code>batchn ~ % sudo cat /run/secrets/api-key
hello world :)%
batchn ~ %
</code></pre><p>Of course, even after rebooting the machine, the secrets remain available without a re-deploy:</p>
<pre tabindex="0"><code>batchn ~ % uptime
 22:09:23  up   0:00,  1 user,  load average: 0,32, 0,08, 0,03
batchn ~ % sudo cat /run/secrets/api-key
hello world :)%
batchn ~ %
</code></pre><h2 id="usage-examples">Usage Examples</h2>
<p>Now that we have secrets stored in files under <code>/run/secrets</code>, how can we use
these secrets?</p>
<p>The following sections show a few common ways.</p>
<h3 id="usage-example-command-line-flags-execstart-wrapper">Usage Example: command-line flags (ExecStart wrapper)</h3>
<p>Let’s assume you have deployed a custom Go server as a systemd service on NixOS
as follows, and you want to start managing the cleartext secret passed via the
<code>-securecookie_hash_key</code> and <code>-securecookie_block_key</code> command-line flags:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>groups<span style="color:#666">.</span>fortuneserver <span style="color:#666">=</span> { };
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>fortuneserver <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    isSystemUser <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    group <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>fortuneserver <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex;"><span>    documentation <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;https://michael.stapelberg.ch&#34;</span> ];
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;multi-user.target&#34;</span> ];
</span></span><span style="display:flex;"><span>    serviceConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      User <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex;"><span>      Group <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      ExecStart <span style="color:#666">=</span> <span style="color:#4070a0">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>fortuneserver<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/fortuneserver&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          -securecookie_hash_key=&#34;some-secret-key&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          -securecookie_block_key=&#34;a-different-secret-key&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">      &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>With the following sops secrets:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">fortuneserver</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">securecookie_hash_key</span>:<span style="color:#bbb"> </span>some-secret-key<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">securecookie_block_key</span>:<span style="color:#bbb"> </span>a-different-secret-key<span style="color:#bbb">
</span></span></span></code></pre></div><p>…we need to adjust our NixOS config to read these secret files at
runtime. Because the <code>ExecStart</code> directive is interpreted by systemd and not
passed through a shell, we use the <a href="https://nixos.org/manual/nixpkgs/stable/#trivial-builder-writeShellScript"><code>writeShellScript</code>
helper</a>
and then just <code>cat</code> the files:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  sops<span style="color:#666">.</span>secrets<span style="color:#666">.</span><span style="color:#4070a0">&#34;fortuneserver/securecookie_hash_key&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    owner <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    restartUnits <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;fortuneserver.service&#34;</span> ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  sops<span style="color:#666">.</span>secrets<span style="color:#666">.</span><span style="color:#4070a0">&#34;fortuneserver/securecookie_block_key&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    owner <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    restartUnits <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;fortuneserver.service&#34;</span> ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>groups<span style="color:#666">.</span>fortuneserver <span style="color:#666">=</span> { };
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>fortuneserver <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    isSystemUser <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    group <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>fortuneserver <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex;"><span>    documentation <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;https://michael.stapelberg.ch&#34;</span> ];
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;multi-user.target&#34;</span> ];
</span></span><span style="display:flex;"><span>    serviceConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      User <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex;"><span>      Group <span style="color:#666">=</span> <span style="color:#4070a0">&#34;fortuneserver&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      ExecStart <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>writeShellScript <span style="color:#4070a0">&#34;fortuneserver-execstart&#34;</span> <span style="color:#4070a0">&#39;&#39;
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#4070a0">        &#34;</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>fortuneserver<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/fortuneserver&#34; \
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#4070a0">          -securecookie_hash_key=&#34;$(cat /run/secrets/fortuneserver/securecookie_hash_key)&#34; \
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#4070a0">          -securecookie_block_key=&#34;$(cat /run/secrets/fortuneserver/securecookie_block_key)&#34;
</span></span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#4070a0">      &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h3 id="usage-example-environment-variable-files">Usage Example: environment variable files</h3>
<p>What if the service in question does not use command-line flags, but environment
variables for configuring secrets? We can put an environment variable file into
a sops-managed secret:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">translate-fe</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">env</span>:<span style="color:#bbb"> </span>|<span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        DEEPL_AUTH_KEY=my-deepl-key</span><span style="color:#bbb">
</span></span></span></code></pre></div><p>…and then we make systemd apply these environment variables from the secrets file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  sops<span style="color:#666">.</span>secrets<span style="color:#666">.</span><span style="color:#4070a0">&#34;translate-fe/env&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    owner <span style="color:#666">=</span> <span style="color:#4070a0">&#34;translatefe&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    restartUnits <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;translate-fe.service&#34;</span> ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>translate-fe <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    documentation <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;https://michael.stapelberg.ch&#34;</span> ];
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;multi-user.target&#34;</span> ];
</span></span><span style="display:flex;"><span>    serviceConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      User <span style="color:#666">=</span> <span style="color:#4070a0">&#34;translatefe&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      EnvironmentFile <span style="color:#666">=</span> [ config<span style="color:#666">.</span>sops<span style="color:#666">.</span>secrets<span style="color:#666">.</span><span style="color:#4070a0">&#34;translate-fe/env&#34;</span><span style="color:#666">.</span>path ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      ExecStart <span style="color:#666">=</span> <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>translatefeExecstart<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/translate-fe&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>If you are configuring a NixOS module (instead of declaring a custom service),
the option might not always be called <code>EnvironmentFile</code>. For example, for the
oauth2-proxy service, you would need to configure the
<a href="https://search.nixos.org/options?channel=25.05&amp;show=services.oauth2-proxy.keyFile&amp;from=0&amp;size=50&amp;sort=relevance&amp;type=packages&amp;query=oauth2-proxy"><code>services.oauth2-proxy.keyFile</code>
option</a>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  services<span style="color:#666">.</span>oauth2-proxy <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    keyFile <span style="color:#666">=</span> config<span style="color:#666">.</span>sops<span style="color:#666">.</span>secrets<span style="color:#666">.</span><span style="color:#4070a0">&#34;oauth2-proxy/env&#34;</span><span style="color:#666">.</span>path;
</span></span><span style="display:flex;"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># …</span>
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><h3 id="usage-example-systemd-credentials">Usage Example: systemd credentials</h3>
<p>In the previous examples, we configured the <code>owner</code> of each secret to the user
account under which the service is running. But what if there is no such user
account, because the service use systemd’s <code>DynamicUser</code> feature?</p>
<p>We can use systemd’s <code>LoadCredential</code> feature! For example, I supply the SMTP
password to my Prometheus Alertmanager as follows:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  sops<span style="color:#666">.</span>secrets<span style="color:#666">.</span><span style="color:#4070a0">&#34;alertmanager/smtp_pw&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    restartUnits <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;alertmanager.service&#34;</span> ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>alertmanager<span style="color:#666">.</span>serviceConfig<span style="color:#666">.</span>LoadCredential <span style="color:#666">=</span> [
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#34;smtp_pw:</span><span style="color:#70a0d0">${</span>config<span style="color:#666">.</span>sops<span style="color:#666">.</span>secrets<span style="color:#666">.</span><span style="color:#4070a0">&#34;alertmanager/smtp_pw&#34;</span><span style="color:#666">.</span>path<span style="color:#70a0d0">}</span><span style="color:#4070a0">&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  services<span style="color:#666">.</span>prometheus<span style="color:#666">.</span>alertmanager <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    configuration <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      global <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>        smtp_smarthost <span style="color:#666">=</span> <span style="color:#4070a0">&#34;smtp.gmail.com:587&#34;</span>;
</span></span><span style="display:flex;"><span>        smtp_from <span style="color:#666">=</span> <span style="color:#4070a0">&#34;alerts@example.net&#34;</span>;
</span></span><span style="display:flex;"><span>        smtp_auth_username <span style="color:#666">=</span> <span style="color:#4070a0">&#34;alerts@example.net&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>        smtp_auth_password_file <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/run/credentials/alertmanager.service/smtp_pw&#34;</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#60a0b0;font-style:italic"># …remaining config goes here…</span>
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h3 id="usage-example-samba-userspasswords">Usage Example: samba users/passwords</h3>
<p>In my blog post <a href="/posts/2025-07-13-nixos-nas-network-storage-config/#samba-nixos">“Migrating my NAS from CoreOS/Flatcar Linux to
NixOS”</a>, I
describe how to configure samba users and passwords (from sops-managed secrets)
with an <code>ExecStartPre</code> shell script (which is very similar to the techniques
already explained).</p>
<h2 id="conclusion">Conclusion</h2>
<p>Managing secrets as separately-encrypted files in your config repository makes
sense to me!</p>
<p>age’s ability to work with SSH keys makes for a really convenient setup, in my
opinion. Encrypting secrets for the destination system’s SSH host key feels very
elegant.</p>
<p>I hope the examples above are sufficient for you to efficiently configure
secrets in NixOS!</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Development shells with Nix: four quick examples]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-07-27-dev-shells-with-nix-4-quick-examples/"/>
    <id>https://michael.stapelberg.ch/posts/2025-07-27-dev-shells-with-nix-4-quick-examples/</id>
    <published>2025-07-27T08:50:00+02:00</published>
    <content type="html"><![CDATA[<p>I wanted to use <a href="https://gocv.io/">GoCV</a> for one of my projects (to find and
extract paper documents from within a larger scan), without permanently having
OpenCV on my system.</p>
<p>This seemed like a good example use-case to demonstrate a couple of Nix commands
I like to use, covering quick interactive one-off dev shells to fully
declarative, hermetic, reproducible, shareable dev shells.</p>
<p>Notably, you don’t need to use NixOS to run these commands! You can <a href="/posts/2025-06-01-nixos-installation-declarative/#setup-nix">install and
use Nix</a> on any
Linux system like Debian, Arch, etc., as long as you set a Nix path or use
Flakes (see <a href="#setup">setup</a>).</p>
<h2 id="debian-way">For comparison: The Debian Way</h2>
<p>Before we start looking at Nix, I will show how to get GoCV running on Debian.</p>
<p>Let’s create a minimal Go program which uses a GoCV function like
<code>gocv.NewMat()</code>, just to verify that we can compile this program:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">package</span><span style="color:#bbb"> </span>main<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">import</span><span style="color:#bbb"> </span><span style="color:#4070a0">&#34;gocv.io/x/gocv&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">main</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span>gocv.<span style="color:#06287e">NewMat</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>If we try to build this on a Debian system, we get:</p>
<pre tabindex="0"><code>debian % mkdir -p /tmp/minimal
debian % cd /tmp/minimal

debian % cat &gt; minimal.go &lt;&lt;&#39;EOT&#39;
package main
import &#34;gocv.io/x/gocv&#34;
func main() { gocv.NewMat(); }
EOT

debian % go mod init minimal
go: creating new go.mod: module minimal
go: to add module requirements and sums:
	go mod tidy

debian % go mod tidy
go: finding module for package gocv.io/x/gocv
go: downloading gocv.io/x/gocv v0.41.0
go: found gocv.io/x/gocv in gocv.io/x/gocv v0.41.0

debian % go build
# gocv.io/x/gocv
# [pkg-config --cflags  -- opencv4]
Package opencv4 was not found in the pkg-config search path.
Perhaps you should add the directory containing `opencv4.pc&#39;
to the PKG_CONFIG_PATH environment variable
Package &#39;opencv4&#39;, required by &#39;virtual:world&#39;, not found
</code></pre><p>On Debian, we can install OpenCV as follows:</p>
<pre tabindex="0"><code>debian % sudo apt install libopencv-dev

[…]

Summary:
  Upgrading: 7, Installing: 512, Removing: 0, Not Upgrading: 27
  Download size: 367 MB
  Space needed: 1590 MB / 281 GB available

Continue? [Y/n]
</code></pre><p>Saying “yes” to this prompt downloads and installs over 500 packages (takes a
few minutes).</p>
<p>Now the build works:</p>
<pre tabindex="0"><code>debian % go build
debian % file minimal
minimal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), […]
</code></pre><p>…but we have over 500 extra packages on our system that will now need to be
updated in all eternity, therefore I would like to separate this one-off
experiment from my usual system.</p>
<p>We could use Docker to start a Debian container and work inside that container,
but, depending on the task, this can be cumbersome precisely because it’s a
separate environment. For this example, I would need to specify a volume mount
to make my input files available to the Docker container, and I would need to
set up environment variables before programs inside the Docker container can
open graphical windows on the host…</p>
<p>Let’s look at how we can use Nix to help us with that!</p>
<h2 id="setup">Setup: Nix-on-Debian (or Nix-on-Arch, or…)</h2>
<p>Users of NixOS can skip this section, as NixOS systems include a ready-to-use
Nix.</p>
<p>Before you can try the examples on your own computer, you need to complete these
three steps:</p>
<ol>
<li>Install Nix</li>
<li>Enable Flakes</li>
<li>Set a Nix path</li>
</ol>
<h3 id="setup-install">Step 1: Install Nix</h3>
<p>Users of Debian, Arch, Fedora, or other Linux systems first need to install
Nix. Luckily, Nix is available for many popular Linux distributions:</p>
<ul>
<li>Debian ships <a href="https://packages.debian.org/trixie/nix-setup-systemd">nix-setup-systemd</a></li>
<li>Arch Linux packages <a href="https://archlinux.org/packages/extra/x86_64/nix/">nix</a>
and provides documentation <a href="https://wiki.archlinux.org/title/Nix">on the Nix Arch Wiki
page</a>. In practice, I installed the
package and <a href="/posts/2025-06-01-nixos-installation-declarative/#setup-nix">configured a couple of <code>nixbld</code>
users</a>.</li>
<li>More generally, there are Nix builds (rpm, deb, pacman) available for a number
of distributions: <a href="https://github.com/nix-community/nix-installers">https://github.com/nix-community/nix-installers</a></li>
</ul>
<h3 id="setup-flakes">Step 2: Enable Flakes</h3>
<p>Nix flakes are <a href="https://determinate.systems/posts/flake-schemas/">“a generic way to package Nix
artifacts”</a>.</p>
<p>Examples 3 and 4 use Nix flakes to pin dependencies, so we need to <a href="/posts/2025-06-01-nixos-installation-declarative/#enabling-flakes">enable Nix
flakes</a>.</p>
<h3 id="setup-nix-path">Step 3: Set a Nix path</h3>
<p>For example 1 and 2, we want to use the Nix expression <code>import &lt;nixpkgs&gt;</code>.</p>
<p>On NixOS, this expression will follow the system version, meaning if you use
<code>import &lt;nixpkgs&gt;</code> on a NixOS 25.05 installation, that will reference <a href="https://github.com/NixOS/nixpkgs/tree/nixos-25.05/">nixpkgs
in version nixos-25.05</a>.</p>
<p>On other Linux systems, you’ll see an error message like this:</p>
<pre tabindex="0"><code>debian-server % nix-shell -p pkg-config opencv
error: file &#39;nixpkgs&#39; was not found in the Nix search path (add it using $NIX_PATH or -I)

       at «string»:1:25:

            1| {...}@args: with import &lt;nixpkgs&gt; args; (pkgs.runCommandCC or pkgs.runCommand) &#34;shell&#34; { buildInputs = [ (pkg-config) (opencv) ]; } &#34;&#34;
             |                         ^
(use &#39;--show-trace&#39; to show detailed location information)
</code></pre><p>We need to tell Nix which version of <code>nixpkgs</code> to use by setting the <a href="https://nixos.org/guides/nix-pills/15-nix-search-paths.html">Nix search
path</a>:</p>
<pre tabindex="0"><code>debian-server % export NIX_PATH=nixpkgs=channel:nixos-25.05
debian-server % nix-shell -p pkg-config opencv
[nix-shell:/tmp/opencv]#
</code></pre><p>Alright! Now we are set up. Let’s jump into the first example!</p>
<h2 id="nix-shell">Example 1: Interactive one-offs: nix-shell</h2>
<p>Nix provides a middle-ground between installing OpenCV on your system (<code>apt install</code> like in the example above) and installing OpenCV in a separate Docker
container: Nix can make available OpenCV without permanently installing it.</p>
<p>We can run <a href="https://manpages.debian.org/nix-shell.1"><code>nix-shell(1)</code></a>
 to start a bash shell in
which the specified packages are available. To successfully build Go code that
uses GoCV, we need to have OpenCV available:</p>
<pre tabindex="0"><code>% nix-shell -p pkg-config opencv
these 194 paths will be fetched (175.80 MiB download, 764.10 MiB unpacked):
  /nix/store/ig2nk0hsha9xaailhaj69yv677nv95q4-abseil-cpp-20210324.2
  /nix/store/yw5xqn8lqinrifm9ij80nrmf0i6fdcbx-alsa-lib-1.2.13
[…]

[nix-shell:/tmp/opencv]$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4
</code></pre><p>In case you were wondering: Yes, we do need to specify <code>pkg-config</code> in this
<code>nix-shell</code> command explicitly, otherwise running <code>pkg-config</code> will run the host
version (outside the dev shell), which cannot find <code>opencv4.pc</code>.</p>
<h2 id="shell.nix">Example 2: nix-shell config file: shell.nix</h2>
<p>Once we have a combination of packages that work for our project (in our
example, just <code>pkg-config</code> and <code>opencv</code>), we can create a <code>shell.nix</code> (in any
directory, but usually in the root of a project) which <code>nix-shell</code> (without the
<code>-p</code> flag) will read:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  pkgs <span style="color:#666">?</span> <span style="color:#007020;font-weight:bold">import</span> <span style="color:#235388">&lt;nixpkgs&gt;</span> { }<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>}:
</span></span><span style="display:flex;"><span>pkgs<span style="color:#666">.</span>mkShell {
</span></span><span style="display:flex;"><span>  packages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># Explicitly list pkg-config so that mkShell will arrange</span>
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># for the PKG_CONFIG_PATH to find the .pc files.</span>
</span></span><span style="display:flex;"><span>    pkg-config
</span></span><span style="display:flex;"><span>    opencv
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>…and then, we just run <code>nix-shell</code>:</p>
<pre tabindex="0"><code>% nix-shell
[nix-shell:/tmp/opencv]$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4
</code></pre><p>If you’re curious, here are a couple of documentation pointers regarding the
boilerplate around the list of packages:</p>
<ul>
<li>Line 1 to 3 <a href="https://nixos.org/guides/nix-pills/05-functions-and-imports.html">declare a
function</a>
with an argument set — this is the required structure for <code>nix-shell</code> to be
able to call your <code>shell.nix</code> file.</li>
<li><a href="https://nixos.org/manual/nixpkgs/stable/#sec-pkgs-mkShell"><code>pkgs.mkShell</code></a> is
a convenience helper for use with <code>nix-shell</code>.</li>
<li>The <code>with pkgs;</code> part allows us to write <code>opencv</code> instead of <code>pkgs.opencv</code>.</li>
</ul>
<p>By the way: With the <a href="https://github.com/nix-community/nixd">nixd language
server</a>, editors with <a href="https://en.wikipedia.org/wiki/Language_Server_Protocol">LSP
support</a> can show the
versions that packages resolve to, point out your spelling mistakes, or provide
features like “jump to definition”.</p>
<p>For example, in this screenshot, I was editing <code>shell.nix</code> in Emacs and was
curious how the Nix source of the <code>opencv</code> package looked like. By pressing
<code>M-.</code> (<code>xref-find-definitions</code>) with
<a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Point.html">“point”</a>
over <code>opencv</code>, I got to <code>opencv/4.x.nix</code> in my local Nix store:</p>















<a href="https://michael.stapelberg.ch/posts/2025-07-27-dev-shells-with-nix-4-quick-examples/2025-07-19-emacs-nix-shell.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-07-27-dev-shells-with-nix-4-quick-examples/2025-07-19-emacs-nix-shell_hu_6ca6f897c44d3994.jpg 2x,https://michael.stapelberg.ch/posts/2025-07-27-dev-shells-with-nix-4-quick-examples/2025-07-19-emacs-nix-shell_hu_ebe840dd6e5fb84c.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-07-27-dev-shells-with-nix-4-quick-examples/2025-07-19-emacs-nix-shell_hu_9fd651da07f25ae.jpg"
  alt="Emacs showing opencv/4.x.nix after jumping to definition of opencv" title="Emacs showing opencv/4.x.nix after jumping to definition of opencv"
  width="600"
  height="374"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h2 id="nix-flakes">Example 3: Hermetic, pinned devShells: Nix Flakes</h2>
<p>The previous examples used nixpkgs from your system (or Nix path), which means
you don’t need to change the <code>.nix</code> file when you upgrade your system —
depending on the use-case, I see this behavior as either convenient or
terrifying.</p>
<p>For use-cases where it is important that the <code>.nix</code> file is built exactly the
same way, no matter what version the surrounding OS uses, we can use <a href="https://wiki.nixos.org/wiki/Flakes">Nix
Flakes</a> to build in a hermetic way, with
dependency versions pinned in the <code>flake.lock</code> file.</p>
<p>A <code>flake.nix</code> contains the same <code>mkShell</code> expression as above, but declares
structure around it: The <code>mkShell</code> expression goes into the
<code>outputs.devShells.x86_64-linux.default</code> attribute and the <code>inputs</code> attribute
contains <a href="https://nix.dev/manual/nix/2.28/command-ref/new-cli/nix3-flake.html#flake-references">Flake
references</a>
that are available to this build:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:NixOS/nixpkgs/nixos-25.05&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  outputs <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>    { self<span style="color:#666">,</span> nixpkgs }:
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      devShells<span style="color:#666">.</span>x86_64-linux<span style="color:#666">.</span>default <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>          pkgs <span style="color:#666">=</span> nixpkgs<span style="color:#666">.</span>legacyPackages<span style="color:#666">.</span>x86_64-linux;
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span>        pkgs<span style="color:#666">.</span>mkShell {
</span></span><span style="display:flex;"><span>          packages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [
</span></span><span style="display:flex;"><span>            <span style="color:#60a0b0;font-style:italic"># Explicitly list pkg-config so that mkShell will arrange</span>
</span></span><span style="display:flex;"><span>            <span style="color:#60a0b0;font-style:italic"># for the PKG_CONFIG_PATH to find the .pc files.</span>
</span></span><span style="display:flex;"><span>            pkg-config
</span></span><span style="display:flex;"><span>            opencv
</span></span><span style="display:flex;"><span>          ];
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>By the way: Despite the name, it is a best practice to use
<code>nixpkgs.legacyPackages</code>, which conceptually provides a single <code>import nixpkgs</code>
result (<a href="https://discourse.nixos.org/t/using-nixpkgs-legacypackages-system-vs-import/17462/8">for
efficiency</a>).</p>
<p>Now, I can use <code>nix develop</code> to get a shell with OpenCV:</p>
<pre tabindex="0"><code>% nix develop
michael@midna$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4
</code></pre><p>The first <code>nix develop</code> run creates a <code>flake.lock</code> file, so running <code>nix develop</code> later will get us exactly the same environment. To update to newer
versions, use <code>nix flake update</code>.</p>
<p><strong>Tip:</strong> Instead of a shell, <code>nix develop --command=emacs</code> is also a useful variant.</p>
<h2 id="system-indep-flake">Example 4: Making the Flake system-independent</h2>
<p>Unfortunately, the above <code>flake.nix</code> hard-codes <code>x86_64-linux</code>, so it will not
be usable on, say, an <code>aarch64-linux</code> (ARM) computer, or on a <code>x86_64-darwin</code>
(Mac).</p>
<p>Having to explicitly specify the <code>system</code> by default is a long-standing
criticism of Nix Flakes.</p>
<p>There are a number of workarounds. For example, we can use
<a href="https://github.com/numtide/flake-utils">numtide/flake-utils</a> and refactor our
<code>flake.nix</code> to use its
<a href="https://github.com/numtide/flake-utils?tab=readme-ov-file#eachdefaultsystem--system---attrs"><code>eachDefaultSystem</code></a>
convenience function:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  inputs <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    nixpkgs<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-25.05&#34;</span>;
</span></span><span style="display:flex;"><span>    flake-utils<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:numtide/flake-utils&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  outputs <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      self<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      nixpkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      flake-utils<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>    }:
</span></span><span style="display:flex;"><span>    flake-utils<span style="color:#666">.</span>lib<span style="color:#666">.</span>eachDefaultSystem (
</span></span><span style="display:flex;"><span>      system:
</span></span><span style="display:flex;"><span>      <span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>        pkgs <span style="color:#666">=</span> nixpkgs<span style="color:#666">.</span>legacyPackages<span style="color:#666">.</span><span style="color:#70a0d0">${</span>system<span style="color:#70a0d0">}</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        formatter <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>nixfmt-tree;
</span></span><span style="display:flex;"><span>        devShells<span style="color:#666">.</span>default <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>mkShell {
</span></span><span style="display:flex;"><span>          packages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [
</span></span><span style="display:flex;"><span>            <span style="color:#60a0b0;font-style:italic"># Explicitly list pkg-config so that mkShell will arrange</span>
</span></span><span style="display:flex;"><span>            <span style="color:#60a0b0;font-style:italic"># for the PKG_CONFIG_PATH to find the .pc files.</span>
</span></span><span style="display:flex;"><span>            pkg-config
</span></span><span style="display:flex;"><span>            opencv
</span></span><span style="display:flex;"><span>          ];
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Or we could use <a href="https://github.com/numtide/blueprint">numtide/blueprint</a>,
its spiritual successor.</p>
<p>LucPerkins’s dev-templates <a href="https://github.com/the-nix-way/dev-templates/blob/main/go/flake.nix">have effectively
inlined</a> a
version of this technique.</p>
<p>For a solution that isn’t part of Nix, but Nix-adjacent:
<a href="https://devenv.sh/">devenv</a> is a separate tool that is built on Nix (no longer
using the CppNix implementation, but <a href="https://devenv.sh/blog/2024/10/22/devenv-is-switching-its-nix-implementation-to-tvix/">tvix
actually</a>),
but with its own .nix files.</p>
<h2 id="profile-install">Tip: Keeping packages around</h2>
<p>If you notice that <code>nix develop</code> or similar commands fetch packages despite the
<code>flake.lock</code> not having changed, you can install the Flake into your profile to
<a href="https://nixos.org/guides/nix-pills/11-garbage-collector.html">declare it as a gcroot to
Nix</a>:</p>
<pre tabindex="0"><code>% nix profile install .#devShells.x86_64-linux.default
</code></pre><p>But wait, isn’t that getting us into the same state as <a href="#debian-way">with The Debian
Way</a>? No! While OpenCV will remain available indefinitely if you
install the flake into your profile, there still is a layer of separation:
Within your system, OpenCV isn’t available, only when you start a development
shell with <code>nix-shell</code> or <code>nix develop</code>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>How do the four examples above compare? Here’s an overview:</p>
<table>
  <thead>
      <tr>
          <th>Example</th>
          <th>Boilerplate</th>
          <th>Pinned?</th>
          <th>System-dependent?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="#nix-shell">Ex 1</a>: <code>nix-shell -p …</code></td>
          <td>😊</td>
          <td>no</td>
          <td>no</td>
      </tr>
      <tr>
          <td><a href="#shell.nix">Ex 2</a>: <code>shell.nix</code></td>
          <td>🙂</td>
          <td>no</td>
          <td>no</td>
      </tr>
      <tr>
          <td><a href="#nix-flakes">Ex 3</a>: <code>flake.nix</code></td>
          <td>😲</td>
          <td>yes</td>
          <td>yes</td>
      </tr>
      <tr>
          <td><a href="#system-indep-flake">Ex 4</a>: system-independent <code>flake.nix</code></td>
          <td>🤨</td>
          <td>yes</td>
          <td>no</td>
      </tr>
  </tbody>
</table>
<p>For personal one-off experiments, I use <code>nix-shell</code>.</p>
<p>Once the experiment works, I typically want to pin the dependencies, so I use a
<code>flake.nix</code>.</p>
<p>If this is software that isn’t just versioned, but also published (or worked on
with multiple people/systems), I go through the effort of making it a
system-independent <code>flake.nix</code>.</p>
<p>I hope in the future, it will become easier to write a system-independent flake.</p>
<p>Despite the rough edges, I appreciate the reproducibility and control that Nix
gives me!</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Migrating my NAS from CoreOS/Flatcar Linux to NixOS]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-07-13-nixos-nas-network-storage-config/"/>
    <id>https://michael.stapelberg.ch/posts/2025-07-13-nixos-nas-network-storage-config/</id>
    <published>2025-07-13T08:17:00+02:00</published>
    <content type="html"><![CDATA[<p>In this article, I want to show how to migrate an existing Linux server to NixOS
— in my case the CoreOS/Flatcar Linux installation on my Network Attached
Storage (NAS) PC.</p>
<p>I will show in detail how the previous CoreOS setup looked like (lots of systemd
units starting Docker containers), how I migrated it into an intermediate state
(using Docker on NixOS) just to get things going, and finally how I migrated all
units from Docker to native NixOS modules step-by-step.</p>
<p>If you haven’t heard of NixOS, I recommend you read the <a href="https://nixos.org">first page of the NixOS
website</a> to understand what NixOS is and what sort of things
it makes possible.</p>
<p>The target audience of this blog post is people interested in trying out NixOS
for the use-case of a NAS, who like seeing examples to understand how to
configure a system.</p>
<p>You can apply these examples by first following <a href="/posts/2025-06-01-nixos-installation-declarative/">my blog post “How I like to
install NixOS
(declaratively)”</a>, then
making your way through the sections that interest you. If you prefer seeing the
full configuration, <a href="#conclusion">skip to the conclusion</a>.</p>















<a href="https://michael.stapelberg.ch/posts/2025-07-13-nixos-nas-network-storage-config/IMG_5563.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-07-13-nixos-nas-network-storage-config/IMG_5563_hu_90f92def47078a68.jpg 2x,https://michael.stapelberg.ch/posts/2025-07-13-nixos-nas-network-storage-config/IMG_5563_hu_fbda205f2b2f2fa9.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-07-13-nixos-nas-network-storage-config/IMG_5563_hu_dfffd317a819e211.jpg"
  alt="PC NAS build from 2023" title="PC NAS build from 2023"
  width="600"
  height="479"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h2 id="history">Context/History</h2>
<p>Over the last decade, I used a number of different operating systems for my
NAS needs. Here’s an overview of the 2 NAS systems storage2 and storage3:</p>
<table>
  <thead>
      <tr>
          <th>Year</th>
          <th>storage2</th>
          <th>storage3</th>
          <th>Details (blog post)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2013</td>
          <td>Debian on qnap</td>
          <td>Debian on qnap</td>
          <td><a href="/posts/2014-01-28-qnap_ts119_wol/">Wake-On-LAN with Debian on a qnap TS-119P2+</a></td>
      </tr>
      <tr>
          <td>2016</td>
          <td>CoreOS on PC</td>
          <td>CoreOS on PC</td>
          <td><a href="/posts/2016-11-21-gigabit-nas-coreos/">Gigabit NAS (running CoreOS)</a></td>
      </tr>
      <tr>
          <td>2023</td>
          <td>CoreOS on PC</td>
          <td>Ubuntu+ZFS on PC</td>
          <td><a href="/posts/2023-10-25-my-all-flash-zfs-network-storage-build/">My all-flash ZFS NAS build</a></td>
      </tr>
      <tr>
          <td>2025</td>
          <td>NixOS on PC</td>
          <td>Ubuntu+ZFS on PC</td>
          <td>→ you are here ←</td>
      </tr>
      <tr>
          <td>?</td>
          <td>NixOS on PC</td>
          <td>NixOS+ZFS on PC</td>
          <td>Converting more PCs to NixOS seems inevitable ;)</td>
      </tr>
  </tbody>
</table>
<h2 id="software-requirements">My NAS Software Requirements</h2>
<ul>
<li>(This post is only about software! For my usage patterns and requirements
regarding hardware selection, see <a href="/posts/2023-10-25-my-all-flash-zfs-network-storage-build/#design-goals">“Design Goals” in my My all-flash ZFS NAS
build post
(2023)</a>.)</li>
<li><strong>Remote management:</strong> I really like the model of having the configuration of
my network storage builds version-controlled and managed on my main PC. It’s a
nice property that I can regain access to my backup setup by re-installing my
NAS from my PC within minutes.</li>
<li><strong>Automated updates, with easy rollback:</strong> Updating all my installations
manually is not my idea of a good time. Hence, automated updates are a must —
but when the update breaks, a quick and easy path to recovery is also a
must.
<ul>
<li>CoreOS/Flatcar achieved that with an A/B updating scheme (update failed?
boot the old partition), whereas NixOS achieves that with its concept of a
“generation” (update failed? select the old generation), which is
finer-grained.</li>
</ul>
</li>
</ul>
<h2 id="why-migrate">Why migrate from CoreOS/Flatcar to NixOS?</h2>
<p>When I started using CoreOS, Docker was pretty new technology. I liked that
using Docker containers allowed you to treat services uniformly — ultimately,
they all expose a port of some sort (speaking HTTP, or Postgres, or…), so you
got the flexibility to run much more recent versions of software on a stable OS,
or older versions in case an update broke something.</p>
<p>Over a decade later, Docker is established tech. People nowadays take for
granted the various benefits of the container approach.</p>
<p>So, here’s my list of reasons why I wasn’t satisfied with Flatcar Linux anymore.</p>
<h4 id="cloud-init">R1. cloud-init is deprecated</h4>
<p>The <a href="https://github.com/coreos/coreos-cloudinit">CoreOS cloud-init</a> project was
deprecated at some point in favor of
<a href="https://github.com/coreos/ignition">Ignition</a>, which is clearly more powerful,
but also more cumbersome to get started with as a hobbyist. As far as I can
tell, I must host my config at some URL that I then provide via a kernel
parameter. The old way of just copying a file seems to no longer be supported.</p>
<p>Ignition also seems less convenient in other ways: YAML is no longer supported,
only JSON, which I don’t enjoy writing by hand. Also, the format seems to
<a href="https://coreos.github.io/ignition/migrating-configs/">change quite a bit</a>.</p>
<p>As a result, I never made the jump from cloud-init to Ignition, and it’s not
good to be reliant on a long-deprecated way to use your OS of choice.</p>
<h4 id="container-bitrot">R2. Container Bitrot</h4>
<p>At some point, I did an audit of all my containers on the Docker Hub and noticed
that most of them were quite outdated. For a while, Docker Hub offered automated
builds based on a <code>Dockerfile</code> obtained from GitHub. However, automated builds
now require a subscription, and I will not accept a subscription just to use my
own computers.</p>
<h4 id="r3-dependency-on-a-central-service">R3. Dependency on a central service</h4>
<p>If Docker at some point ceases operation of the Docker Hub, I am unable to
deploy software to my NAS. This isn’t a very hypothetical concern: In 2023,
Docker Hub <a href="https://news.ycombinator.com/item?id=35154025">announced the end of organizations on the Free
tier</a> and then backpedaled after
community backlash.</p>
<p>Who knows how long they can still provide free services to hobbyists like myself.</p>
<h4 id="no-immich">R4. Could not try Immich on Flatcar</h4>
<p>The final nail in the coffin was when I noticed that I could not try Immich on
my NAS system! Modern web applications like Immich need multiple Docker
containers (for Postgres, Redis, etc.) and hence only offer <a href="https://immich.app/docs/install/docker-compose">Docker
Compose</a> as a supported way of
installation.</p>
<p>Unfortunately, Flatcar <a href="https://github.com/flatcar/Flatcar/issues/894">does not include Docker
Compose</a>.</p>
<p>I was not in the mood to re-package Immich for non-Docker-Compose systems on an
ongoing basis, so I decided that a system on which I can neither run software
like Immich directly, nor even run Docker Compose, is not sufficient for my
needs anymore.</p>
<h4 id="reason-summary">Reason Summary</h4>
<p>With all of the above reasons, I would have had to set up automated container
builds, run my own central registry and would still be unable to run well-known
Open Source software like Immich.</p>
<p>Instead, I decided to try NixOS again (after a 10 year break) because it seems
like the most popular declarative solution nowadays, with a large community and
large selection of packages.</p>
<p>How does NixOS compare for my situation?</p>
<ul>
<li>Same: I also need to set up an automated job to update my NixOS systems.
<ul>
<li>I already have such a job for updating my <a href="https://gokrazy.org">gokrazy</a> devices.</li>
<li>Docker push is asynchronous: After a successful push, I still need extra
automation for pulling the updated containers on the target host and
restarting the affected services, whereas NixOS includes all of that.</li>
</ul>
</li>
<li>Better: There is no central registry. With NixOS, I can push the build result
directly to the target host via SSH.</li>
<li>Better: The corpus of available software in NixOS is much larger (including
Immich, for example) and the NixOS modules generally seem to be expressed at a
higher level of abstraction than individual Docker containers, meaning you can
configure more features with fewer lines of config.</li>
</ul>
<h2 id="vm-prototyping">Prototyping in a VM</h2>
<p>My NAS setup needs to work every day, so I wanted to prototype my desired
configuration in a VM before making changes to my system. This is not only
safer, it also allows me to discover any roadblocks, and what working with NixOS
feels like without making any commitments.</p>
<p>I copied my NixOS configuration from a previous test installation (see <a href="/posts/2025-06-01-nixos-installation-declarative/">“How I
like to install NixOS
(declaratively)”</a>) and used
the following command to build a VM image and start it in QEMU:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>nix build .#nixosConfigurations.storage2.config.system.build.vm
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#007020">export</span> <span style="color:#bb60d5">QEMU_NET_OPTS</span><span style="color:#666">=</span><span style="color:#bb60d5">hostfwd</span><span style="color:#666">=</span>tcp::2222-:22
</span></span><span style="display:flex;"><span><span style="color:#007020">export</span> <span style="color:#bb60d5">QEMU_KERNEL_PARAMS</span><span style="color:#666">=</span><span style="color:#bb60d5">console</span><span style="color:#666">=</span>ttyS0
</span></span><span style="display:flex;"><span>./result/bin/run-nixplay-vm
</span></span></code></pre></div><p>The configuration instructions below can be tried out in this VM, and once
you’re happy enough with what you have, you can repeat the steps on the actual
machine to migrate.</p>
<h2 id="migrating">Migrating</h2>
<p>For the migration of my actual system, I defined the following milestones that
should be achievable within a typical session of about an hour (after
prototyping them in a VM):</p>
<ul>
<li>M1. Install NixOS</li>
<li>M2. Set up remote disk unlock</li>
<li>M3. Set up Samba for access</li>
<li>M4. Set up SSH/rsync for backups</li>
<li>Everything extra is nice-to-have and could be deferred to a future session on
another day.</li>
</ul>
<p>In practice, this worked out exactly as planned: the actual installation of
NixOS and setting up my config to milestone M4 took a little over one hour. All
the other nice-to-haves were done over the following days and weeks as time
permitted.</p>
<p><strong>Tip:</strong> After losing data due to an installer bug in the 2000s, I have adopted
the habit of physically disconnecting all data disks (= pulling out the SATA
cable) when re-installing the system disk.</p>
<h3 id="m1-install-nixos">M1. Install NixOS</h3>
<p>After following <a href="/posts/2025-06-01-nixos-installation-declarative/">“How I like to install NixOS
(declaratively)”</a>, this is
my initial <code>configuration.nix</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ modulesPath<span style="color:#666">,</span> lib<span style="color:#666">,</span> pkgs<span style="color:#666">,</span> <span style="color:#666">...</span> }:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  imports <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>    [
</span></span><span style="display:flex;"><span>      (modulesPath <span style="color:#666">+</span> <span style="color:#4070a0">&#34;/installer/scan/not-detected.nix&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#235388">./hardware-configuration.nix</span>
</span></span><span style="display:flex;"><span>      <span style="color:#235388">./disk-config.nix</span>
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  <span style="color:#60a0b0;font-style:italic"># Adding michael as trusted user means</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  <span style="color:#60a0b0;font-style:italic"># we can upgrade the system via SSH (see Makefile).</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  nix<span style="color:#666">.</span>settings<span style="color:#666">.</span>trusted-users <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;michael&#34;</span> <span style="color:#4070a0">&#34;root&#34;</span> ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  <span style="color:#60a0b0;font-style:italic"># Clean the Nix store every week.</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  nix<span style="color:#666">.</span>gc <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    automatic <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    dates <span style="color:#666">=</span> <span style="color:#4070a0">&#34;weekly&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    options <span style="color:#666">=</span> <span style="color:#4070a0">&#34;--delete-older-than 7d&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  boot<span style="color:#666">.</span>loader<span style="color:#666">.</span>systemd-boot <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    configurationLimit <span style="color:#666">=</span> <span style="color:#40a070">10</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  boot<span style="color:#666">.</span>loader<span style="color:#666">.</span>efi<span style="color:#666">.</span>canTouchEfiVariables <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  networking<span style="color:#666">.</span>hostName <span style="color:#666">=</span> <span style="color:#4070a0">&#34;storage2&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  time<span style="color:#666">.</span>timeZone <span style="color:#666">=</span> <span style="color:#4070a0">&#34;Europe/Zurich&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  <span style="color:#60a0b0;font-style:italic"># Use systemd for networking</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  services<span style="color:#666">.</span>resolved<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  networking<span style="color:#666">.</span>useDHCP <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>networks<span style="color:#666">.</span><span style="color:#4070a0">&#34;10-e&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    matchConfig<span style="color:#666">.</span>Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;e*&#34;</span>;  <span style="color:#60a0b0;font-style:italic"># enp9s0 (10G) or enp8s0 (1G)</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    networkConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      IPv6AcceptRA <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      DHCP <span style="color:#666">=</span> <span style="color:#4070a0">&#34;yes&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  i18n<span style="color:#666">.</span>supportedLocales <span style="color:#666">=</span> [
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#34;en_DK.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#34;de_DE.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#34;de_CH.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#34;en_US.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  i18n<span style="color:#666">.</span>defaultLocale <span style="color:#666">=</span> <span style="color:#4070a0">&#34;en_US.UTF-8&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  users<span style="color:#666">.</span>mutableUsers <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  security<span style="color:#666">.</span>sudo<span style="color:#666">.</span>wheelNeedsPassword <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>michael <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    openssh<span style="color:#666">.</span>authorizedKeys<span style="color:#666">.</span>keys <span style="color:#666">=</span> [
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      <span style="color:#4070a0">&#34;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5secret&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      <span style="color:#4070a0">&#34;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5key&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    isNormalUser <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;Michael Stapelberg&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    extraGroups <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;networkmanager&#34;</span> <span style="color:#4070a0">&#34;wheel&#34;</span> ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    initialPassword <span style="color:#666">=</span> <span style="color:#4070a0">&#34;secret&#34;</span>;  <span style="color:#60a0b0;font-style:italic"># XXX: change!</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    shell <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>zsh;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    packages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    git  <span style="color:#60a0b0;font-style:italic"># for checking out github.com/stapelberg/configfiles</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    rsync
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    zsh
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    vim
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    emacs
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    wget
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    curl
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  programs<span style="color:#666">.</span>zsh<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  services<span style="color:#666">.</span>openssh<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># This value determines the NixOS release from which the default</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># settings for stateful data, like file locations and database versions</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># on your system were taken. It‘s perfectly fine and recommended to leave</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># this value at the release version of the first install of this system.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Before changing this value read the documentation for this option</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).</span>
</span></span><span style="display:flex;"><span>  system<span style="color:#666">.</span>stateVersion <span style="color:#666">=</span> <span style="color:#4070a0">&#34;25.05&#34;</span>; <span style="color:#60a0b0;font-style:italic"># Did you read the comment?</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>All following sections describe changes within this <code>configuration.nix</code>.</p>
<p>All devices in my home network obtain their IP address via DHCP. If I want to
make an IP address static, I configure it accordingly on my router.</p>
<p>My NAS PCs have one specialty with regards to IP addressing: They are reachable
via IPv4 and IPv6, and the IPv6 address can be derived from the IPv4 address.</p>
<p>Hence, I changed the systemd-networkd configuration from above such that it
configures a static IPv6 address in a dynamically configured IPv6 network:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>networks<span style="color:#666">.</span><span style="color:#4070a0">&#34;10-e&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    matchConfig<span style="color:#666">.</span>Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;e*&#34;</span>;  <span style="color:#60a0b0;font-style:italic"># enp9s0 (10G) or enp8s0 (1G)</span>
</span></span><span style="display:flex;"><span>    networkConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      IPv6AcceptRA <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>      DHCP <span style="color:#666">=</span> <span style="color:#4070a0">&#34;yes&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    ipv6AcceptRAConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      Token <span style="color:#666">=</span> <span style="color:#4070a0">&#34;::10:0:0:252&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    };
</span></span><span style="display:flex;"><span>  };</span></span></code></pre></div>
<p>✅ This fulfills milestone M1.</p>
<h3 id="m2-set-up-remote-disk-unlock">M2. Set up remote disk unlock</h3>
<p>To unlock my encrypted disks on boot, I have a custom systemd service unit that
uses <a href="https://manpages.debian.org/wget.1"><code>wget(1)</code></a>
 and <a href="https://manpages.debian.org/cryptsetup.8"><code>cryptsetup(8)</code></a>
 to split the key file between the NAS and a remote server (= an
attacker needs both pieces to unlock).</p>
<p>With CoreOS/Flatcar, my <code>cloud-init</code> configuration looked as follows:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">coreos</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span><span style="color:#062873;font-weight:bold">units</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span>- <span style="color:#062873;font-weight:bold">name</span>:<span style="color:#bbb"> </span>unlock.service<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">      </span><span style="color:#062873;font-weight:bold">command</span>:<span style="color:#bbb"> </span>start<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">      </span><span style="color:#062873;font-weight:bold">content</span>:<span style="color:#bbb"> </span>|<span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        [Unit]
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Description=unlock hard drive
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Wants=network.target
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        After=systemd-networkd-wait-online.service
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Before=samba.service
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        [Service]
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Type=oneshot
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        RemainAfterExit=yes
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        # Wait until the host is actually reachable.
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStart=/bin/sh -c &#34;c=0; while [ $c -lt 5 ]; do /bin/ping6 -n -c 1 r.zekjur.net &amp;&amp; break; c=$((c+1)); sleep 1; done&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStart=/bin/sh -c &#34;[ -e \&#34;/dev/mapper/S5SSNF0T205183F_crypt\&#34; ] || (echo -n my_local_secret &amp;&amp; wget --retry-connrefused --ca-directory=/dev/null --ca-certificate=/etc/ssl/certs/r.zekjur.net.crt -qO - https://r.zekjur.net:8443/nascrypto) | /sbin/cryptsetup --key-file=- luksOpen /dev/disk/by-id/ata-Samsung_SSD_870_QVO_8TB_S5SSNF0T205183F S5SSNF0T205183F_crypt&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStart=/bin/sh -c &#34;[ -e \&#34;/dev/mapper/S5SSNJ0T205991B_crypt\&#34; ] || (echo -n my_local_secret &amp;&amp; wget --retry-connrefused --ca-directory=/dev/null --ca-certificate=/etc/ssl/certs/r.zekjur.net.crt -qO - https://r.zekjur.net:8443/nascrypto) | /sbin/cryptsetup --key-file=- luksOpen /dev/disk/by-id/ata-Samsung_SSD_870_QVO_8TB_S5SSNJ0T205991B S5SSNJ0T205991B_crypt&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStart=/bin/sh -c &#34;vgchange -ay&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStart=/bin/mount /dev/mapper/data-data /srv</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#062873;font-weight:bold">write_files</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span>- <span style="color:#062873;font-weight:bold">path</span>:<span style="color:#bbb"> </span>/etc/ssl/certs/r.zekjur.net.crt<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">content</span>:<span style="color:#bbb"> </span>|<span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">      -----BEGIN CERTIFICATE-----
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">      MIID8TCCAlmgAwIBAgIRAPWwvYWpoH+lGKv6rxZvC4MwDQYJKoZIhvcNAQELBQAw
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">      […]
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">      -----END CERTIFICATE-----</span><span style="color:#bbb">
</span></span></span></code></pre></div><p>I converted it into the following NixOS configuration:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>unlock <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;multi-user.target&#34;</span> ];
</span></span><span style="display:flex;"><span>    description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;unlock hard drive&#34;</span>;
</span></span><span style="display:flex;"><span>    wants <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;network.target&#34;</span> ];
</span></span><span style="display:flex;"><span>    after <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;systemd-networkd-wait-online.service&#34;</span> ];
</span></span><span style="display:flex;"><span>    serviceConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      Type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;oneshot&#34;</span>;
</span></span><span style="display:flex;"><span>      RemainAfterExit <span style="color:#666">=</span> <span style="color:#4070a0">&#34;yes&#34;</span>;
</span></span><span style="display:flex;"><span>      ExecStart <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#60a0b0;font-style:italic"># Wait until the host is actually reachable.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;/bin/sh -c &#34;c=0; while [ $c -lt 5 ]; do </span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>iputils<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/ping -n -c 1 r.zekjur.net &amp;&amp; break; c=$((c+1)); sleep 1; done&#34;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;/bin/sh -c &#34;[ -e \&#34;/dev/mapper/S5SSNF0T205183F_crypt\&#34; ] || (echo -n my_local_secret &amp;&amp; </span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>wget<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/wget --retry-connrefused --ca-directory=/dev/null --ca-certificate=/etc/ssl/certs/r.zekjur.net.crt -qO - https://r.zekjur.net:8443/sdb2_crypt) | </span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>cryptsetup<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/cryptsetup --key-file=- luksOpen /dev/disk/by-id/ata-Samsung_SSD_870_QVO_8TB_S5SSNF0T205183F S5SSNF0T205183F_crypt&#34;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;/bin/sh -c &#34;[ -e \&#34;/dev/mapper/S5SSNJ0T205991B_crypt\&#34; ] || (echo -n my_local_secret &amp;&amp; </span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>wget<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/wget --retry-connrefused --ca-directory=/dev/null --ca-certificate=/etc/ssl/certs/r.zekjur.net.crt -qO - https://r.zekjur.net:8443/sdc2_crypt) | </span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>cryptsetup<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/cryptsetup --key-file=- luksOpen /dev/disk/by-id/ata-Samsung_SSD_870_QVO_8TB_S5SSNJ0T205991B S5SSNJ0T205991B_crypt&#34;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;/bin/sh -c &#34;</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>lvm2<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/vgchange -ay&#34;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;/run/wrappers/bin/mount /dev/mapper/data-data /srv&#39;&#39;</span>
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><p>We’ll also need to store the custom TLS certificate file on disk. For that, we
can use the <code>environment.</code> configuration:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  environment<span style="color:#666">.</span>etc<span style="color:#666">.</span><span style="color:#4070a0">&#34;ssl/certs/r.zekjur.net.crt&#34;</span><span style="color:#666">.</span>text <span style="color:#666">=</span> <span style="color:#4070a0">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">-----BEGIN CERTIFICATE-----
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">MIID8TCCAlmgAwIBAgIRAPWwvYWpoH+lGKv6rxZvC4MwDQYJKoZIhvcNAQELBQAw
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">[…]
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">-----END CERTIFICATE-----
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">&#39;&#39;</span>;
</span></span></code></pre></div><p>The references like <code>${pkgs.wget}</code> will be replaced with a path to the Nix store
(<a href="https://nix.dev/tutorials/nix-language.html#paths">→ nix.dev
documentation</a>). On
CoreOS/Flatcar, I was limited to using just the (minimal set of) software
included in the base image, or I had to reach for Docker. On NixOS, we can use
all packages available in nixpkgs.</p>
<p>After <a href="/posts/2025-06-01-nixos-installation-declarative/#making-changes">deploying</a>
and <code>reboot</code>ing, I can access my unlocked disk under <code>/srv</code>! 🎉</p>
<pre tabindex="0"><code>% df -h /srv
Filesystem             Size  Used Avail Use% Mounted on
/dev/mapper/data-data   15T   14T  342G  98% /srv
</code></pre><p>When listing my files, I noticed that the group id was different between my old
system and the new system. This can be fixed by explicitly specifying the
desired group id:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  users<span style="color:#666">.</span>groups<span style="color:#666">.</span>michael <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    gid <span style="color:#666">=</span> <span style="color:#40a070">1000</span>;  <span style="color:#60a0b0;font-style:italic"># for consistency with storage3</span>
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><p>✅ M2 is complete.</p>
<h3 id="m3-set-up-samba-for-access">M3. Set up Samba for access</h3>
<p>Whereas I want to configure remote disk unlock at the systemd service level, for
Samba I want to use Docker: I wanted to first transfer my old (working)
Docker-based setups as they are, and only later convert them to Nix.</p>
<p>We enable the <a href="https://search.nixos.org/options?query=virtualisation.docker.enable">Docker NixOS
module</a>
which sets up the daemons that Docker needs and whatever else is needed to make
it work:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  virtualisation<span style="color:#666">.</span>docker<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span></code></pre></div><p>This is already sufficient for other services to use Docker, but I also want to
be able to run the <code>docker</code> command interactively for debugging. Therefore, I
added <code>docker</code> to <code>systemPackages</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [
</span></span><span style="display:flex;"><span>    git  <span style="color:#60a0b0;font-style:italic"># for checking out github.com/stapelberg/configfiles</span>
</span></span><span style="display:flex;"><span>    rsync
</span></span><span style="display:flex;"><span>    zsh
</span></span><span style="display:flex;"><span>    vim
</span></span><span style="display:flex;"><span>    emacs
</span></span><span style="display:flex;"><span>    wget
</span></span><span style="display:flex;"><span>    curl
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    docker
</span></span><span style="display:flex;"><span>  ];</span></span></code></pre></div>
<p>After deploying this configuration, I can run <code>docker run -ti debian</code> to verify things work.</p>
<p>The <code>cloud-init</code> version of samba looked like this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">coreos</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span><span style="color:#062873;font-weight:bold">units</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span>- <span style="color:#062873;font-weight:bold">name</span>:<span style="color:#bbb"> </span>samba.service<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">      </span><span style="color:#062873;font-weight:bold">command</span>:<span style="color:#bbb"> </span>start<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">      </span><span style="color:#062873;font-weight:bold">content</span>:<span style="color:#bbb"> </span>|<span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        [Unit]
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Description=samba server
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        After=docker.service unlock.mount
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Requires=docker.service unlock.mount
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        [Service]
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Restart=always
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        StartLimitInterval=0
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        # Always pull the latest version (bleeding edge).
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=-/usr/bin/docker pull stapelberg/docker-samba:latest
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        # Set up samba users (cannot be done in the (public) Dockerfile because
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        # users/passwords are sensitive information).
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=-/usr/bin/docker kill smb
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=-/usr/bin/docker rm smb
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=-/usr/bin/docker rm smb-prep
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=/usr/bin/docker run --name smb-prep stapelberg/docker-samba sh -c &#39;adduser --quiet --disabled-password --gecos &#34;&#34; --uid 29901 michael &amp;&amp; sed -i &#34;s,\\[global\\],[global]\\nserver multi channel support = yes\\naio read size = 1\\naio write size = 1,g&#34; /etc/samba/smb.conf&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=/usr/bin/docker commit smb-prep smb-prepared
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=/usr/bin/docker rm smb-prep
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=/usr/bin/docker run --name smb-prep smb-prepared /bin/sh -c &#34;echo \&#34;secret\nsecret\n&#34; | tee - | smbpasswd -a -s michael&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=/usr/bin/docker commit smb-prep smb-prepared
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStart=/usr/bin/docker run \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">          -p 137:137 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">          -p 138:138 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">          -p 139:139 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">          -p 445:445 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">          --tmpfs=/run \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">          -v /srv/data:/srv/data \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">          --name smb \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">          -t \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">          smb-prepared \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">            /usr/sbin/smbd --foreground --debug-stdout --no-process-group</span><span style="color:#bbb">
</span></span></span></code></pre></div><p>We can translate this 1:1 to NixOS:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>samba <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;multi-user.target&#34;</span> ];
</span></span><span style="display:flex;"><span>    description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;samba server&#34;</span>;
</span></span><span style="display:flex;"><span>    after <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;unlock.service&#34;</span> ];
</span></span><span style="display:flex;"><span>    requires <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;unlock.service&#34;</span> ];
</span></span><span style="display:flex;"><span>    serviceConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      Restart <span style="color:#666">=</span> <span style="color:#4070a0">&#34;always&#34;</span>;
</span></span><span style="display:flex;"><span>      StartLimitInterval <span style="color:#666">=</span> <span style="color:#40a070">0</span>;
</span></span><span style="display:flex;"><span>      ExecStartPre <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#60a0b0;font-style:italic"># Always pull the latest version.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker pull stapelberg/docker-samba:latest&#39;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#60a0b0;font-style:italic"># Set up samba users (cannot be done in the (public) Dockerfile because</span>
</span></span><span style="display:flex;"><span>        <span style="color:#60a0b0;font-style:italic"># users/passwords are sensitive information).</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker kill smb&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker rm smb&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker rm smb-prep&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker run --name smb-prep stapelberg/docker-samba sh -c &#39;adduser --quiet --disabled-password --gecos &#34;&#34; --uid 29901 michael &amp;&amp; sed -i &#34;s,\\[global\\],[global]\\nserver multi channel support = yes\\naio read size = 1\\naio write size = 1,g&#34; /etc/samba/smb.conf&#39; &#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker commit smb-prep smb-prepared&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker rm smb-prep&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker run --name smb-prep smb-prepared /bin/sh -c &#34;echo \&#34;secret\nsecret\n&#34; | tee - | smbpasswd -a -s michael&#34;&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker commit smb-prep smb-prepared&#39;&#39;</span>
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      ExecStart <span style="color:#666">=</span> <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker run \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">           -p 137:137 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">           -p 138:138 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">           -p 139:139 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">           -p 445:445 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">           --tmpfs=/run \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">           -v /srv/data:/srv/data \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">           --name smb \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">           -t \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">           smb-prepared \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">             /usr/sbin/smbd --foreground --debug-stdout --no-process-group
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">             &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span><span style="">}</span>
</span></span></code></pre></div><p>✅ Now I can manage my files over the network, which completes M3!</p>
<p>See also: <a href="#samba-nixos">Nice-to-haves: N5. samba from NixOS</a></p>
<h3 id="m4-set-up-sshrsync-for-backups">M4. Set up SSH/rsync for backups</h3>
<p>For backing up data, I use rsync over SSH. I restrict this SSH access to run
only rsync commands by using <code>rrsync</code> (in a Docker container). To configure the
SSH <a href="https://manpages.debian.org/authorized_keys.5"><code>authorized_keys(5)</code></a>
, we set:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>root<span style="color:#666">.</span>openssh<span style="color:#666">.</span>authorizedKeys<span style="color:#666">.</span>keys <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#39;&#39;command=&#34;</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker run --log-driver none -i -e SSH_ORIGINAL_COMMAND -v /srv/backup/midna:/srv/backup/midna stapelberg/docker-rsync /srv/backup/midna&#34; ssh-rsa AAAAB3Npublickey root@midna&#39;&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="">}</span>;
</span></span></code></pre></div><p>✅ A successful test backup run completes milestone M4!</p>
<p>See also: <a href="#rrsync-nixos">Nice-to-haves: N6. rrsync from NixOS</a></p>
<h2 id="nice-to-haves">Nice-to-haves</h2>
<h3 id="prometheus-node-exporter">N1. Prometheus Node Exporter</h3>
<p>I like to monitor all my machines with <a href="https://prometheus.io">Prometheus</a> (and
Grafana). For network connectivity and authentication, I use the Tailscale mesh
VPN.</p>
<p>To install Tailscale, I <a href="https://search.nixos.org/options?query=services.tailscale.enable">enable its NixOS
module</a> and
make the <code>tailscale</code> command available:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  services<span style="color:#666">.</span>tailscale<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [ tailscale ];
</span></span></code></pre></div><p>After deploying, I run <code>sudo tailscale up</code> and open the login link in my browser.</p>
<p>The Prometheus Node Exporter can also easily be enabled <a href="https://search.nixos.org/options?query=services.prometheus.exporters.node.enable">through its NixOS
module</a>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  services<span style="color:#666">.</span>prometheus<span style="color:#666">.</span>exporters<span style="color:#666">.</span>node <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    listenAddress <span style="color:#666">=</span> <span style="color:#4070a0">&#34;storage2.example.ts.net&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><p>However, this isn’t reliable yet: When Tailscale’s startup takes a while during
system boot, the Node Exporter might burn through its entire restart budget when
it cannot listen on the Tailscale IP address yet. We can enable <a href="/posts/2024-01-17-systemd-indefinite-service-restarts/">indefinite
restarts</a> for the
service to eventually come up:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span><span style="color:#4070a0">&#34;prometheus-node-exporter&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    startLimitIntervalSec <span style="color:#666">=</span> <span style="color:#40a070">0</span>;
</span></span><span style="display:flex;"><span>    serviceConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      Restart <span style="color:#666">=</span> <span style="color:#4070a0">&#34;always&#34;</span>;
</span></span><span style="display:flex;"><span>      RestartSec <span style="color:#666">=</span> <span style="color:#40a070">1</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><h3 id="reliable-mount">N2. Reliable mounting</h3>
<p>While migrating my setup, I noticed that calling <a href="https://manpages.debian.org/mount.8"><code>mount(8)</code></a>
 from <code>unlock.service</code> directly is not reliable, and it’s better
to let systemd manage the mounting:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  fileSystems<span style="color:#666">.</span><span style="color:#4070a0">&#34;/srv&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    device <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/dev/mapper/data-data&#34;</span>;
</span></span><span style="display:flex;"><span>    fsType <span style="color:#666">=</span> <span style="color:#4070a0">&#34;ext4&#34;</span>;
</span></span><span style="display:flex;"><span>    options <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>      <span style="color:#4070a0">&#34;nofail&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#4070a0">&#34;x-systemd.requires=unlock.service&#34;</span>
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><p>Afterwards, I could just remove the <a href="https://manpages.debian.org/mount.8"><code>mount(8)</code></a>
 call
from <code>unlock.service</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -247,7 +247,10 @@ fry/U6A=
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span>         &#39;&#39;/bin/sh -c &#34;${pkgs.lvm2.bin}/bin/vgchange -ay&#34;&#39;&#39;
</span></span><span style="display:flex;"><span><span style="color:#a00000">-        &#39;&#39;/run/wrappers/bin/mount /dev/mapper/data-data /srv&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+        # Let systemd mount /srv based on the fileSystems./srv
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+        # declaration to prevent race conditions: mount
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+        # might not succeed while the fsck is still in progress,
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+        # for example, which otherwise makes unlock.service fail.
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>       ];
</span></span><span style="display:flex;"><span>     };
</span></span></code></pre></div><p>In systemd services, I can now depend on the <code>/srv</code> mount unit:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>jellyfin <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    unitConfig<span style="color:#666">.</span>RequiresMountsFor <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;/srv&#34;</span> ];
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;srv.mount&#34;</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><h3 id="nginx-healthz">N3. nginx-healthz</h3>
<p>To save power, I turn off my NAS when they are not in use.</p>
<p>My backup orchestration uses Wake-on-LAN to start a wakeup and needs to wait
until the NAS is fully booted up and has mounted its <code>/srv</code> mount before it
can start backup jobs.</p>
<p>For this purpose, I have configured a web server (without any files) that
depends on the <code>/srv</code> mount. So, once the web server responds to HTTP requests,
we know <code>/srv</code> is mounted.</p>
<p>The <code>cloud-init</code> config looked as follows:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">coreos</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span><span style="color:#062873;font-weight:bold">units</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span>- <span style="color:#062873;font-weight:bold">name</span>:<span style="color:#bbb"> </span>healthz.service<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">      </span><span style="color:#062873;font-weight:bold">command</span>:<span style="color:#bbb"> </span>start<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">      </span><span style="color:#062873;font-weight:bold">content</span>:<span style="color:#bbb"> </span>|<span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        [Unit]
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Description=nginx for /srv health check
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Wants=network.target
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        After=srv.mount
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Requires=srv.mount
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        StartLimitInterval=0
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        [Service]
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        Restart=always
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=/bin/sh -c &#39;systemctl is-active docker.service&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=/usr/bin/docker pull nginx:1
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=-/usr/bin/docker kill nginx-healthz
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStartPre=-/usr/bin/docker rm -f nginx-healthz
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">        ExecStart=/usr/bin/docker run \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">            --name nginx-healthz \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">            --publish 10.0.0.252:8200:80 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">            --log-driver=journald \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-style:italic">            nginx:1</span><span style="color:#bbb">
</span></span></span></code></pre></div><p>The Docker version (ported from Flatcar Linux) looks like this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>healthz <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nginx for /srv health check&#34;</span>;
</span></span><span style="display:flex;"><span>    wants <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;network.target&#34;</span> ];
</span></span><span style="display:flex;"><span>    unitConfig<span style="color:#666">.</span>RequiresMountsFor <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;/srv&#34;</span> ];
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;srv.mount&#34;</span> ];
</span></span><span style="display:flex;"><span>    startLimitIntervalSec <span style="color:#666">=</span> <span style="color:#40a070">0</span>;
</span></span><span style="display:flex;"><span>    serviceConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      Restart <span style="color:#666">=</span> <span style="color:#4070a0">&#34;always&#34;</span>;
</span></span><span style="display:flex;"><span>      ExecStartPre <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;/bin/sh -c &#39;systemctl is-active docker.service&#39; &#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker pull nginx:1&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker kill nginx-healthz&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker rm -f nginx-healthz&#39;&#39;</span>
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      ExecStart <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker run \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">            --name nginx-healthz \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">            --publish 10.0.0.252:8200:80 \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">            --log-driver=journald \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">            nginx:1
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#39;&#39;</span>
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><p>This configuration gets a lot simpler when migrating it from Docker to NixOS:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Signal readiness on HTTP port 8200 once /srv is mounted:</span>
</span></span><span style="display:flex;"><span>  networking<span style="color:#666">.</span>firewall<span style="color:#666">.</span>allowedTCPPorts <span style="color:#666">=</span> [ <span style="color:#40a070">8200</span> ];
</span></span><span style="display:flex;"><span>  services<span style="color:#666">.</span>caddy <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    virtualHosts<span style="color:#666">.</span><span style="color:#4070a0">&#34;http://10.0.0.252:8200&#34;</span><span style="color:#666">.</span>extraConfig <span style="color:#666">=</span> <span style="color:#4070a0">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">      respond &#34;ok&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>caddy <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    unitConfig<span style="color:#666">.</span>RequiresMountsFor <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;/srv&#34;</span> ];
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;srv.mount&#34;</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><h3 id="jellyfin">N4. NixOS Jellyfin</h3>
<p>The Docker version (ported from Flatcar Linux) looks like this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  networking<span style="color:#666">.</span>firewall<span style="color:#666">.</span>allowedTCPPorts <span style="color:#666">=</span> [ <span style="color:#40a070">4414</span> <span style="color:#40a070">8096</span> ];
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>jellyfin <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;multi-user.target&#34;</span> ];
</span></span><span style="display:flex;"><span>    description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;jellyfin&#34;</span>;
</span></span><span style="display:flex;"><span>    after <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;docker.service&#34;</span> <span style="color:#4070a0">&#34;srv.mount&#34;</span> ];
</span></span><span style="display:flex;"><span>    requires <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;docker.service&#34;</span> <span style="color:#4070a0">&#34;srv.mount&#34;</span> ];
</span></span><span style="display:flex;"><span>    startLimitIntervalSec <span style="color:#666">=</span> <span style="color:#40a070">0</span>;
</span></span><span style="display:flex;"><span>    serviceConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      Restart <span style="color:#666">=</span> <span style="color:#4070a0">&#34;always&#34;</span>;
</span></span><span style="display:flex;"><span>      ExecStartPre <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker pull lscr.io/linuxserver/jellyfin:latest&#39;&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker rm jellyfin&#39;&#39;</span>
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>      ExecStart <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#39;&#39;-</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker run \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          --rm \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          --net=host \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          --name=jellyfin \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          -e TZ=Europe/Zurich \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          -v /srv/jellyfin/config:/config \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          -v /srv/data/movies:/data/movies:ro \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          -v /srv/data/series:/data/series:ro \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          -v /srv/data/mp3:/data/mp3:ro \
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">          lscr.io/linuxserver/jellyfin:latest
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#39;&#39;</span>
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><p>As before, when using jellyfin from NixOS, the configuration gets simpler:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  services<span style="color:#666">.</span>jellyfin <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    openFirewall <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>jellyfin <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    unitConfig<span style="color:#666">.</span>RequiresMountsFor <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;/srv&#34;</span> ];
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;srv.mount&#34;</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><p>For a while, I had also set up compatibility symlinks that map the old location
(<code>/data/movies</code>, inside the Docker container) to the new location
(<code>/srv/data/movies</code>), but I encountered strange issues in Jellyfin and ended up
just re-initializing my whole Jellyfin state. While the required configuration
had more lines, I found it neat to move it into its own file, so here is how to
do that:</p>
<p>Remove the lines above from <code>configuration.nix</code> and move them into
<code>jellyfin.nix</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  config<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  lib<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  pkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  modulesPath<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#666">...</span>
</span></span><span style="display:flex;"><span>}:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  services<span style="color:#666">.</span>jellyfin <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    openFirewall <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    dataDir <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/srv/jellyfin&#34;</span>;
</span></span><span style="display:flex;"><span>    cacheDir <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/srv/jellyfin/config/cache&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>jellyfin <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    unitConfig<span style="color:#666">.</span>RequiresMountsFor <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;/srv&#34;</span> ];
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;srv.mount&#34;</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then, in <code>configuration.nix</code>, add <code>jellyfin.nix</code> to the <code>imports</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>   imports <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>     <span style="color:#235388">./hardware-configuration.nix</span>
</span></span><span style="display:flex;"><span>     <span style="color:#235388">./jellyfin.nix</span>
</span></span><span style="display:flex;"><span>   ];
</span></span></code></pre></div><h3 id="samba-nixos">N5. NixOS samba</h3>
<p>To use Samba from NixOS, I replaced my <code>systemd.services.samba</code> config from M3
with this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  services<span style="color:#666">.</span>samba <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    openFirewall <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    settings <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#4070a0">&#34;global&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#34;map to guest&#34;</span> <span style="color:#666">=</span> <span style="color:#4070a0">&#34;bad user&#34;</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      <span style="color:#4070a0">&#34;data&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#34;path&#34;</span> <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/srv/data&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#34;comment&#34;</span> <span style="color:#666">=</span> <span style="color:#4070a0">&#34;public data&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#34;read only&#34;</span> <span style="color:#666">=</span> <span style="color:#4070a0">&#34;no&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#34;create mask&#34;</span> <span style="color:#666">=</span> <span style="color:#4070a0">&#34;0775&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#34;directory mask&#34;</span> <span style="color:#666">=</span> <span style="color:#4070a0">&#34;0775&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#4070a0">&#34;guest ok&#34;</span> <span style="color:#666">=</span> <span style="color:#4070a0">&#34;yes&#34;</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  system<span style="color:#666">.</span>activationScripts<span style="color:#666">.</span>samba_user_create <span style="color:#666">=</span> <span style="color:#4070a0">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">      smb_password=&#34;secret&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">      echo -e &#34;$smb_password\n$smb_password\n&#34; | </span><span style="color:#70a0d0">${</span>lib<span style="color:#666">.</span>getExe&#39; pkgs<span style="color:#666">.</span>samba <span style="color:#4070a0">&#34;smbpasswd&#34;</span><span style="color:#70a0d0">}</span><span style="color:#4070a0"> -a -s michael
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    &#39;&#39;</span>;
</span></span></code></pre></div><p>Note: Setting the samba password in the activation script works for small
setups, but if you want to keep your samba passwords out of the Nix store,
you’ll need to use a different approach. On a different machine, I use
<a href="https://github.com/Mic92/sops-nix">sops-nix</a> to manage secrets and found that
refactoring the <code>smbpasswd</code> call like so works reliably:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>  setPasswords <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>writeShellScript <span style="color:#4070a0">&#34;samba-set-passwords&#34;</span> <span style="color:#4070a0">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    set -euo pipefail
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    for user in michael; do
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        smb_password=&#34;$(cat /run/secrets/samba_passwords/$user)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        echo -e &#34;$smb_password\n$smb_password\n&#34; | </span><span style="color:#70a0d0">${</span>lib<span style="color:#666">.</span>getExe&#39; pkgs<span style="color:#666">.</span>samba <span style="color:#4070a0">&#34;smbpasswd&#34;</span><span style="color:#70a0d0">}</span><span style="color:#4070a0"> -a -s $user
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    done
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">  &#39;&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># …</span>
</span></span><span style="display:flex;"><span>  services<span style="color:#666">.</span>samba <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># …as above…</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span>samba-smbd<span style="color:#666">.</span>serviceConfig<span style="color:#666">.</span>ExecStartPre <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>setPasswords<span style="color:#70a0d0">}</span><span style="color:#4070a0">&#34;</span>
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  sops<span style="color:#666">.</span>secrets<span style="color:#666">.</span><span style="color:#4070a0">&#34;samba_passwords/michael&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    restartUnits <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;samba-smbd.service&#34;</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I also noticed that NixOS does not create a group for each user by default, but
I am used to managing my permissions like that. We can easily declare a group
like so:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  users<span style="color:#666">.</span>groups<span style="color:#666">.</span>michael <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    gid <span style="color:#666">=</span> <span style="color:#40a070">1000</span>; <span style="color:#60a0b0;font-style:italic"># for consistency with storage3</span>
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>michael <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    extraGroups <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>      <span style="color:#4070a0">&#34;wheel&#34;</span> <span style="color:#60a0b0;font-style:italic"># Enable ‘sudo’ for the user.</span>
</span></span><span style="display:flex;"><span>      <span style="color:#4070a0">&#34;docker&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#60a0b0;font-style:italic"># By default, NixOS does not add users to their own group:</span>
</span></span><span style="display:flex;"><span>      <span style="color:#60a0b0;font-style:italic"># https://github.com/NixOS/nixpkgs/issues/198296</span>
</span></span><span style="display:flex;"><span>      <span style="color:#4070a0">&#34;michael&#34;</span>
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><h3 id="rrsync-nixos">N6. NixOS rrsync</h3>
<p>The Docker version (ported from Flatcar Linux) looks like this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>root<span style="color:#666">.</span>openssh<span style="color:#666">.</span>authorizedKeys<span style="color:#666">.</span>keys <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#39;&#39;command=&#34;</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker run --log-driver none -i -e SSH_ORIGINAL_COMMAND -v /srv/backup/midna:/srv/backup/midna stapelberg/docker-rsync /srv/backup/midna&#34; ssh-rsa AAAAB3Npublickey root@midna&#39;&#39;</span>
</span></span><span style="display:flex;"><span>  ];
</span></span></code></pre></div><p>To use <code>rrsync</code> from NixOS, I changed the configuration like so:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>root<span style="color:#666">.</span>openssh<span style="color:#666">.</span>authorizedKeys<span style="color:#666">.</span>keys <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#39;&#39;command=&#34;</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>rrsync<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/rrsync /srv/backup/midna&#34; ssh-rsa AAAAB3Npublickey root@midna&#39;&#39;</span>
</span></span><span style="display:flex;"><span>  ];
</span></span></code></pre></div><h3 id="syncpl-nixos">N7. sync.pl script</h3>
<p>The Docker version (ported from Flatcar Linux) looks like this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>root<span style="color:#666">.</span>openssh<span style="color:#666">.</span>authorizedKeys<span style="color:#666">.</span>keys <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#39;&#39;command=&#34;</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>docker<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/docker run --log-driver none -i -e SSH_ORIGINAL_COMMAND -v /srv/data:/srv/data -v /root/.ssh:/root/.ssh:ro -v /etc/ssh:/etc/ssh:ro -v /etc/static/ssh:/etc/static/ssh:ro -v /nix/store:/nix/store:ro stapelberg/docker-sync&#34;,no-port-forwarding,no-X11-forwarding ssh-ed25519 AAAAC3Npublickey sync@dr&#39;&#39;</span>
</span></span><span style="display:flex;"><span>  ];
</span></span></code></pre></div><p>I wanted to stop managing the following <code>Dockerfile</code> to ship <code>sync.pl</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Dockerfile" data-lang="Dockerfile"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">FROM</span><span style="color:#bbb"> </span><span style="color:#4070a0">debian:stable</span><span style="">
</span></span></span><span style="display:flex;"><span><span style="">
</span></span></span><span style="display:flex;"><span><span style=""></span><span style="color:#60a0b0;font-style:italic"># Install full perl for Data::Dumper</span><span style="">
</span></span></span><span style="display:flex;"><span><span style=""></span><span style="color:#007020;font-weight:bold">RUN</span> apt-get update <span style="color:#4070a0;font-weight:bold">\
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-weight:bold"></span>    <span style="color:#666">&amp;&amp;</span> apt-get install -y rsync ssh perl<span style="">
</span></span></span><span style="display:flex;"><span><span style="">
</span></span></span><span style="display:flex;"><span><span style=""></span><span style="color:#007020;font-weight:bold">ADD</span> sync.pl /usr/bin/<span style="">
</span></span></span><span style="display:flex;"><span><span style="">
</span></span></span><span style="display:flex;"><span><span style=""></span><span style="color:#007020;font-weight:bold">ENTRYPOINT</span> [<span style="color:#4070a0">&#34;/usr/bin/sync.pl&#34;</span>]<span style="">
</span></span></span></code></pre></div><p>To get rid of the Docker container, I translated the <code>sync.pl</code> file into a Nix
expression that writes the <code>sync.pl</code> Perl script to the Nix store:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ pkgs }:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># For string literal escaping rules (&#39;&#39;${), see:</span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># https://nix.dev/manual/nix/2.26/language/string-literals#string-literals</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># For writers.writePerlBin, see https://wiki.nixos.org/wiki/Nix-writers</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>pkgs<span style="color:#666">.</span>writers<span style="color:#666">.</span>writePerlBin <span style="color:#4070a0">&#34;syncpl&#34;</span> { libraries <span style="color:#666">=</span> []; } <span style="color:#4070a0">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0"># This script is run via ssh from dornröschen.
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">use strict;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">use warnings;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">use Data::Dumper;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">if (my ($destination) = ($ENV{SSH_ORIGINAL_COMMAND} =~ /^([a-z0-9.]+)$/)) {
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    print STDERR &#34;rsync version: &#34; . `</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>rsync<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/rsync --version` . &#34;\n\n&#34;;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    my @rsync = (
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;</span><span style="color:#70a0d0">${</span>pkgs<span style="color:#666">.</span>rsync<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/rsync&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;-e&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;ssh&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;--max-delete=-1&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;--verbose&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;--stats&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        # Intentionally not setting -X for my data sync,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        # where there are no full system backups; mostly media files.
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;-ax&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;--ignore-existing&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;--omit-dir-times&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;/srv/data/&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">        &#34;</span><span style="color:#4070a0;font-weight:bold">&#39;&#39;$</span><span style="color:#4070a0">{destination}:/&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    );
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    print STDERR &#34;running: &#34; . Dumper(\@rsync) . &#34;\n&#34;;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    exec @rsync;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">} else {
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">    print STDERR &#34;Could not parse SSH_ORIGINAL_COMMAND.\n&#34;;
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">}
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0">&#39;&#39;</span>
</span></span></code></pre></div><p>I can then reference this file by importing it in my <code>configuration.nix</code> and
pointing it to the <code>pkgs</code> expression of my NixOS configuration:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ modulesPath<span style="color:#666">,</span> lib<span style="color:#666">,</span> pkgs<span style="color:#666">,</span> <span style="color:#666">...</span> }:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  syncpl <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> <span style="color:#235388">./syncpl.nix</span> { pkgs <span style="color:#666">=</span> pkgs; };
</span></span><span style="display:flex; background-color:#d8d8d8"><span><span style="color:#007020;font-weight:bold">in</span> {
</span></span><span style="display:flex;"><span>  imports <span style="color:#666">=</span> [ <span style="color:#235388">./hardware-configuration.nix</span> ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>root<span style="color:#666">.</span>openssh<span style="color:#666">.</span>authorizedKeys<span style="color:#666">.</span>keys <span style="color:#666">=</span> [
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#39;&#39;command=&#34;</span><span style="color:#70a0d0">${</span>syncpl<span style="color:#70a0d0">}</span><span style="color:#4070a0">/bin/syncpl&#34;,no-port-forwarding,no-X11-forwarding ssh-ed25519 AAAAC3Npublickey sync@dr&#39;&#39;</span>
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># For interactive usage (when debugging):</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> [ syncpl ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># …</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>This works, but is it the best approach? Here are some thoughts:</p>
<ul>
<li>By managing this script in a Nix expression, I can no longer use my editor’s
Perl support.
<ul>
<li>I could probably also keep <code>sync.pl</code> as a separate file and use string
interpolation in my Nix expression to inject an absolute path to the <code>rsync</code>
binary into the script.</li>
</ul>
</li>
<li>Another alternative would be to add a wrapper script to my Nix expression
which ensures that <code>$PATH</code> contains <code>rsync</code> and then the script wouldn’t need
an absolute path anymore.</li>
<li>For small glue scripts like this one, I consider it easier to manage the
contents “inline” in the Nix expression, because it means one fewer file in my
config directory.</li>
</ul>
<h3 id="flakes">N8. Sharing configs</h3>
<p>I want to configure all my NixOS systems such that my user settings are
identical everywhere.</p>
<p>To achieve that, I can extract parts of my <code>configuration.nix</code> into a
<a href="https://github.com/stapelberg/nix/blob/main/user-settings.nix"><code>user-settings.nix</code></a>
and then <a href="https://github.com/stapelberg/nix/blob/30cdd7db9e0ab4b7cc3a38b7953e1b7e1e238d75/flake.nix#L7">declare an accompanying
<code>flake.nix</code></a>
that provides this expression as an output.</p>
<p>After publishing these files in a git repository, I can reference said
repository in my <code>flake.nix</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  inputs <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    nixpkgs<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-25.05&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    stapelbergnix<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/nix&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  outputs <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      self<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      nixpkgs<span style="color:#666">,</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>	  stapelbergnix<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>    }:
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>      system <span style="color:#666">=</span> <span style="color:#4070a0">&#34;x86_64-linux&#34;</span>;
</span></span><span style="display:flex;"><span>      pkgs <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      nixosConfigurations<span style="color:#666">.</span>storage2 <span style="color:#666">=</span> nixpkgs<span style="color:#666">.</span>lib<span style="color:#666">.</span>nixosSystem {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> pkgs;
</span></span><span style="display:flex;"><span>        modules <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>          <span style="color:#235388">./configuration.nix</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>userSettings
</span></span><span style="display:flex; background-color:#d8d8d8"><span>          <span style="color:#60a0b0;font-style:italic"># Not on this machine; We have our own networking config:</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>          <span style="color:#60a0b0;font-style:italic"># stapelbergnix.lib.systemdNetwork</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>          <span style="color:#60a0b0;font-style:italic"># Use systemd-boot as bootloader</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>          stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>systemdBoot
</span></span><span style="display:flex;"><span>        ];
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      formatter<span style="color:#666">.</span><span style="color:#70a0d0">${</span>system<span style="color:#70a0d0">}</span> <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>nixfmt-tree;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Everything <a href="https://github.com/stapelberg/nix/blob/main/user-settings.nix">declared in the
<code>user-settings.nix</code></a>
can now be removed from <code>configuration.nix</code>!</p>
<h3 id="immich-nixos">N9. Trying immich!</h3>
<p>One of the motivating reasons for switching away from CoreOS/Flatcar was that I
couldn’t try Immich, so let’s give it a shot on NixOS:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  services<span style="color:#666">.</span>immich <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    host <span style="color:#666">=</span> <span style="color:#4070a0">&#34;10.0.0.252&#34;</span>;
</span></span><span style="display:flex;"><span>    port <span style="color:#666">=</span> <span style="color:#40a070">2283</span>;
</span></span><span style="display:flex;"><span>    openFirewall <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    mediaLocation <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/srv/immich&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Because /srv is a separate file system, we need to declare:</span>
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>services<span style="color:#666">.</span><span style="color:#4070a0">&#34;immich-server&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    unitConfig<span style="color:#666">.</span>RequiresMountsFor <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;/srv&#34;</span> ];
</span></span><span style="display:flex;"><span>    wantedBy <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;srv.mount&#34;</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>You can find the <a href="https://github.com/stapelberg/zkj-nas-tools/tree/master/_2025-07-nixos-nas-configs">full configuration directory on
GitHub</a>.</p>
<p>I am pretty happy with this NixOS setup! Previously (with CoreOS/Flatcar), I
could declaratively manage my base system, but had to manage tons of Docker
containers in addition. With NixOS, I can declaratively manage <em>everything</em> (or
as much as makes sense).</p>
<p>Custom configuration like my SSH+rsync-based backup infrastructure can be
expressed cleanly, in one place, and structured at the desired level of
abstraction/reuse.</p>
<p>If you’re considering managing at least one other system with NixOS, I would
recommend it! One of my follow-up projects is to convert storage3 (my other NAS
build) from Ubuntu Server to NixOS as well to cut down on manual
management. Being able to just copy the entire config to set up another system,
or try out an idea in a throwaway VM, is just such a nice workflow 🥰</p>
<p>…but if you have just a single system to manage, probably all of this is too
complicated.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[How I like to install NixOS (declaratively)]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/"/>
    <id>https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/</id>
    <published>2025-06-01T08:20:43+02:00</published>
    <content type="html"><![CDATA[<p>For one of my <a href="/posts/2023-10-25-my-all-flash-zfs-network-storage-build/#previously-coreos">network storage PC
builds</a>,
I was looking for an alternative to <a href="https://www.flatcar.org/">Flatcar Container
Linux</a> and tried out <a href="https://nixos.org/">NixOS</a> again
(after an almost 10 year break). There are many ways to install NixOS, and in
this article I will outline how I like to install NixOS on physical hardware or
virtual machines: over the network and fully declaratively.</p>
<h2 id="declarative">Introduction: Declarative?</h2>
<p>The term <a href="https://en.wikipedia.org/wiki/Declarative_programming">declarative</a>
means that you describe <em>what</em> should be accomplished, not how. For NixOS, that
means you declare what software you want your system to include (add to config
option
<a href="https://search.nixos.org/options?show=environment.systemPackages"><code>environment.systemPackages</code></a>,
or enable a module) instead of, say, running <code>apt install</code>.</p>
<p>A nice property of the declarative approach is that your system follows your
configuration, so by reverting a configuration change, you can cleanly revert
the change to the system as well.</p>
<p>I like to manage declarative configuration files under version control,
typically with Git.</p>
<p>When I originally set up my current network storage build, I chose CoreOS (later
Flatcar Container Linux) because it was an auto-updating base system with a
declarative <a href="https://cloud-init.io/">cloud-init</a> config.</p>
<h2 id="ways">Ways of installing NixOS</h2>
<h3 id="graphical-install">Graphical Installer: Only for Desktops</h3>
<p>The <a href="https://nixos.org/manual/nixos/stable/#ch-installation">NixOS manual’s “Installation”
section</a> describes a
graphical installer (“for desktop users”, based on the
<a href="https://en.wikipedia.org/wiki/Calamares_(software)">Calamares</a> system installer
and added in 2022) and a manual installer.</p>
<p>With the graphical installer, it’s easy to install NixOS to disk: just confirm
the defaults often enough and you’ll end up with a working system. But there are
some downsides:</p>
<ul>
<li>You need to manually enable SSH after the installation — locally, not via the
network.</li>
<li>The graphical installer generates an initial NixOS configuration for you, but
there is no way to inject your own initial NixOS configuration.</li>
</ul>
<p>The graphical installer is clearly not meant for remote installation or
automated installation.</p>
<h3 id="manual-install">Manual Installation</h3>
<p>The manual installer on the other hand is too manual for my taste: expand
“Example 2” and “Example 3” in <a href="https://nixos.org/manual/nixos/stable/#sec-installation-manual-summary">the NixOS manual’s Installation
summary</a>
section to get an impression. To be clear, the steps are very doable, but I
don’t want to install a system this way in a hurry. For one, manual procedures
are prone to mistakes under stress. And also, copy &amp; pasting commands
interactively is literally the opposite of writing declarative configuration
files.</p>
<h3 id="nixos-anywhere">Network Installation: nixos-anywhere</h3>
<p>Ideally, I would want to perform most of the installation from the comfort of my
own PC, meaning the installer must be usable over the network. Also, I want the
machine to come up with a working initial NixOS configuration immediately after
installation (no manual steps!).</p>
<p>Luckily, there is a (community-provided) solution:
<a href="https://github.com/nix-community/nixos-anywhere">nixos-anywhere</a>. You take care
of booting a NixOS installer, then run a single command and nixos-anywhere will
SSH into that installer, partition your disk(s) and install NixOS to
disk. Notably, nixos-anywhere is configured declaratively, so you can repeat
this step any time.</p>
<p>(I know that nixos-anywhere can even SSH into arbitrary systems and kexec-reboot
them into a NixOS installer, which is certainly a cool party trick, but I like
the approach of explicitly booting an installer better as it seems less risky
and more generally applicable/repeatable to me.)</p>
<h2 id="setup-nix">Setup: Installing Nix</h2>
<p>I want to use NixOS for one of my machines, but not (currently) on my main desktop PC.</p>
<p>Hence, I installed only the <code>nix</code> tool (for building, even without running
NixOS) on Arch Linux:</p>
<pre tabindex="0"><code>% sudo pacman -S nix
% sudo groupadd -r nixbld
% for n in $(seq 1 24); do sudo useradd -c &#34;Nix build user $n&#34; \
    -d /var/empty -g nixbld -G nixbld -M -N -r -s &#34;$(which nologin)&#34; \
    nixbld$n; done
% sudo systemctl enable --now nix-daemon.socket
</code></pre><p>Now, running <code>nix-shell -p hello</code> should drop you in a new shell in which the
GNU hello package is installed:</p>
<pre tabindex="0"><code>% export NIX_PATH=nixpkgs=channel:nixos-25.05
% nix-shell -p hello
hello

[nix-shell:/tmp]$ hello
Hello, world!
</code></pre><p>By the way, the <a href="https://wiki.archlinux.org/title/Nix">Nix page on the Arch Linux
wiki</a> explains how to use nix to install
packages, but that’s not what I am interested in: I only want to remotely manage
NixOS systems.</p>
<h2 id="own-installer">Building your own installer</h2>
<p>Previously, I said “you take care of booting a NixOS installer”, and that’s easy
enough: write the ISO image to a USB stick and boot your machine from it (or
select the ISO and boot your VM).</p>
<p>But before we can log in remotely via SSH, we need to manually set a password. I
also need to SSH with the <code>TERM=xterm</code> environment variable because the termcap
file of rxvt-unicode (my preferred terminal) is not included in the default
NixOS installer environment. Similarly, my configured locales do not work and my
preferred shell (Zsh) is not available.</p>
<p>Wouldn’t it be much nicer if the installer was pre-configured with a convenient
environment?</p>
<p>With other Linux distributions, like Debian, Fedora or Arch Linux, I wouldn’t
attempt to re-build an official installer ISO image. I’m sure their processes
and tooling work well, but I am also sure it’s one extra thing I would need to
learn, debug and maintain.</p>
<p>But building a NixOS installer is <em>very similar</em> to configuring a regular NixOS
system: same configuration, same build tool. The procedure is documented in the
<a href="https://wiki.nixos.org/wiki/Creating_a_NixOS_live_CD">official NixOS wiki</a>.</p>
<p>I copied the customizations I would typically put into <code>configuration.nix</code>,
imported the <code>installation-cd-minimal.nix</code> module from <code>nixpkgs</code> and put the
result in the <code>iso.nix</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ config<span style="color:#666">,</span> pkgs<span style="color:#666">,</span> <span style="color:#666">...</span> }:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  imports <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#235388">&lt;nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#235388">&lt;nixpkgs/nixos/modules/installer/cd-dvd/channel.nix&gt;</span>
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  i18n<span style="color:#666">.</span>supportedLocales <span style="color:#666">=</span> [
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#34;en_DK.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#34;de_DE.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#34;de_CH.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    <span style="color:#4070a0">&#34;en_US.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  i18n<span style="color:#666">.</span>defaultLocale <span style="color:#666">=</span> <span style="color:#4070a0">&#34;en_US.UTF-8&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  security<span style="color:#666">.</span>sudo<span style="color:#666">.</span>wheelNeedsPassword <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>michael <span style="color:#666">=</span> {
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    openssh<span style="color:#666">.</span>authorizedKeys<span style="color:#666">.</span>keys <span style="color:#666">=</span> [
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      <span style="color:#4070a0">&#34;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5secret&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>      <span style="color:#4070a0">&#34;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5key&#34;</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    isNormalUser <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;Michael Stapelberg&#34;</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    extraGroups <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;wheel&#34;</span> ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    initialPassword <span style="color:#666">=</span> <span style="color:#4070a0">&#34;SGZ3odMZIesxTuh2Y2pUaJA&#34;</span>;  <span style="color:#60a0b0;font-style:italic"># random for this post</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    shell <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>zsh;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    packages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  };
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    git  <span style="color:#60a0b0;font-style:italic"># for checking out github.com/stapelberg/configfiles</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    rsync
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    zsh
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    vim
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    emacs
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    wget
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    curl
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    rxvt-unicode  <span style="color:#60a0b0;font-style:italic"># for terminfo</span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>    lshw
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  ];
</span></span><span style="display:flex; background-color:#d8d8d8"><span>
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  programs<span style="color:#666">.</span>zsh<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex; background-color:#d8d8d8"><span>  services<span style="color:#666">.</span>openssh<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># This value determines the NixOS release from which the default</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># settings for stateful data, like file locations and database versions</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># on your system were taken. It‘s perfectly fine and recommended to leave</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># this value at the release version of the first install of this system.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Before changing this value read the documentation for this option</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).</span>
</span></span><span style="display:flex;"><span>  system<span style="color:#666">.</span>stateVersion <span style="color:#666">=</span> <span style="color:#4070a0">&#34;25.05&#34;</span>; <span style="color:#60a0b0;font-style:italic"># Did you read the comment?</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>To build the ISO image, I set the <code>NIX_PATH</code> environment variable to point <a href="https://manpages.debian.org/nix-build.1"><code>nix-build(1)</code></a>
 to the <code>iso.nix</code> file and to select the
upstream channel for NixOS 25.05:</p>
<pre tabindex="0"><code>% export NIX_PATH=nixos-config=$PWD/iso.nix:nixpkgs=channel:nixos-25.05
% nix-build &#39;&lt;nixpkgs/nixos&gt;&#39; -A config.system.build.isoImage
</code></pre><p>After about 1.5 minutes on my <a href="/posts/2025-05-15-my-2025-high-end-linux-pc/">2025 high-end Linux
PC</a>, the installer ISO can be
found in <code>result/iso/nixos-minimal-25.05.802216.55d1f923c480-x86_64-linux.iso</code>
(1.46 GB in size in my case).</p>
<h2 id="enabling-flakes">Enabling Nix Flakes</h2>
<p>Unfortunately, the nix project has not yet managed to enable the “experimental”
new command-line interface (CLI) by default yet, despite 5+ years of being
available, so we need to create a config file and enable the modern
<code>nix-command</code> interface:</p>
<pre tabindex="0"><code>% mkdir -p ~/.config/nix
% echo &#39;experimental-features = nix-command flakes&#39; &gt;&gt; ~/.config/nix/nix.conf
</code></pre><p>How can you tell old from new? The old commands are hyphenated (<code>nix-build</code>),
the new ones are separated by a blank space (<code>nix build</code>).</p>
<p>You’ll notice I also enabled <a href="https://wiki.nixos.org/wiki/Flakes">Nix flakes</a>,
which I use so that my nix builds are hermetic and pinned to a certain revision
of nixpkgs and any other nix modules I want to include in my build. I like to
compare flakes to version lock file in other programming environments: the idea
is that building the system in 5 months will yield the same result as it does
today.</p>
<p>To verify that flakes work, run <code>nix shell</code> (not <code>nix-shell</code>):</p>
<pre tabindex="0"><code>% nix shell nixpkgs#hello
/tmp 2 % hello
Hello, world!
</code></pre><h2 id="reinstall-steps">(Re-)Installation Steps</h2>
<p>For reference, here is the configuration I use to create a new VM for NixOS in
Proxmox. The most important setting is <code>bios=ovmf</code> (= UEFI boot, which is not
the default), so that I can use the same boot loader configuration on physical
machines as in VMs:</p>















<a href="https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/2025-05-17-proxmox-frigaten.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/2025-05-17-proxmox-frigaten_hu_6bf4477cb712a89f.jpg 2x,https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/2025-05-17-proxmox-frigaten_hu_4c049a51d6ef646f.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-06-01-nixos-installation-declarative/2025-05-17-proxmox-frigaten_hu_ad95b7d92750e4a1.jpg"
  alt="proxmox VM creation dialog screenshot" title="proxmox VM creation dialog screenshot"
  width="600"
  height="455"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>Before we can boot our (unsigned) installer, we need to enter the UEFI setup and
disable Secure Boot. Note that Proxmox enables Secure Boot by default, for
example.</p>
<p>Then, boot the custom installer ISO on the target system, and ensure <code>ssh michael@nixos.lan</code> works without prompting for a password.</p>
<p>Declare a <code>flake.nix</code> with the following content:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  inputs <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    nixpkgs<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-25.05&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    disko<span style="color:#666">.</span>url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nix-community/disko&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#60a0b0;font-style:italic"># Use the same version as nixpkgs</span>
</span></span><span style="display:flex;"><span>    disko<span style="color:#666">.</span>inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  outputs <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      nixpkgs<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      disko<span style="color:#666">,</span>
</span></span><span style="display:flex;"><span>      <span style="color:#666">...</span>
</span></span><span style="display:flex;"><span>    }:
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">let</span>
</span></span><span style="display:flex;"><span>      system <span style="color:#666">=</span> <span style="color:#4070a0">&#34;x86_64-linux&#34;</span>;
</span></span><span style="display:flex;"><span>      pkgs <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    <span style="color:#007020;font-weight:bold">in</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      nixosConfigurations<span style="color:#666">.</span>zammadn <span style="color:#666">=</span> nixpkgs<span style="color:#666">.</span>lib<span style="color:#666">.</span>nixosSystem {
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> system;
</span></span><span style="display:flex;"><span>        <span style="color:#007020;font-weight:bold">inherit</span> pkgs;
</span></span><span style="display:flex;"><span>        modules <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>          disko<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>disko
</span></span><span style="display:flex;"><span>          <span style="color:#235388">./configuration.nix</span>
</span></span><span style="display:flex;"><span>        ];
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      formatter<span style="color:#666">.</span><span style="color:#70a0d0">${</span>system<span style="color:#70a0d0">}</span> <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>nixfmt-tree;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Declare your disk config in <code>disk-config.nix</code>:</p>
<details>
<summary>disk-config.nix</summary>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ lib<span style="color:#666">,</span> <span style="color:#666">...</span> }:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  disko<span style="color:#666">.</span>devices <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    disk <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      main <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>        device <span style="color:#666">=</span> lib<span style="color:#666">.</span>mkDefault <span style="color:#4070a0">&#34;/dev/sda&#34;</span>;
</span></span><span style="display:flex;"><span>        type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;disk&#34;</span>;
</span></span><span style="display:flex;"><span>        content <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>          type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;gpt&#34;</span>;
</span></span><span style="display:flex;"><span>          partitions <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>            ESP <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>              type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;EF00&#34;</span>;
</span></span><span style="display:flex;"><span>              size <span style="color:#666">=</span> <span style="color:#4070a0">&#34;500M&#34;</span>;
</span></span><span style="display:flex;"><span>              content <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>                type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;filesystem&#34;</span>;
</span></span><span style="display:flex;"><span>                format <span style="color:#666">=</span> <span style="color:#4070a0">&#34;vfat&#34;</span>;
</span></span><span style="display:flex;"><span>                mountpoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/boot&#34;</span>;
</span></span><span style="display:flex;"><span>                mountOptions <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;umask=0077&#34;</span> ];
</span></span><span style="display:flex;"><span>              };
</span></span><span style="display:flex;"><span>            };
</span></span><span style="display:flex;"><span>            root <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>              size <span style="color:#666">=</span> <span style="color:#4070a0">&#34;100%&#34;</span>;
</span></span><span style="display:flex;"><span>              content <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>                type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;filesystem&#34;</span>;
</span></span><span style="display:flex;"><span>                format <span style="color:#666">=</span> <span style="color:#4070a0">&#34;ext4&#34;</span>;
</span></span><span style="display:flex;"><span>                mountpoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/&#34;</span>;
</span></span><span style="display:flex;"><span>              };
</span></span><span style="display:flex;"><span>            };
</span></span><span style="display:flex;"><span>          };
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></details>
<p>Declare your desired NixOS config in <code>configuration.nix</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ modulesPath<span style="color:#666">,</span> lib<span style="color:#666">,</span> pkgs<span style="color:#666">,</span> <span style="color:#666">...</span> }:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  imports <span style="color:#666">=</span>
</span></span><span style="display:flex;"><span>    [
</span></span><span style="display:flex;"><span>      (modulesPath <span style="color:#666">+</span> <span style="color:#4070a0">&#34;/installer/scan/not-detected.nix&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#235388">./hardware-configuration.nix</span>
</span></span><span style="display:flex;"><span>      <span style="color:#235388">./disk-config.nix</span>
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Adding michael as trusted user means</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># we can upgrade the system via SSH (see Makefile).</span>
</span></span><span style="display:flex;"><span>  nix<span style="color:#666">.</span>settings<span style="color:#666">.</span>trusted-users <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;michael&#34;</span> <span style="color:#4070a0">&#34;root&#34;</span> ];
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Clean the Nix store every week.</span>
</span></span><span style="display:flex;"><span>  nix<span style="color:#666">.</span>gc <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    automatic <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    dates <span style="color:#666">=</span> <span style="color:#4070a0">&#34;weekly&#34;</span>;
</span></span><span style="display:flex;"><span>    options <span style="color:#666">=</span> <span style="color:#4070a0">&#34;--delete-older-than 7d&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  boot<span style="color:#666">.</span>loader<span style="color:#666">.</span>systemd-boot <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    configurationLimit <span style="color:#666">=</span> <span style="color:#40a070">10</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  boot<span style="color:#666">.</span>loader<span style="color:#666">.</span>efi<span style="color:#666">.</span>canTouchEfiVariables <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  networking<span style="color:#666">.</span>hostName <span style="color:#666">=</span> <span style="color:#4070a0">&#34;zammadn&#34;</span>;
</span></span><span style="display:flex;"><span>  time<span style="color:#666">.</span>timeZone <span style="color:#666">=</span> <span style="color:#4070a0">&#34;Europe/Zurich&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Use systemd for networking</span>
</span></span><span style="display:flex;"><span>  services<span style="color:#666">.</span>resolved<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>  networking<span style="color:#666">.</span>useDHCP <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>networks<span style="color:#666">.</span><span style="color:#4070a0">&#34;10-e&#34;</span> <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    matchConfig<span style="color:#666">.</span>Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;e*&#34;</span>;  <span style="color:#60a0b0;font-style:italic"># enp9s0 (10G) or enp8s0 (1G)</span>
</span></span><span style="display:flex;"><span>    networkConfig <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>      IPv6AcceptRA <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>      DHCP <span style="color:#666">=</span> <span style="color:#4070a0">&#34;yes&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  i18n<span style="color:#666">.</span>supportedLocales <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#34;en_DK.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#34;de_DE.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#34;de_CH.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#4070a0">&#34;en_US.UTF-8/UTF-8&#34;</span>
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>  i18n<span style="color:#666">.</span>defaultLocale <span style="color:#666">=</span> <span style="color:#4070a0">&#34;en_US.UTF-8&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>mutableUsers <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>  security<span style="color:#666">.</span>sudo<span style="color:#666">.</span>wheelNeedsPassword <span style="color:#666">=</span> <span style="color:#60add5">false</span>;
</span></span><span style="display:flex;"><span>  users<span style="color:#666">.</span>users<span style="color:#666">.</span>michael <span style="color:#666">=</span> {
</span></span><span style="display:flex;"><span>    openssh<span style="color:#666">.</span>authorizedKeys<span style="color:#666">.</span>keys <span style="color:#666">=</span> [
</span></span><span style="display:flex;"><span>      <span style="color:#4070a0">&#34;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5secret&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#4070a0">&#34;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5key&#34;</span>
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    isNormalUser <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>    description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;Michael Stapelberg&#34;</span>;
</span></span><span style="display:flex;"><span>    extraGroups <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;networkmanager&#34;</span> <span style="color:#4070a0">&#34;wheel&#34;</span> ];
</span></span><span style="display:flex;"><span>    initialPassword <span style="color:#666">=</span> <span style="color:#4070a0">&#34;install&#34;</span>;  <span style="color:#60a0b0;font-style:italic"># TODO: change!</span>
</span></span><span style="display:flex;"><span>    shell <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>zsh;
</span></span><span style="display:flex;"><span>    packages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [
</span></span><span style="display:flex;"><span>    git  <span style="color:#60a0b0;font-style:italic"># for checking out github.com/stapelberg/configfiles</span>
</span></span><span style="display:flex;"><span>    rsync
</span></span><span style="display:flex;"><span>    zsh
</span></span><span style="display:flex;"><span>    vim
</span></span><span style="display:flex;"><span>    emacs
</span></span><span style="display:flex;"><span>    wget
</span></span><span style="display:flex;"><span>    curl
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  programs<span style="color:#666">.</span>zsh<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  services<span style="color:#666">.</span>openssh<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># This value determines the NixOS release from which the default</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># settings for stateful data, like file locations and database versions</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># on your system were taken. It‘s perfectly fine and recommended to leave</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># this value at the release version of the first install of this system.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># Before changing this value read the documentation for this option</span>
</span></span><span style="display:flex;"><span>  <span style="color:#60a0b0;font-style:italic"># (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).</span>
</span></span><span style="display:flex;"><span>  system<span style="color:#666">.</span>stateVersion <span style="color:#666">=</span> <span style="color:#4070a0">&#34;25.05&#34;</span>; <span style="color:#60a0b0;font-style:italic"># Did you read the comment?</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>…and lock it:</p>
<pre tabindex="0"><code>% nix flake lock
</code></pre><ol start="3">
<li>Using nixos-anywhere, fetch the hardware-configuration.nix from the installer and install NixOS to disk:</li>
</ol>
<pre tabindex="0"><code>% nix run github:nix-community/nixos-anywhere -- \
  --flake .#zammadn \
  --generate-hardware-config nixos-generate-config ./hardware-configuration.nix \
  --target-host michael@nixos.lan
</code></pre><p>After about one minute, my VM was installed and rebooted!</p>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><p><strong>Tip:</strong> Last month, I had to temporarily pin to the latest released version
(1.9.0) because of <a href="https://github.com/nix-community/nixos-anywhere/issues/510">issue
nixos-anywhere#510</a>
like so:</p>
<pre tabindex="0"><code>% nix run github:nix-community/nixos-anywhere/1.9.0 -- \
  […same as above…]
</code></pre></div>
  </div>
</aside>

<details>
<summary>Full <code>nixos-anywhere</code> installation transcript, if you’re curious</summary>
<pre tabindex="0"><code>% nix run github:nix-community/nixos-anywhere -- \      
  --flake .#wiki \                                                                 
  --generate-hardware-config nixos-generate-config ./hardware-configuration.nix \
  --target-host michael@10.25.0.87                                               
Warning: Identity file /tmp/tmp.BT4E7i6eqJ/nixos-anywhere not accessible: No such file or directory.
### Uploading install SSH keys ###
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: &#34;/tmp/tmp.BT4E7i6eqJ/nixos-anywhere.pub&#34;
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.

Number of key(s) added: 1

Now try logging into the machine, with: &#34;ssh -i /tmp/tmp.BT4E7i6eqJ/nixos-anywhere -o &#39;IdentitiesOnly=no&#39; -o &#39;ConnectTimeout=10&#39; -o &#39;IdentitiesOnly=yes&#39; -o &#39;UserKnownHostsFile=/dev/null&#39; -o &#39;StrictHostKeyChecking=no&#39; &#39;michael@10.25.0.87&#39;&#34;
and check to make sure that only the key(s) you wanted were added.

### Gathering machine facts ###
Pseudo-terminal will not be allocated because stdin is not a terminal.
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.
### Generating hardware-configuration.nix using nixos-generate-config ###
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.
~/machines/wiki ~/machines/wiki
~/machines/wiki
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.
warning: Git tree &#39;/home/michael/machines&#39; is dirty
warning: Git tree &#39;/home/michael/machines&#39; is dirty
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.
Connection to 10.25.0.87 closed.
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.
### Formatting hard drive with disko ###
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.
umount: /mnt: not mounted
++ realpath /dev/sda
+ disk=/dev/sda
+ lsblk -a -f
NAME   FSTYPE   FSVER            LABEL                      UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
loop0  squashfs 4.0                                                                                    0   100% /nix/.ro-store
loop1                                                                                                           
loop2                                                                                                           
loop3                                                                                                           
loop4                                                                                                           
loop5                                                                                                           
loop6                                                                                                           
loop7                                                                                                           
sda                                                                                                             
├─sda1 vfat     FAT16                                       83DA-E750                                           
└─sda2 ext4     1.0                                         b136d6fd-d060-4b61-90fb-a8c1f9492f6e                
sr0    iso9660  Joliet Extension nixos-minimal-25.05-x86_64 1980-01-01-00-00-00-00                     0   100% /iso
+ lsblk --output-all --json
++ dirname /nix/store/fpwn44vygjj6bfn8s1jj9p8yh6jhfxni-disk-deactivate/disk-deactivate
+ bash -x
+ jq -r -f /nix/store/fpwn44vygjj6bfn8s1jj9p8yh6jhfxni-disk-deactivate/zfs-swap-deactivate.jq
+ lsblk --output-all --json
+ bash -x
++ dirname /nix/store/fpwn44vygjj6bfn8s1jj9p8yh6jhfxni-disk-deactivate/disk-deactivate
+ jq -r --arg disk_to_clear /dev/sda -f /nix/store/fpwn44vygjj6bfn8s1jj9p8yh6jhfxni-disk-deactivate/disk-deactivate.jq
+ set -fu
+ wipefs --all -f /dev/sda1
/dev/sda1: 8 bytes were erased at offset 0x00000036 (vfat): 46 41 54 31 36 20 20 20
/dev/sda1: 1 byte was erased at offset 0x00000000 (vfat): eb
/dev/sda1: 2 bytes were erased at offset 0x000001fe (vfat): 55 aa
+ wipefs --all -f /dev/sda2
/dev/sda2: 2 bytes were erased at offset 0x00000438 (ext4): 53 ef
++ type zdb
++ zdb -l /dev/sda
++ sed -nr &#39;s/ +name: &#39;\&#39;&#39;(.*)&#39;\&#39;&#39;/\1/p&#39;
+ zpool=
+ [[ -n &#39;&#39; ]]
+ unset zpool
++ lsblk /dev/sda -l -p -o type,name
++ awk &#39;match($1,&#34;raid.*&#34;) {print $2}&#39;
+ md_dev=
+ [[ -n &#39;&#39; ]]
+ wipefs --all -f /dev/sda
/dev/sda: 8 bytes were erased at offset 0x00000200 (gpt): 45 46 49 20 50 41 52 54
/dev/sda: 8 bytes were erased at offset 0xc7ffffe00 (gpt): 45 46 49 20 50 41 52 54
/dev/sda: 2 bytes were erased at offset 0x000001fe (PMBR): 55 aa
+ dd if=/dev/zero of=/dev/sda bs=440 count=1
1+0 records in
1+0 records out
440 bytes copied, 0.000306454 s, 1.4 MB/s
+ lsblk -a -f
NAME  FSTYPE   FSVER            LABEL                      UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
loop0 squashfs 4.0                                                                                    0   100% /nix/.ro-store
loop1                                                                                                          
loop2                                                                                                          
loop3                                                                                                          
loop4                                                                                                          
loop5                                                                                                          
loop6                                                                                                          
loop7                                                                                                          
sda                                                                                                            
sr0   iso9660  Joliet Extension nixos-minimal-25.05-x86_64 1980-01-01-00-00-00-00                     0   100% /iso
++ mktemp -d
+ disko_devices_dir=/tmp/tmp.YvWbz8ZKHk
+ trap &#39;rm -rf &#34;$disko_devices_dir&#34;&#39; EXIT
+ mkdir -p /tmp/tmp.YvWbz8ZKHk
+ destroy=1
+ device=/dev/sda
+ imageName=main
+ imageSize=2G
+ name=main
+ type=disk
+ device=/dev/sda
+ efiGptPartitionFirst=1
+ type=gpt
+ blkid /dev/sda
+ sgdisk --clear /dev/sda
Creating new GPT entries in memory.
The operation has completed successfully.
+ sgdisk --align-end --new=1:0:+500M --partition-guid=1:R --change-name=1:disk-main-ESP --typecode=1:EF00 /dev/sda
The operation has completed successfully.
+ partprobe /dev/sda
+ udevadm trigger --subsystem-match=block
+ udevadm settle --timeout 120
+ sgdisk --align-end --new=2:0:-0 --partition-guid=2:R --change-name=2:disk-main-root --typecode=2:8300 /dev/sda
The operation has completed successfully.
+ partprobe /dev/sda
+ udevadm trigger --subsystem-match=block
+ udevadm settle --timeout 120
+ device=/dev/disk/by-partlabel/disk-main-ESP
+ extraArgs=()
+ declare -a extraArgs
+ format=vfat
+ mountOptions=(&#39;umask=0077&#39;)
+ declare -a mountOptions
+ mountpoint=/boot
+ type=filesystem
+ blkid /dev/disk/by-partlabel/disk-main-ESP
+ grep -q TYPE=
+ mkfs.vfat /dev/disk/by-partlabel/disk-main-ESP
mkfs.fat 4.2 (2021-01-31)
+ device=/dev/disk/by-partlabel/disk-main-root
+ extraArgs=()
+ declare -a extraArgs
+ format=ext4
+ mountOptions=(&#39;defaults&#39;)
+ declare -a mountOptions
+ mountpoint=/
+ type=filesystem
+ blkid /dev/disk/by-partlabel/disk-main-root
+ grep -q TYPE=
+ mkfs.ext4 /dev/disk/by-partlabel/disk-main-root
mke2fs 1.47.2 (1-Jan-2025)
Discarding device blocks: done                            
Creating filesystem with 12978688 4k blocks and 3245872 inodes
Filesystem UUID: 57975635-9165-4895-93ea-72053294a185
Superblock backups stored on blocks: 
	32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
	4096000, 7962624, 11239424

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (65536 blocks): done
Writing superblocks and filesystem accounting information: done   

+ set -efux
+ destroy=1
+ device=/dev/sda
+ imageName=main
+ imageSize=2G
+ name=main
+ type=disk
+ device=/dev/sda
+ efiGptPartitionFirst=1
+ type=gpt
+ destroy=1
+ device=/dev/sda
+ imageName=main
+ imageSize=2G
+ name=main
+ type=disk
+ device=/dev/sda
+ efiGptPartitionFirst=1
+ type=gpt
+ device=/dev/disk/by-partlabel/disk-main-root
+ extraArgs=()
+ declare -a extraArgs
+ format=ext4
+ mountOptions=(&#39;defaults&#39;)
+ declare -a mountOptions
+ mountpoint=/
+ type=filesystem
+ findmnt /dev/disk/by-partlabel/disk-main-root /mnt/
+ mount /dev/disk/by-partlabel/disk-main-root /mnt/ -t ext4 -o defaults -o X-mount.mkdir
+ destroy=1
+ device=/dev/sda
+ imageName=main
+ imageSize=2G
+ name=main
+ type=disk
+ device=/dev/sda
+ efiGptPartitionFirst=1
+ type=gpt
+ device=/dev/disk/by-partlabel/disk-main-ESP
+ extraArgs=()
+ declare -a extraArgs
+ format=vfat
+ mountOptions=(&#39;umask=0077&#39;)
+ declare -a mountOptions
+ mountpoint=/boot
+ type=filesystem
+ findmnt /dev/disk/by-partlabel/disk-main-ESP /mnt/boot
+ mount /dev/disk/by-partlabel/disk-main-ESP /mnt/boot -t vfat -o umask=0077 -o X-mount.mkdir
+ rm -rf /tmp/tmp.YvWbz8ZKHk
Connection to 10.25.0.87 closed.
### Uploading the system closure ###
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.
copying path &#39;/nix/store/k64q0bbrf8kxvcx1zlvhphcshzqn2xg6-acl-2.3.2-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3rnsaxgfam1df8zx6lgcjbzrxhcg1ibg-acl-2.3.2-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ircpdw4nslfzmlpds59pn9qlak8gn81r-attr-2.5.2-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mrxc0jlwhw95lgzphd78s6w33whhkfql-attr-2.5.2-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qm7ybllh3nrg3sfllh7n2f6llrwbal58-bash-completion-2.16.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3frg3li12mwq7g4fpmgkjv43x5bqad7d-bash-interactive-5.2p37-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/88cs6k2j021mh2ir1dzsl6m8vqgydyiw-bash-interactive-5.2p37-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/s3zz5nasd7qr894a8jrp6fy52pdrz2f1-bash-interactive-5.2p37-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/azy34jpyn6sskplqzpbcs6wgrajkkqy0-bind-9.20.9-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g28l15mbdbig59n102zd0ardsfisiw32-binfmt_nixos.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/k5r6p8gvf18l9dd9kq1r22ddf7ykfim2-build-vms.nix&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dxhfmzg1dhyag26r70xns91f8078vq82-alsa-firmware-1.2.4-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/d46ilc6gzd1piyjfm9sbrl7pq3b3k0hg-busybox-1.36.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yq76x7ha0rv3mn9vxrar53zlkmxlkdas-bzip2-1.0.8-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9wvnmd2mr2qr8civvznnfi6s773fjvfh-coreutils-full-9.7-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/innps8d9bl9jikd3nsq8bd5irgrlay6f-curl-8.13.0-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6yiazrx84xj8m8xqal238g3mzglvwid2-dbus-1.14.10-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4bys54210khcipi91d6ivfz4g5qx33kh-dbus-1.14.10-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zh5iazbs69x4irfdml5fzbh9nm05spgb-dejavu-fonts-minimal-2.37&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/55wbmnssa48mi96pbaihz9wr4a44vxsd-diffutils-3.12-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qxk9122p34qwivq20k154jflwxjjjxb3-dns-root-data-2025-04-14&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/nqzrl9jhqs4cdxk6bpx54wfwi14x470f-e2fsprogs-1.47.2-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/b0qk1rsi8w675h1514l90p55iacswy5i-e2fsprogs-1.47.2-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/33ka30bacgl8nm7g7rcf2lz4n3hpa791-etc-bash_logout&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/0sl4azq1vls6f7lfjpjgpn9gpmwxh3a5-etc-fuse.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m4xpifh68ayw6pn7imyiah5q8i03ibzx-etc-host.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qdhp1g45sqkz5limyh3pr54jr0vzrhyg-etc-lsb-release&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/61z4n7pkrbhhnahpvndvpc2iln06kcl3-etc-lvm-lvm.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/l75amyv04p2ldiz6iv5cmlm03m417yfd-etc-man_db.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yb8n9alg0flvl93842savj8fk880a5s8-etc-modprobe.d-nixos.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/v5zxfkpwma99vvbnwh7pv3qjvv09q9mf-etc-netgroup&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cb8dadanahyrgyh4yrd02j1pn4ipg3h1-etc-nscd.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ixzrf8qqzdp889kffwhi5l1i5b906wm2-etc-nsswitch.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mw3qf7jsf2cr6bdh2dwhsfaj46ddvdj4-etc-systemd-coredump.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ysxak9fplmg53wd40z86bacviss02wxj-etc-resolvconf.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/w0027gbp2ppnzpakjqdsj04k1qnv8xai-etc-systemd-journald.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/73qr9mvgrkk9g351h1560rqblpv8bkli-etc-systemd-logind.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/w8r9xylr9a1bd2glfp4zdwxiq8z2bhxb-etc-systemd-networkd.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/i1v3l8mmgr1zni58zsdgrf19xz5wpihs-etc-systemd-oomd.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/42mdjpbx4dablvbkj4l75xfjjlhpyb7a-etc-systemd-resolved.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g2d3zjbsa94jdqybcwbldzn3w98pwzhk-etc-systemd-sleep.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1wi887sd535dk4l4s0w7hp822fdys18j-etc-systemd-system-preset-00-nixos.preset&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/r4cjphi2kzkyvkc33y7ik3h8z1l5zs2q-etc-systemd-timesyncd.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/n5y58mvq44mibwxkzzjb646v0nck9psd-etc-systemd-user-preset-00-nixos.preset&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7d2j36mn359g17s2qaxsb7fjd2bm4s7p-etc-systemd-user.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ziyrzq721iziyhvlchvg4zllcdr0rbd4-etc-zprofile&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6zl92vca58p27i20dck95j27lvj5lv16-etc-zinputrc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y7y1v7l88mxkljbijs7nwzm1gcg9yrjw-extra-utils&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kkbfwys01v37rxcrahc79mzw7bqqg1ha-X-Restart-Triggers-systemd-journald&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/15k9rkd7sqzwliiax8zqmbk9sxbliqmd-X-Restart-Triggers-systemd-journald-&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/08c95zkcyr5d4gcb2nzldf6a5l791zsl-fc-10-nixos-rendering.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fnrpg6pljxzbwz5f2wbiayirb4z63rid-fc-52-nixos-default-fonts.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hx4rm1z8sjh6s433sfxfjjwapr1r2lnm-X-Reload-Triggers-systemd-resolved&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/045cq354ckg28php9gf0267sa4qgywj9-X-Restart-Triggers-systemd-timesyncd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xj6dycqkvs35yla01gd2mmrrpw1d1606-fc-53-nixos-reject-type1.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/c1l35xhz88v0hz3bfnzwi7k3pirk89gx-fc-53-no-bitmaps.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/izcym87m13m4nhjbxr2b2fp0r6wpl1s6-fontconfig-2.16.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/b5qqfs0s3fslirivph8niwdxh0r0qm4g-fc-cache&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yjab7vlimxzqpndjdqzann33i34x6pyy-findutils-4.10.0-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/sgxf74s67kbx0kx38hqjzpjbrygcnl81-fuse-2.9.9-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3p531g8jpnfjl6y0f4033g3g2f14s32y-gawk-5.3.2-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vp5ra8m1sg9p3xgnz3zd7mi5mp0vdy25-fuse-3.16.2-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ndir5b1ag9pk4dyrpvhiidaqqg1xjdqm-gawk-5.3.2-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6hqzbvz50bm87hcj4qfn51gh7arxj8a6-gcc-14.2.1.20250322-libgcc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7dfxlvdhr5g57b1v8gxwpa2gs7i9g3y5-git-2.49.0-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ikhb97s6a22dn21lhxlzhambsmisrvff-gnugrep-3.11-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hgx3ai0sm533zfd9iqi5nz5vwc50sprm-fc-00-nixos-cache.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/i65zra2i21y5khnsnvl0pvd5rkvw5qhl-gnused-4.9-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/10p1z2bqsw0c6r5c5f59yn4lnl82lqxi-gnutar-1.35-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/a9fcrsva5nw1y3nqdjfzva8cp4sj7l91-gzip-1.14-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/0i7mzq93m8p7253bxnh7ydahmjsjrabk-gzip-1.14-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/diprg8qwrk8zwx73bjnjzjvaccdq5z1g-hicolor-icon-theme-0.18&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/c7y25162xaplam12ysj17g5pwgs8vj99-hwdb.bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j4gc8fk7wazgn2hqnh0m8b12xx6m1n75-iana-etc-20250108&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dwv0wf3szv3ipgyyyrf1zxh4iqlckiip-inputrc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/nvz2hs89yjb8znxf7zw2y1rl8g0zc24g-intel2200BGFirmware-3.1-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8v0wnff8rpa64im6gkfwf702f0d13asb-iptables-1.8.11-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ib4za959rmvhyvhfn0p6y25szq9agzvv-X-Restart-Triggers-systemd-networkd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7vswj657kcfyz8g0i5lgm17k28nw9b6q-keymap&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1lcg48lg3yw873x21gybqzdmp06yqf0f-kmod-blacklist-31+20240202-2ubuntu8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m1arp7n5z5cqsv88l0gjazzfvkc8ia84-fontconfig-conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/q1f1r3hqs0h6gjkas71kzaafsnbipkp9-kmod-debian-aliases.conf-30+20230601-2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fanpm1fxx8x5wrizmddhqgqpxrw253bf-less-668-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qlbfg75i4wz6sb2ipzh4n1k0p8gp4wjp-lessconfig&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mhxn5kwnri3z9hdzi3x0980id65p0icn-lib.sh&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fsbyh73wsjl7gfl2k4rvdc6y02ixljmk-libcap-2.75-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5ja0hlyfnyvq1yyd2h8pzrmwwk9bgayy-libreelec-dvb-firmware-1.5.0-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mdf936r0ahj70lqqc09147msz4yxi3hb-libressl-4.0.0-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mddq2k6rmr77bz96j42y947wywcxin50-libcap-2.75-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yypqcvqhnv8y4zpicgxdigp3giq81gzb-libunistring-1.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ahfbv5byr6hiqfa2jl7pi4qh35ilvxzg-fontconfig-etc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6fmfvkxjq2q8hzvhmi5717i0zmwjkrpw-liburing-2.9&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/737acshv7jgp9jbg0cg9766m6izcwllh-link-units&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/303izw3zmxza3n01blxaa5a44abbqkkr-linux-6.12.30&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8pncaz101prqwhvcrdfx0pbmv4ayq5bf-linux-firmware-20250509-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hxrjrzngydk24ah8b5n8cl777n39y08b-linux-headers-6.12.7&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/c4inn6fkfc4flai72ym5470jp2va8b6c-linux-pam-1.6.1-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x4a9ksmwqbhirjxn82cddvnhqlxfgw8l-linux-headers-static-6.12.7&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hi41wm3spb6awigpdvkp1sqyj0gj67vf-linux-pam-1.6.1-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m97qnhb417rmaiwwlw8qz2nvimgbmhxj-local-cmds&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6yd58721msbknn6fs57w0j82v04vpzw6-locale.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7kdkx4y7lbb15lb2qksw0nzal23mkhjy-login.defs&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x4mjvy4h92qy7gzi3anp0xbsw9icn3qj-logrotate.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/li71ly6mmsc7m9rm1hl98m4ka508s52i-lvm2-2.03.31-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1jj2lq1kzys105rqq5n1a2r4v59arz43-mailcap-2.1.54&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qkvqycyhqc9g9vpyp446b5cx7hv1c5zi-man-db-2.13.0-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gkbc6nv3h0hsp06kqk0p6s9911c2a1gg-mounts.sh&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qdv28rq2xlj68lsgrar938dq38v2lh5b-multiuser.nix&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6nkqdqzpa75514lhglgnjs5k4dklw4sb-libidn2-2.3.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vqykgcs16rs0ny39wlqb2hihb19f5bc8-nano-8.4-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6c69fcc0583xx7mqc4avszsv8dj1glfb-ncurses-6.5-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/smpby3mgssbggz941499y9x9r35w8cbh-nix-2.28.3-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/k9chrrif685hvkiqkc3fgfib19v2mh2y-nix-2.28.3-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bik2ny1bj83jby10lvq912i9v5gzy8g3-nix-bash-completions-0.6.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/90asb028hphm9iqh2h0xk3c52j3117rf-nix-zsh-completions-0.5.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/lwhcdpa73h0p6z2hc8f5mqx6x03widq4-nixos-configuration-reference-manpage&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/0249ff3p72ggrd308l2yk9n700f95kir-nixos-manual-html&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/z8dgwwnab96n86v0fnr37mn107w26s1f-nixos-manual.desktop&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gj6hz9mj23v01yvq1nn5f655jrcky1qq-nixos-option.nix&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6fv8ayzjvgyl3rdhxp924zdhwvhz2iq6-nss-cacert-3.111&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/l7rjijvn6vx8njaf95vviw5krn3i9nnx-nss-cacert-3.111-p11kit&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/as6v2kmhaz3syhilzzi25p9mn0zi9y0b-other.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/d6kfv0rb15n92pi1jsjk65nd9264wja6-perl-5.40.0-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/v0r2ndk31k1lsj967qrywdwxb87zdil6-perl5.40.0-Digest-HMAC-1.04&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/l6b79dzj572yjifnwnrmjmf2r8qx1542-perl5.40.0-Encode-Locale-1.05&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mri94g6brszrzi5spdp3yjqig0dix246-perl5.40.0-FCGI-ProcManager-0.28&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vxmnihhgnkyd2yh1y6gsyrw7lzqyh0sn-perl5.40.0-File-Slurp-9999.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jvy29fslpki9ygmipnawxkacs0gdpwbg-perl5.40.0-HTML-TagCloud-0.38&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/187sf67ng5l08pirjv1hcnvvsx6bg6vi-perl5.40.0-Authen-SASL-2.1700&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/q6gp62h0h2z2lx3qh318crhikwc86m2y-perl5.40.0-HTML-Tagset-3.20&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1f3pkwqxmhglz59hdl9mizgaafrcxr2g-perl5.40.0-IO-HTML-1.004&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6insghd7kklnnilycdmbwl71l1gi9nkb-perl5.40.0-IO-Stringy-2.113&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cqa81jdkhwvkjnz810laxhd6faw8q917-perl5.40.0-JSON-4.10&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gvjb0301bm7lc20cbbp6q4mznb3k09j3-perl5.40.0-LWP-MediaTypes-6.04&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3fvhcxjgn3a4r6pkidwz9nd4cs84p6jv-perl5.40.0-Mozilla-CA-20230821&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pimqpkya3wybrpcm17zk298gpivhps5j-perl5.40.0-Test-Needs-0.002010&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wq3ij7g3r6jfkx61d3nbxrfmyw3f3bng-perl5.40.0-Test-RequiresInternet-0.05&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j5agsmr85pb3waxmzxn2m79yb1i7hhmh-perl5.40.0-TimeDate-2.33&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/lyr4v74c0vw9j77fvr0d6dribm1lmfsr-perl5.40.0-Try-Tiny-0.31&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1hb2dxywm239rfwgdrd55z090hb1zbg3-perl5.40.0-URI-5.21&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5vc5pjg9yqxkxk855il2anp6jm5gkpa3-perl5.40.0-libnet-3.15&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kqz08h7qzxq13n3r3dymsl3jafgxl60x-php.ini&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/97qlbk0b8y0xs2hpjs37rp3sq6bdh99w-perl5.40.0-Config-IniFiles-3.000003&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/nfwlyasnxxdbnpiziw2nixwkz9b5f7g3-publicsuffix-list-0-unstable-2025-03-12&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/30qhz45nwgfyns13ijq0nwrsjp8m7ypa-relaxedsandbox.nix&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5h63p4i2p25ba728pi4fr6vdcxa1227j-rt5677-firmware-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/w5f538hq8zh4cxpjs3lx4jdhr2p6wvq8-rtl8192su-unstable-2016-10-05-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xnfzahna7b6jb6m1vdczap4v103qmr6w-perl5.40.0-Test-Fatal-0.017&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rb472zb1d7245j44iwm7xsnn9xkhv28r-rtl8761b-firmware-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/blgz4vzk56rbajaavr6kg437zr7jcabp-perl5.40.0-HTTP-Date-6.06&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3zf0hlfxwam2pcpr28374plf3zwcbkr0-perl5.40.0-Net-HTTP-6.23&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5i759vgj25fdy680l9v0sjhjg65q0q4h-perl5.40.0-WWW-RobotRules-6.02&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zqzjsf740jc5jqrzidw7qzkrsrl95d2b-rxvt-unicode-unwrapped-9.31-terminfo&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6602zq9jmd3r4772ajw866nkzn6gk1j0-sandbox.nix&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rg5rf512szdxmnj9qal3wfdnpfsx38qi-setup-etc.pl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kw5mdls5m8iqzh620iwm6h42rjqcbj93-shadow-4.17.4-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mvaibwlc8b5gfj13b3za7g5408hgjgwn-sof-firmware-2025.01.1-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x51649mj5ppmj97qrgxwr0calf82m9a5-perl5.40.0-File-Listing-6.16&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/krfhnl4n5a9w201z5pzwgps9fgz8z5j5-perl5.40.0-HTTP-CookieJar-0.014&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wcf11ld95pf7h1sn6nglgmrizbjlcw2f-sound-theme-freedesktop-0.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2g8wdl6qgkpk9dhj9rir7zkf9nxnjqzw-source&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ids7wg1swihwhh17qbdbpmbdx67k5w21-ssh-root-provision.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2959xcdddldhls7wslkm7gv2xf5pki1x-strace-6.14-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/p7r0byvn43583rx7rvvy2pj44yv5c1jj-stub-ld-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/v9ibkbvwc03ni062gh3ml4s0mswq0zfs-sudoers&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xx25wf50ww3bci4dvhfj2mrgccdfinja-system-generators&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/k5k5dvfz26a0py2xljmhz9a08y42gkkv-system-shutdown&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/0iyxf2pmg0i16d4kxarqdfd3nqfa9mc5-systemd-257.5-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qyihkwbhd70ynz380whj3bsxk1d2lyc4-tzdata-2025b&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/q7pmljnijxmihsz0lsn90b7l2yvncvwm-udev-rules&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qzv2iqy6b9jl7x76pfcplqb81gs8sarx-unit--.slice&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5wap65qygkmwxnaykp2k00xbip0203ah-unit-dbus.socket&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/djhz08ld7cqvi36v4by31mr560lbbgdy-unit-fs.target&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yyjy3ni8amh8lmpgikv6qps1ygphhg9h-unit-fstrim.timer&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/68ymaa7yqz8b9c3m86awp9qrs3z5gmb9-unit-keys.target&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3mpivh2pqa1bbyp8h3n2wk8s0fvhp2rg-unit-local-fs.target&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3i90ba6lh4d8jd58kqgznxr53kzha657-unit-logrotate.timer&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/65pm1jd651q5891y7171sl2nsvnmh1a2-unit-multi-user.target&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m2chlkrf4dhjcnq50x6qnjlfvhz9c60s-unit-network-local-commands.service-disabled&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/c1b80rjkrfis8704c9xxwl8chg0kpxd2-unit-nix-daemon.socket&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fl6il46drw769y6z9h4b89yv1k55xps3-unit-nixos-fake-graphical-session.target&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/n4cwpsbmd30nhps87yic15rnxfvnlvaw-unit-phpfpm.target&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8zmflchf01g3wlj9j6csfnd47j0lgzcg-unit-post-resume.target&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/842zkhkx2aa0zy94qws3346dnd1cm3h6-unit-remote-fs.target&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9gkhxinv1884d1vy74rnkjd9vj2zn89p-unit-run-initramfs.mount&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4aiwrxc5i77s856dgx6b7yvqnxbq8x0g-unit-run-wrappers.mount&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gyxhzj5v8k01vwva1s476ny2zll2nvzm-unit-sysinit-reactivation.target&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/p1k14mysynvbwyclk1nfjyyvcnrv65bp-unit-system-phpfpm.slice&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/10wi26kk0cjrifnvdsyrl8w4987z4hsb-unit-system.slice&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8g5vq29riss8693g7syg8n0bj2d7vc9l-unit-systemd-journald-audit.socket&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g29nsjbhdlc1xzgl0a0cybqvy9mg895l-unit-systemd-networkd.socket&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5wcg3gl5qzna3qn53id02sghbzfqa67z-unit-user-.slice&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7sb1nkpf82nb5kj7qc4bbqkwj1l1mdv9-update-users-groups.pl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/l19w3rl6k8767i9znna0rfkjvl5cz4kg-urxvt-autocomplete-all-the-things-1.6.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ym3cf4rnxblhlpsxj2cd5wm8rp8pgfr7-urxvt-perls-2.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/na57lsanf2c453zdz1508wnzvbh9w4rg-urxvt-resize-font-2019-10-05&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/l8qxbnizariir6sncianl8q0i4a0zaya-urxvt-tabbedex-19.21&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/d8k53n8mmb8j1a6v4f3wvhhap8xwcssd-urxvt-theme-switch-unstable-2014-12-21&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3fgvp4zddvbkkyviq5sajbl7wc7lmx5q-user-generators&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/k7bynf83k39pk9x6012vjrd6fll2wdqh-useradd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wcrrwx3yvbvwa1hryjpgcbysdf8glnix-util-linux-2.41-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vq0m8mcigxkjfjdwrgzvizjam5vx669h-wireless-regdb-2025.02.20-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rqxaqpliqlygv3hw53j4j7s54qj5hjri-vconsole.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/za53jjhjl1xajv3y1zpjvr9mh4w0c1ay-xgcc-14.2.1.20250322-libgcc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/18w6fpxmn5px02bpfgk702bs9k7yj5ml-xorgproto-2024.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/l63r9kidyd8siydvr485g71fsql8s48b-xz-5.8.1-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3drnnkrsdfrqdrdg425wda83k79nlmwp-xz-5.8.1-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/lxbkfad3nbyfx3hsc1ajlvqs3s67li6x-zd1211-firmware-1.5-zstd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wysmwjrvwx7gk5w6dxd0d1jwjbjj350a-zsh-5.9-doc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4y2v97rjk4mic266vzbvmlxjnjnisnmm-zsh-5.9-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/af0jrnzsydq1i28vcnkpgp0110ac2cj3-zsh-5.9-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/184bcjcc97x3klsz63fy29ghznrzkipg-zstd-1.5.7-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cg9s562sa33k78m63njfn1rw47dp9z0i-glibc-2.40-66&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8syylmkvnn7lg2nar9fddpp5izb4gh56-attr-2.5.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/a6w0pard602b6j7508j5m95l8ji0qvn6-aws-c-common-0.10.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xy4jjgw87sbgwylm5kn047d9gkbhsr9x-bash-5.2p37&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7a8gf62bfl22k4gy2cd300h7cvqmn9yl-brotli-1.1.0-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6ycmjimp1h3z4xgf47jjxxmps9skbdw1-cpio-2.15&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/w762xfdg6qkyamncs8s33m182n45nmma-dav1d-1.5.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pyfpxwjw1a7fj5j7n2czlk4g7lvzhvhy-dosfstools-4.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2x51wvk10m9l014lyrfdskc3b360ifjp-ed-1.21.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/p9k7bd23v5yvmap9594f9x7hpvacdh32-expand-response-params&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j0bzxly2rvcym1zkhn393adiqcwn8np6-expat-2.7.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/719j8zd8g3pa5605b7a6w5csln323b1x-fribidi-1.0.16&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qlwqqqjdvska6nyjn91l9gkxjjw80a97-editline-1.17.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zrnqzhcvlpiycqbswl0w172y4bpn0lb4-bzip2-1.0.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fcyn0dqszgfysiasdmkv1jh3syncajay-gawk-5.3.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7c0v0kbrrdc2cqgisi78jdqxn73n3401-gcc-14.2.1.20250322-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qkzkz12l4q06lzbji0ifgynzrd44bpjs-gdbm-1.25-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/d9x4blp2xwsbamz8az3c54x7la08j6ln-giflib-5.2.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1abbyfv3bpxalfjfgpmwg8jcy931bf76-bzip2-1.0.8-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/303islqk386z1w2g1ngvxnkl4glfpgrs-glibc-2.40-66-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3mi59bgj22xx29dyss7jhmx3sgznd85m-acl-2.3.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zhpgx7kcf8ii2awhk1lz6p565vv27jv5-attr-2.5.2-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/w4hr24l1bfj07b56vm3zrp0rzxsd3537-aws-c-compression-0.3.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ifvslnvmvg3nb26yliprya6ja1kb5yaf-aws-c-sdkutils-0.2.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/26ddah1lva210rn57dzkan1dgjvj7dn4-aws-checksums-0.2.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/if83fp73ln7ksdnp1wkywvyv53b6fw3f-glibc-2.40-66-getent&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dwwc14ppzkl0yphcgsz25xvi24c9d1zm-gmp-6.3.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/c341wfmk7r827k691yp5ynjnv5014xqf-audit-disable&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rjlwg1dlbhkv2bhrq03m794xbhcwcgh6-audit-stop&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1191qk37q1bxyj43j0y1l534jvsckyma-acl-2.3.2-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/padpqlhkvnr56a5j4ma5mlfrp46ibg7g-container-init&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y7g9g1gfg1f6y3gm2h02i7hmjzv10f9q-dav1d-1.5.1-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/24gnm4vyck53sppsvlzcmknvz7jp8x0p-firewall-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y7ljc4ir2hkwkr7lhgm9xj5hw3kw8275-firewall-stop&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cab2yvnph1hfym998vdq0q4nr9zfndrs-gnum4-1.4.19&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j2v7jjnczkj7ra7jsgq6kv3242a1l52x-getent-glibc-2.40-66&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/clbb2cvigynr235ab5zgi18dyavznlk2-gnused-4.9&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wrxvqj822kz8746608lgns7h8mkpn79f-gnutar-1.35&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pl3wb7v54542kdaj79dms8r2caqbn0nv-gpm-unstable-2020-06-17&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/afhkqb5a94zlwjxigsnwsfwkf38h21dk-gzip-1.14&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/677sx4qrmnmgk83ynn0sw8hqgh439g6b-json-c-0.18&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4v64wga9rk0c919ip673j36g6ikx26ha-keyutils-1.6.3-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bkm4ppw3rpyndsvy5r18fjpngg2730ip-libICE-1.1.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/psjc7gv2314bxncywpvsg76gvbk2dn00-libXau-1.0.12&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/aq5b44b37zp5dfwz5330pxqm699gs4g3-isl-0.20&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hx0kbryivbs7qccnvpmr17y6x818dhxc-libXdmcp-1.1.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mhhia7plis47fhrv713fmjibqal96w1g-libaio-0.3.113&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1rlljm73ch98b2q9qqk8g0vhv2n9mya8-libapparmor-4.1.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qsyxh2zqqkqzaaa0v5scpjz364ksmj3m-libargon2-20190702&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/r25srliigrrv5q3n7y8ms6z10spvjcd9-glibc-2.40-66-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wcjq2bl1vhvnc07xzl5m41jncf745yz4-firewall-reload&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bh1hxs692a2fv806wkiprig10j5znd7c-libcap-2.75-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/142lbjxi74mv9nkb9k4831v2x8v5w5zv-bison-3.8.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/nsi5mszs52rj3hgkpa8cnc90nnqvl11a-boehm-gc-8.2.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/z98iwn19jjspfha4adjkp32r5nj56grw-bootspec-1.0.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x9hwyp3ld0mdqs8jcghshihwjdxm114l-boehm-gc-8.2.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fm2ky0fkkkici6zpf2s41c1lvkcpfbm5-db-4.8.30&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/10glq3a1jbsxv50yvcw1kxxz06vq856w-db-5.3.28&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wzwlizg15dwh6x0h3ckjmibdblfkfdzf-flex-2.6.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/sdqvwr8gc74ms9cgf56yvy409xvl8hsf-gettext-0.22.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kxhsmlrscry4pvbpwkbbbxsksmzg0gp0-gmp-with-cxx-6.3.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/nzg6zqsijbv7yc95wlfcdswx6bg69srq-gmp-with-cxx-6.3.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/088li1j480s9yv1736wiz7a26bxi405w-graphite2-1.3.14&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/s86p50hcjcp9phyv9gxd5hra8nwczvrk-groff-1.23.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x4b392vjjza0kz7wxbhpji3fi8v9hr86-gtest-1.16.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rw826fx75sw7jywfvay6z5a6cnj74l1g-icu4c-73.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9hpylx077slqmzb5pz8818mxjws3appp-iputils-20240905&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y4ygj0jgwmz5y8n7jg4cxgxv4lc1pwfy-jemalloc-5.3.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jgxvk139zdfxi1wgdi9pjj1yhhgwvrff-lerc-4.0.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ckwwqi6p7x3w64qdhx14avy2vf8a4wiq-libICE-1.1.2-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x2wlg9cm3yrinz290r4v2fxpbpkw8gki-libcap-2.75&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2bjcjfzxnwk3zjhkrxi3m762p8dv6f1s-libcap-ng-0.8.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/87fck6hm17chxjq7badb11mq036zbyv9-coreutils-9.7&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dfznrcrr2raj9x4bdysvs896jfnx84ih-libcbor-0.12.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jrd3xs0yvb2xssfqn38rfxhnzxz9827s-libcpuid-0.7.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/w53vh0qqs6l2xm4saglkxaj97gi50nr5-libdatrie-2019-12-20-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/amy9kqbm05wv18z5z66a3kprc2ccp390-libdeflate-1.23&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yai7mpy5d4rw0jvflyxdf0vzjkiqxhv6-libevent-2.1.12&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/90c412b9wqhfny300rg5s2gpsbrqb31q-libffi-3.4.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9z7wv6k9i38k83xpbgqcapaxhdkbaqhz-libgpg-error-1.51&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vwj8664lvyx3svjp856baijyk17vv9lc-libidn-1.42&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9f6bvnw1hxy79shw6lva854ck3cmi43j-libjpeg-turbo-3.0.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/56fi3kcbg9haxf5c1innrn2p9dx2da2j-libmd-1.1.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9hbdbr5hikxjb16ir40w2v24gbivv22x-libmnl-1.0.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ygz5dcpzd7qkw44wpbd65rl6amwpxp5f-libnfnetlink-1.0.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/635dz3p1afjwym9snp2r9hm0vaznwngy-libnl-3.11.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/59j7x0s1zybrjhnq5cv1ksm0di4zyb4n-libpipeline-1.5.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2sbq4hd9imczmbb5za1awq0gvg0cbrwr-libbsd-0.12.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bxs5j3zhh35nwhyhwc3db724c7nzfl36-libpsl-0.21.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/q0dsazc8234b7imr9y4vv5rv09r58mqi-libptytty-2.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/f7y5q4jwja2z3i5zlylgbv5av6839a54-libnftnl-1.2.9&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6wrjb93m2arv7adx6k2x9nlb0y7rmgpi-libnetfilter_conntrack-1.1.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kvycshxci0x434bcgnsvr9c0qgmsw6v5-libressl-4.0.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/a7zbljj0cwkbfzn22v6s2cbh39dj9hip-libseccomp-2.6.0-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7h0sard22wnbz0jyz07w8y9y0fcs795r-diffutils-3.12&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1wp5qqj9n3ccjvlbhpdlg9pp9dpc00ns-copy-extra-files&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7y59hzi3svdj1xjddjn2k7km96pifcyl-findutils-4.10.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rmrbzp98xrk54pdlm7cxhayj4344zw6h-libassuan-3.0.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/0dqmgjr0jsc2s75sbgdvkk7d08zx5g61-libgcrypt-1.10.3-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9gzvhlrpxmkhggn32q7q9r38cfg6gasn-libsodium-1.0.20&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zf61wng66ik05clni78571wfmfp5kqzq-libtasn1-4.20.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/z53ai32niqhghbqschnlvii5pmgg2gcx-libthai-0.1.29&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/np37flx1k0dj0j0xgxzkxs069sb5h4k3-libtool-2.5.4-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1warn5bb3r7jwfkpdgr4npab3s63sivj-liburcu-0.15.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9mcjnb75xq17mvr8ikm3sg5yhx6ga62r-libuv-1.50.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/sh1rrkag3x04p0gs80723iwfdwlysxf8-libvmaf-3.0.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qn01pv62sbpzbsy0a6m0q23syrmkk3bv-libxcb-1.17.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qizipyz9y17nr4w4gmxvwd3x4k0bp2rh-libxcrypt-4.4.38&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1r4qwdkxwc1r3n0bij0sq9q4nvfraw6i-libpcap-1.10.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xv0pc5nc41v5vi0lac1i2d353s3rqlkm-libxml2-2.13.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/39zbg3zrp77ima6ih51ihzlzmm1yj5vh-libyuv-1908&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3mqzj6ndzyy2v86xm70d5hdd1nsl1y9f-lm-sensors-3.6.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g3j7jsv3nsfnxkq98asi01n0fink0dk9-llhttp-9.2.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/iyh7nfcs7f249fzrbavqgxzwiy0z7xii-lowdown-1.3.2-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/51sr6m5fb8fff9vydnz7gkqyl5sjpixl-lz4-1.10.0-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gpbn3j498s0909h5j8fb3h4is8dn8rll-lzo-2.10&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zfb1cj0swnadhvfjvp0jm2zhgwiy927f-make-initrd-ng-0.1.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/h4zr885cac368xv73qrhscbpc7irqly8-mcpp-2.7.2.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g2gsgbka17hdr999v8k9yhkq825mb6zz-mkpasswd-5.6.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mpvxc1dbpnk74345lk69dw497iqcjvj0-libX11-1.8.12&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9nn8vbf2n55zkb7dh6ldxckbis3pkh30-libaom-3.11.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3ccwi70k69wrxq6nxy6v3iwwvawgsw6m-libressl-4.0.0-nc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/029cprg174i7c4gvn1lwmnm4vdl6k8df-libvmaf-3.0.0-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wxkbp7kwvpxvjh28rigmf6lfq64zlsyj-iptables-1.8.11&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2l8jg5lpi7084sc1q33jmpd7fph41n2g-libxcb-1.17.0-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wyf93cvh25b2xg82mkjcpmwgcspk0ggr-mpdecimal-4.0.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/n52k1dccv0mipz1s4gkk45x64cmmcvrf-mpfr-4.2.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/iszvcck61smiji8gxmbf02z3gi8zr7i3-mtools-4.0.48&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/74is8yi7sy8q58xg806fy0ja99fswjva-libxslt-1.1.43&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mhmg8c5dmx8qi63rlz347931br8bmq08-ncompress-5.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vfmnmqsnfiiqmphy7ffh2zqynsxfck1q-ncurses-6.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/skd9hg5cdz7jwpq1wp38fvzab9y8p0m6-net-tools-2.10&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m4yrdwg3zv50mw8hy2zni5dyy7ljlg7j-nettle-3.10.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/v7rzgm8p6p0ghg5mqcin4vbx6pcrvc0j-nghttp2-1.65.0-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hbjkfqhx0anx8x2rs0d9kbfhy80jfc7n-nixos-build-vms&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/833wqy1r0qpp5h5vd4yiqm5f2rjjc7jg-node_exporter-1.9.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ci5nyvrii461hnaw267c1zvna0sjfxif-npth-1.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6czlz4s2n2lsvn6xqlfw59swc0z21n89-nsncd-1.5.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/82n465240j5a8ap7c60gqy3a6kwqv1rs-numactl-2.0.18&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/z8rlklqfzxq7azbzyp30938x7wh5zf3c-oniguruma-6.9.10-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mb407pssv7zc7pfb4d910k6fshfagm6j-libmpc-1.3.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zllk6n33p6mx8y9cf4vhs2brcbis3na4-libX11-1.8.12-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gmirqf6vp6rskn2dhfyd7haphy6kjnvk-libXext-1.3.6&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/21aj13sj7jg5ld96s3q7nd40s1iwzfld-libXfixes-6.0.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/a5mrmf5bjmjfq2y90hsn8xnw3lb0cqil-libXpm-3.5.17&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/md8kapandyhs7bbw5s782aanw38p2kax-gnupg-2.4.7&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pbg3xkihyscyx3978z0pfc0xixb10pf6-libXrender-0.9.12&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9m6a4iv2nh6v4aga830r499s4arknsfb-p11-kit-0.25.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8pviily4fgsl02ijm65binz236717wfs-openssl-3.4.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4sfqx63v2k8argz2wmnbspr0rh49y1c1-libXi-1.8.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x0kaspzb5jqvgp357bj27z6iq24ximfg-patch-2.7.6&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ifbr2frwmyf8p0a260hn5vzg3cagww14-pcre-8.45&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/a9c6rz5183psp30q1nhkakis6ab4km4b-pcre2-10.44&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pkxrqwd26nqr7gh9d4gi9wf7hj6rk29a-libXcursor-1.2.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/sdqzcyfy52y0vf10nfsxy3mhv9b2vmkv-jq-1.7.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wlzslync0dv270mvi1f7c0s1hf4p27yf-pcre2-10.44&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qqpgwzhpakcqaz6fiy95x19iydj471ca-pcsclite-2.3.0-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pi7vpdqikh160rj4vyfh58x0z2hksgj7-libaom-3.11.0-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/r4bvdpg1761bqc4jxn4sqxr6ymbcdw8f-perl5.40.0-Clone-0.46&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vqhxms7i64vb86p07i8q50x32yi9gv5c-perl5.40.0-FCGI-0.82&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bafwfabi148bigqra4nc5w029zj7dx7c-perl5.40.0-TermReadKey-2.38&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/clh9w1vpiijlv9a1i6gjkvwziwqzsp78-php-calendar-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bvh4vwr9dr5iaiz57igi5b4mryqnwpaa-php-bcmath-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vim07ywfgdqz217qnmik9knbmm5glpcn-perl5.40.0-HTTP-Message-6.45&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/n9ch6ggimi6ri5vx62mqmzgrrkb3qfwg-jq-1.7.1-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gwdxl7y6c54smp9x268diyjqwg1naylk-php-ctype-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gqmr3gixlddz3667ba1iyqck3c0dkpvd-gnugrep-3.11&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ybd0aamz6dwc51x1ab62b7qycccqb0z0-libselinux-3.8.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/b78nah09ykpmxky3br6fl5akjjwcg1g5-php-dom-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mq46ybxbw3f7jcv07hlk06sa8cqwy4f8-php-exif-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wfsrpgcf3mpl9np032nhj6rvz53y4la5-php-fileinfo-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1f91m6wkjrm4v6317za4wklgqh6qraan-php-filter-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j6rlrmd7jqk1902s98mjjxj8d563hv8q-perl5.40.0-HTML-Parser-3.81&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cxqg93vhjddswn75f5bdzbkcpbix32gg-perl5.40.0-HTTP-Cookies-6.10&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dfyds9allpyy0nwhr2j729jvkb49mrxn-perl5.40.0-HTTP-Daemon-6.16&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/z88zb5wamza6irc3lkz6aj52ga3q5sl3-libaom-3.11.0-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3pd9kl0nnn22in35ad4p6v5zha8s24gj-perl5.40.0-HTTP-Negotiate-6.01&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9mlfyarh1nzzzc0zsn6vf0axdjjbq2l4-gpm-unstable-2020-06-17&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/66ld17ifbjz63firjjv88aydxsc3rcs6-less-668&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xs1qm9vidbfn1932z9csmnwdkrx4lch6-libedit-20240808-3.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/i9b4ix0ih1qnf2b4rj0sxjzkqzqhg7mk-php-ftp-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bssap9z2zp2nbzzr6696dqqr6axac57g-php-gettext-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fmf4095h2x5rxsgsyxz655049k2ibchl-perl5.40.0-CGI-4.59&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pdknwq3rbhy1g6343m4v45y98zilv929-php-gmp-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/f3fc31rc8gnmbbz0naniaay6406y5xy8-php-iconv-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/sxi98visi8s3yk1p05ly3pljh683wg1f-php-intl-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/h57pwp22kkwjm3yqh3id3ms2jymc27rq-php-mbstring-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jlzg258kgf0k3vcn54p91n43kb8afllk-php-mysqli-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j0ljd9127519pkb01zbyxcf42kjhp2l8-aws-c-cal-0.8.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/axi2kqwlrr7lvkfj42p7mav2x7apffrq-coreutils-full-9.7&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bmjb20jhxkq881f43pd264240sp677an-krb5-1.21.3-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4qks83jh0avrs4111c6rlwn3llqlily0-ldns-1.8.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bmckdjhp1cn78n4md1m55zglpqxwijj3-libtpms-0.10.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/d4knbwl8kbkaai01bp5pb0ph0xpb7bnz-perl5.40.0-Net-SSLeay-1.92&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/idgpi0g62yyq8plhrdc2ps2gcrkd44jz-dash-0.5.12&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fipal54rj1drz2q567pacy6q2gsnm2hq-php-opcache-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/57csaam2bhhfzbhw2j70ylaxq25wj09g-php-openssl-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/lapy63xgm8gpjbxj55g4w74idmbnavzm-php-pcntl-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/n1lb8wdk0avd6f06fhiydwa2f4x91pz4-php-pdo-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/47kv3znq3rx6lhp5h3shs2zx0gd7r3zv-php-pdo_mysql-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/0gn4mrgjlx9dhffs19yshpdrhi9pcbyl-perl5.40.0-CGI-Fast-2.16&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yqj36zvdh3nmv5fpyfsp5mr06h1n4npc-php-posix-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7yi71ajqcpsdmz1qa6r8aprm6vgqj74s-php-session-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fmj7d945ychgybv2bld5dnk4lzcm1m10-php-simplexml-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/w8cc84nvjzikcrgfc0pi8qap5wiq1cb8-php-soap-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9rz570nz0d51y8r6dmjniqqbzgc4bnrg-php-sockets-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/svl7fda13ygkdyvisywihlsslrcqvbp8-php-sodium-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/70aw279ymnhp8dadzwfk4clh4f1m7wsn-php-sysvsem-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3ym3wzyd6z19hfqzqwchzqbd9vzdk345-php-tokenizer-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j04vms72z22650kvbx69b8qkpbgi5na6-php-xmlreader-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/n75k5rjvbc7gfp4zpajpab5rykajdcmy-perl5.40.0-IO-Socket-SSL-2.083&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1c0xn8mx6ha6fcpaxi4p0q16lvr7zfrr-php-xmlwriter-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/r7sp55wajh5p7yh2ahgifr1c8jbqjgnl-pixman-0.44.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dli16nly2z52s1mi1phbcgmhw7nkq7x6-pkg-config-0.29.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6mnmfhfsz94zgsyskz7zanian98ssykf-bind-9.20.9-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7v3h0hsyq17zl8wd7zpkzhif215ywagw-cyrus-sasl-2.1.28&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/004mzgs45wax9qlxrqpzhjbnzz049gsy-gsasl-2.2.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/039lh7h4fv88p1mxybhw35fz6y3y5mb3-libpq-17.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xam5b9xk11767mz35dc9i5gcmy9ggsaf-popt-1.19&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8vqqb9hp35whmp9fxd4c01z2zrdy8g5g-pre-switch-checks&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1l2x502h3j9bkp2ln3axm9qp70ibg7a1-qrencode-4.1.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hng18h8w0w3axygpknq9p9pn7yd0c1m5-rapidcheck-0-unstable-2023-12-14&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/971mpk4nqhqcxggx0yi60w9y1ya570bj-readline-8.2p13&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6vw0k4y7zxrbl3sikwbmn8aflzyi923q-s2n-tls-1.5.17&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mi0yqfw3ppyk0a4y6azvijaa4bmhg70y-system-sendmail-1.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/v03zr9slrp64psxlpwh7gn0m5gcdglwm-systemd-minimal-libs-257.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qih5jc5im739yjgdslbswyxmz8kslqdl-perl5.40.0-Net-SMTP-SSL-1.04&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wcawvp0ilpqmmjfx8z6nbcsmcbpfa6i7-logrotate-3.22.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4iqgxg1ixmnvf8cq6jagz6ipas0p4bg5-tbb-2021.11.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/c96bpmpg46wr7pq4ls8k56jrlysmz9nr-time-1.9&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2crk9xnq5x9v7yf0r2nwkgj8qsmxr4ly-pkg-config-wrapper-0.29.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pxflxwl6fa54jjc619fqdja5z4fn5p35-openldap-2.6.9&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7rdy1m9afs7036hwhf1r8lw1c900bmfb-php-pdo_pgsql-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hsnmgsywiz5izr59sm8q1fwcs64d8p85-php-pgsql-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xa5nkrg7h2akk0962c3k9hxly104yq0k-tree-sitter-0.25.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8y5hcryppj548yfx6akiw93qrw8zv6js-unbound-1.23.0-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/08inrjiy9snpmn77siczc0ncwpcbfv4v-unit-script-container_-post-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8afssqln4ckx4ii555ly707i2xvk17xy-unit-script-container_-pre-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xqmz2x2zmg6w76wl1b1kznv0b4x7dfr6-perl5.40.0-ExtUtils-PkgConfig-1.16&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1q9lw4r2mbap8rsr8cja46nap6wvrw2p-bash-interactive-5.2p37&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jp25r6a51rfhnapv9lp8p00f2nzmfxxz-bind-9.20.9-host&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xzv4hkskh8di1mk7ik75rvbkyr7is882-guile-2.2.7&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/llcqfkdwbj1m1s4fbby82b49hffxqdb0-php-readline-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/a7j3s4lqfa5pfrxlddmmkxx3vjz6mjzf-aws-c-io-0.15.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vj7inkvjyd3s0r30h4b5pq81f4jlkffr-tbb-2021.11.0-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/lqn8cpyf4nq8704p7k3wjbym51q87rh3-unit-script-post-resume-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3wb1ngcfqajx6slx4c335lvb83js9csr-unit-script-pre-sleep-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/03pbln3nwbxc6ars4gwskgci3wj557yy-unit-script-prepare-kexec-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zf9xnwy0r9mzm3biig8b56hgyahfhf6b-unit-script-sshd-keygen-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2pvhq9kgqh5669qj6805vpasngivad8h-lvm2-2.03.31-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3z98iawifra8xn74bmdda6xbwgr5z0lh-unit-script-systemd-timesyncd-pre-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/c5vpbrb9iiq9jynnx57f0h114qar1dkw-unixODBC-2.3.12&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/675r4l9rpmaxdanw0i48z4n7gzchngv7-util-linux-minimal-2.41-login&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/0jj4mmc0861dqz2h603v76rny65mjidx-vim-9.1.1336-xxd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dqcl4f3r1z7ck24rh9dw2i6506g7wky5-which-2.23&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bbq0c28cvahc9236sp33swq4d3gqn2rc-xlsfonts-1.0.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/n50daiwz9v6ijhw0inflrbdddq50k3sq-aws-c-event-stream-0.5.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7j95a3ykfjgagicfam6ga6gds2n45xc0-aws-c-http-0.9.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kqrqlcdqs48qslxsqsnygdyx32w7lpwg-php-ldap-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/a232zjl9jnmhq56hfr5n5lz4qg5fpb83-xxHash-0.8.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/za3c1slqlz1gpm6ygzwnh3hd2f0lg31z-libblake3-1.8.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mzvz45f54a0r0zjjygvlzn6pidfkkwj3-audit-4.0.3-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/s5cy3qgb3w0i1ylwm8dbsnk3m5jqxik4-m17n-db-1.8.10&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7d0871d6pn8r51sbpclclg56mmrq761a-nix-info&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xzfhjkn4am173n6klibs9ikvy1l08hfg-nixos-firewall-tool&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/i5323bb72x07y56d8z2iwb589g56k2y8-vim-9.1.1336&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pa60s415p92gnhv5ffz1bmfgzzfvhvd8-xz-5.8.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/srby6wmvg7dp454pwb6qvaxdiri38sc1-zlib-1.3.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/k8asbblj2xn748rslklcll68b4ygh2am-zlib-ng-2.2.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3p844hlrf7c7n8jpgp4y4kn9y4jffn4i-php-pdo_odbc-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/myzfn9vnyglhq3vj4wf99fi8qj98mqri-zlib-ng-2.2.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vcrjkcll3rnr95xjql8rz57gjlhh2267-zsh-5.9&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/d8vq999dg607ha6718fimpakacfax0gd-zstd-1.5.7&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4rdbzw9g2vpyvs0b07pgmc1554pwdma4-aws-c-auth-0.8.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/k4xya9rihwkd175zxvcfnsqbzwrsgwmb-aws-c-mqtt-0.11.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/974a51073v6cb7cr5j0dazanxzmk9bxg-binutils-2.44-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/a7h3ly9qzh8wk1vsycpdk69xp82dl5ry-cracklib-2.10.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/p3sknfsxw0rjmxbbncal6830ic9bbaxv-audit-4.0.3-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rrnlyc5y7gd5b0f91a89vbw1flhnlm73-file-5.46&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9ds850ifd4jwcccpp3v14818kk74ldf2-gcc-14.2.1.20250322&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yba197xwc8vvxv9wmcrs9bngmmgp5njb-gnutls-3.8.9&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4f7ssdb8qgaajl4pr1s1p77r51qsrb8y-kexec-tools-2.0.29&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fjnh5mgnlsahv2vsb8z1jh41ci924f7k-aws-c-s3-0.7.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5f0bv68v1sjrp4pnr8c6p7k04271659w-libfido2-1.15.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qvyvscqgr6vyqvmjdgxqa521myv5db0p-kmod-31&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/89bxhx3rhk6r4d5fvwaysrykpmvmgcnm-kmod-31-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/v63bxfiacw082c7ijshf60alvvrpfxsq-binutils-2.44&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g91dviqva4rkkw8lw30zy3gj14c1p23s-libarchive-3.7.8-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dm19r683p4f07v2js5jnfnja5l296gs6-aws-crt-cpp-0.29.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jqhlcbmg1fsvc8w2w3ai9f9i8lzk7yfv-libgccjit-14.2.1.20250322&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y3x4m9wy3a731ibvgvs194j10znc392m-libpng-apng-1.6.46&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9642gi5dl4w9nkhab0l6xry685cg403c-libssh2-1.11.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yn4y14blp0j4l9044jxzjzf9i11kpjsx-libzip-1.11.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vrdwlbzr74ibnzcli2yl1nxg9jqmr237-linux-pam-1.6.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7m1s3j4inc333vynaahynfgda1284iyh-m17n-lib-1.8.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5i64l61if26whc3r9lzq6ycxpd2xnlgm-freetype-2.13.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zr0hlr1hybxs08j44l38b8na1m8xpkms-libwebp-1.5.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/v578vkzh0qhzczjvrzf64lqb2c74d5pk-curl-8.13.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hd1ys7pkiablfdgjvd1aq15k9jplsm2j-libgit2-1.9.0-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1dxfw2zshri809ddyfqllvff3cfj96ma-libmicrohttpd-1.0.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g81krn6p9fmyb2ymkd6d7cndjma3hzq0-etc-shells&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2vd9h77mrciiff8ldj1260qd6dlylpvh-nano-8.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9wlknpyvdm3n4sh6dkabs0za1n5nvfjn-aws-sdk-cpp-1.11.448&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/s2np0ri22gq9pq0fnv3yqjsbsbmw16xi-curl-8.13.0-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cly4pxh7avd579girjmpxmx8z6ad4dyp-elfutils-0.192&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ldn53xpxivf489d7z673c95fkihs5l8r-fontconfig-2.16.0-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/vam5p76i7kbh1pwhdvlrhb33wgyfzy6x-chfn.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yr8x6yvh2nw8j8cqxana4kwn8qp9pjh2-chpasswd.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/d4zhdmcqi6z247436jqahvz8v1khrcbi-chsh.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/p83i191brxfj966zk8g7aljpb8ixqy1m-groupadd.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xlxar4qknywb8i3rf27g8v85l6vxlh2j-groupdel.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kkv7k9fmsfy5iljy4y2knirmrpkbplzs-groupmems.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/76gwx407dhh1ni9fn64h0yha3c1zwabp-groupmod.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qzk9gj8jdl37xqiccxa98g442byp3rrq-libtiff-4.7.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/64zabz1hxymxbcvp78hp9kacrygnf9l9-fontconfig-2.16.0-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ka4yf6hhsx1vlqkff4bvrvn27kbp28gg-mariadb-connector-c-3.3.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gs6syc444yjbg5ivf36sn535chg6mkrx-libXft-2.3.9&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bmmmy3sz3fmlxx64rlw1apm7ffywpyap-libpwquality-1.4.5-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3qk4g71ciq7hf06nmy252qf4ng36g0s7-nginx-1.28.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/i0nlz4mcyxzxd96x5dv0zcy23z6xkvzy-openssh-10.0p2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/s1c3kiwdwcff03bzbikya9bszz45mmkc-etc-nanorc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/pn4js43jj8ag504ib4dyf5vd5ap2ilkg-libwebp-1.5.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gg124glj125xfc8jzvkl6r47ph8nl6pw-passwd.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/fx0cjyvqjmfnbqxcd60bwaf36ak16q2q-pciutils-3.13.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/al9x8cr5xifp3qd2f5cdzh6z603kb5ps-perl-5.40.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ls9jrqk9arnwrm3cmm1gd9wgllpn4b3b-php-curl-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7hnpi2q3cxfzkzh7miv5rkl4b74gpzk4-php-imap-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cq6kbdji0q5c3r2m0ikaaiip5z0p6318-php-mysqlnd-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/w0nkgg89ls4xvk49lnv483blmhq2ac9x-php-zip-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/h1z0wlyb4av929a6qkxblhndha0d6byn-php-zlib-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7f3nwfvk0f32663rz1xn38cbsl66idx2-libbpf-1.5.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mww31587ng38jw87pf1dv121ih27clf5-plocate-1.1.23&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/sb4ml8qjxcr2idzdgjcw2bz11p6nzff4-rsync-3.4.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hhfm5fkvb1alg1np5a69m2qlcjqhr062-binutils-wrapper-2.44&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7nln4vh5kbwba6q9d3ga77vk2vj72mdk-runuser-l.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qb0d1xc8vxdr8s2bkp4q8msj8bhkvmg8-runuser.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wgq5kj4qhi78sr70mwj3bgnmx4ya87fr-security-wrapper-unix_chkpwd-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/c0clf3w4bfkcg9fc7nl6bfhgivz24wvc-shishi-1.0.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yfjzkkkyxcalyj7l1n4d4y6s81i65hmy-sqlite-3.48.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x0ncvjhy2vgz174bhm8yycywwrjvgr9a-strace-6.14&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ywy0hjiydvv561a5wds6ba7z059zj9im-sudo-1.9.16p2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qywjdkbap2h7g17qrzhi4nm243cqpx1f-sudo.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dk55smr7wdjad151r7cv1pln0winqq9x-tcb-1.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g4d8pli27k90s0n4nnm5nipxbyrcd9vl-useradd.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/71v1svlxdziiqy8qmzki3wsrg7yv7ybq-userdel.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/npkqihnvfw9cx3a1mzr59x23vkqql51g-sshd.conf-final&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bxznmkg59a4s2p559fmbizc2qcgjr3ny-iproute2-6.14.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bqa6kwd5ds2jrj76nch6ixdvzzcy4sxl-usermod.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/b895xnbwyfj1msj6ljcsvwfdhwqhd2vd-shadow-4.17.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zx9qxw749wmla1fad93al7yw2mg1jvzf-vlock.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/z21r5fak3raias1zlc0grawnsrcq094x-X-Restart-Triggers-sshd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/98zamhd8d0jq3skqwz28dlgph94mrqir-xz-5.8.1-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/l9xn7mbn0wh0z7swfcfj1n56byvcrisw-zstd-1.5.7-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/clfkfybsfi0ihp7hjkz4dkgphj7yy0l4-nix-2.28.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yz7bsddsmyssnylilblxr8gxyaijfis7-php-pdo_sqlite-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rmj2j70y96zfnl2bkgczc1jjxxp1gpc2-php-sqlite3-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5c38fjjwfnlfjiiq62qyrr545q0n60ki-util-linux-2.41-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/r03ly1w54924k8fag1dhjl3yrllj6czd-util-linux-minimal-2.41-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mmz4qa42fhacp04wfjhwlslnlfffyxjv-append-initrd-secrets&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9r81a64smasyz3j7x3ah684hyzivmplx-kbd-2.7.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/324bqqlvdjbsixcbagdn8yjxc6zcj28a-security-wrapper-newgidmap-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mvgsv5643miclpcpwzv43kibj5ydpxvl-security-wrapper-newgrp-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/n42x8ly03p2dyj6lqnmaynrcw8mg72d7-gss-1.0.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xrdkznkvi79w8pp1cyhzi40prmxilw8y-security-wrapper-newuidmap-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/p7vixy3km13dwf3g4rkg9n3qwkj2vhik-security-wrapper-sg-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2adjiqpm8p55hfhhrw3f1kvi340allma-security-wrapper-sudo-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/0b1qa8fm793qvcn8bvr5kg5jl4indh9y-security-wrapper-sudoedit-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4hjw4c56ml09jbac2mzz38qc958d3fb2-shadow-4.17.4-su&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m4w8d2h3v76anng7s9cv9c1iq9w6y2jj-cryptsetup-2.7.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jf0v9bq4dlk56acbkpq6i84zwjg4g466-e2fsprogs-1.47.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bkpj51fz88rbyjd60i6lrp0xdax1b24g-glib-2.84.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/170jn0hjz46hab3376z1fj79vmn0nynm-libSM-1.2.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8w718rm43x7z73xhw9d6vh8s4snrq67h-python3-3.12.10&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/a885zzx9s5y8dxbfvahwdcwcx6pdzm9q-tpm2-tss-4.1.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m2dkj8xcpcrymd4f4p46c3m59670cj9y-security-wrapper-su-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/d4icl77wfbz3y5py1yni18nmqwkrb4lr-libSM-1.2.5-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2rxzdljx3dp4cgj1xlald496gdsjnwj8-libXt-1.3.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/agpxymqp96k4bksyz3bbzr5y8jgykf4p-util-linux-minimal-2.41-mount&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/sjsapivqvz7hs93rbh1blcd7p91yvzk1-console-env&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jws80m7djgv03chq0ylw7vmv3vqsbvgg-util-linux-minimal-2.41-swap&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2050009wgldpv3lxld3acz5pr6cr7x53-wget-1.25.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/77z9fh96318kyjmmidi558hyyssv00s8-bcache-tools-1.0.8&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/lg0d9891d12dl3n1nm68anmlf3wczf28-btrfs-progs-6.14&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cx6fbilhj4nmq9dl8c8c73mimm08x60z-e2fsprogs-1.47.2-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hlmmf01lhg62fpqhzispzs8rhzn7gg4p-libXmu-1.2.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wfxr783my1pr6pnzd6x22dpi8amjwkkd-X-Restart-Triggers-reload-systemd-vconsole-setup&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m506rljkkpxc4d0j0j41qjhldqrwxz4x-libXt-1.3.1-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9qqln0vxf1g6ll2wpkdfa2cmpm4nn17y-libXaw-1.0.16&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/if9z6wmzmb07j63c02mvfkhn1mw1w5p4-systemd-257.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ykzprjkb2l61gnlcm368vh8wnj7adwx6-systemd-minimal-257.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mwan3006nzdq6ia8lw3hyk4vlc585g17-libXmu-1.2.1-dev&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1nxchlxi7i0b1nhsyq732al8sm1blywm-util-linux-2.41-login&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mhibs2q4f3mjpzwgm6wdk2c4d6vkaklv-Xaw3d-1.6.6&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g51ca42mmgxzz7xngf0jzhwd4whi19lj-util-linux-2.41-mount&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8m86a49g1p7fvqiazi5cdmb386z7w5zf-libotf-0.9.16&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zma6jllb9xn22i98jy9n8mz3wld9njwk-util-linux-2.41-swap&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mw6bvyrwv9mk36knn65r80zp8clnw9jl-util-linux-minimal-2.41-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/94jfyay8h0dwbakr69b91rsf8pdvah05-xauth-1.1.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j3h72p435glzylc2qjny8lqd4gml03ym-xrdb-1.2.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zpprhp27r6chnkfkb85wl42p33vsawj8-su.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/41qbms27n02859ja4sc7wsd9mfp3ward-cairo-1.18.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/8bsl1vrab2pwj8ilpzfn2iwzbrps8jgq-harfbuzz-10.2.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j61kasrhqidgpj9l9zb1wvnizk1bsiqf-qemu-host-cpu-only-9.2.3-ga&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cdr8agx3ffy086z30wiysnclrc5m8x69-gdk-pixbuf-2.42.12&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/b5xcfdnccfslm89c8kd3lajgb5drx3h4-shared-mime-info-2.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y7gbjn3x388syav1bjzciia9ppia2zqw-urxvt-font-size-1.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/hb6l900n8qiaxg0zj6l20yy7bn9ghxp3-wmctrl-1.07&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/808xr68djvk0x3r754mi81yvm2yr9ppq-libavif-1.2.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y5bgh0pyxzcgp90ywmgl9dk2m1j3hcbr-urxvt-perl-unstable-2015-01-16&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/z0kbsgnma0mijn5ssqfi3dk9z28bqlwj-pango-1.56.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/lzj596ffj1xk0r9v9l4gpgwg9w8jb0fr-check-mountpoints&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/5q42cwjbqj7ir7pvdqn411bbzr304g2j-etc-systemd-system.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7c0l3jk0fszisqidxrc2bby99dv5d261-fuse-2.9.9-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/nmyh57dqf1v6l6swghywkrb63aqmzzh8-fuse-3.16.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dh54wizfsivqa4ygx76jn49lpxkqbaf6-lvm2-2.03.31-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zp3db1aj7gs7p73wkm9v76x36z641nsi-man-db-2.13.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qcf53qls5h6jk0czdiwdwncfvfnvfmpb-gd-2.3.3&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9z8dbw37f5b6nrmh07310g1b2kdcs8sf-nixos-enter&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/14spdmgq38vmzywkkm65s65ab6923y6p-librsvg-2.60.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/csx6axnwacbq8ypl375p10why1fc2z8p-security-wrapper-fusermount-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j1d4jkh31x2yq5c8pibjifwcm5apa06l-fuse-3.16.2-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/j3jbm4d3hmz0nh4z3pqfy68zgil8immv-nixos-install&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/0r8953vg0n1b38d0jkk9lgbjfxvf8yc4-php-gd-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7hg38dsdzfk0jnb9q3q77ql9q1chp4fz-nixos-option&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mz9qpdl066bzg4n3rzb7x82dmx5jy386-security-wrapper-fusermount3-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/sm4b1vl7578rl2yiss62acs7ls7qinad-lvm2-2.03.31&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gxsa0wrpl9r1vl2zp3s1vkhmdf8ia0ca-php-extra-init-8.1.32.ini&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yi0knhi2qccafj49a8yd76rizllzx7bd-dbus-1.14.10-lib&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rys6134aqazihxi4g5ayc0ky829v7mf0-dbus-1.14.10&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/75m2zly9vl6gvx3gc23y7hgjsbarqf7r-switch-to-configuration-0.1.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6kldkgh0i8h6wwfi78nviki6a15h03bw-perl-5.40.0-env&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rqy3y1p2c1acfnbhkxzpixdshnivqaxl-perl-5.40.0-env&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zql0aksg8vpmaivh4ylkzg8ky4k1r3ms-perl-5.40.0-env&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/4ccfn37h8jfpppsi2i0rx0dx9c73qmsa-perl5.40.0-DBI-1.644&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gf1gs0w896yg73wyphgwdzhwa08ryw3n-perl5.40.0-String-ShellQuote-1.04&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/p90lckzsmp16zh0rfx7pfc6ryf77y3c6-perl5.40.0-libwww-perl-6.72&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/20f0f68rsai61a7rkcy6zxl6c0vh1z41-perl5.40.0-urxvt-bidi-2.15&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/g5lvfibif6crcl82mmzwicq6xwv9dcvf-rxvt-unicode-unwrapped-9.31&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kdrbnjhy3wchgbpkiz486k0qcv5z9a07-rxvt-unicode-vtwheel-0.3.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xj5y2ng1jbpx99nzi2pjajs5pdjn07rg-security-wrapper-dbus-daemon-launch-helper-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6v5a3nd0fxwddy5rlgl02hx7qmmb14ky-texinfo-interactive-7.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/s8lhl3z9z2jjaq1qschc4g0wd3dy91im-w3m-0.5.3+git20230121&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/50piw9b7b80vfjf9yny54zxfgjx3f3va-etc-ssh-ssh_config&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dkx6gwpq53a80aya87fi1vs43pr42s91-etc-sysctl.d-60-nixos.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/27xwi66r3zx83cfr2p4nz4d3p8q5mvcd-htop-3.4.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/p6z4ag3v1a3bmdd7b2ga8n2s53r3rb7s-login.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/q9idyw3m487xfb10dyk4v773kcyzq2da-php-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x3bxjpkcbfyzmy5695g1cchf04fbz8ca-procps-4.0.4&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/3wikk2w5zb68g0j90xqcqbn4dhq59910-nixos-generate-config&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/54a3gbciba6is1fvi29k291v04hkgihb-X-Restart-Triggers-systemd-sysctl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/6pgj3ja7zvlahqbcycd43iyc4g498ki0-perl5.40.0-DBD-SQLite-1.74&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/qvznfa46sqccjdh8vlnpzpqfkqh58s2j-sshd.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gsman0cwlms2l679bla5vgmf21jc5lvl-systemd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/q8ycjc7hnjm71p4n106acywcdsjjpskl-systemd-user.pam&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kyf94km34b9ydzy33gvrvdd893py5pc5-rxvt-unicode-9.31&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/bvvqvfbh0wq04di5f3lkrzjqy5pvq4w3-unit-script-container_-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/af291yai47szhz3miviwslzrjqky31xw-util-linux-2.41-bin&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/iqqhya38s39vgh1bk4v5sr6jvrmi5sg3-nixos-help&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jrrzha35h0bxbp2h30nv4dpa0fk4qhgb-perl-5.40.0-env&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rmnxnpxvm1wmlmgh5krgdf9wrym5ks99-tailscale-1.82.5&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xc3zdwldi1bbsrvjjvix7s57s31hsv29-command-not-found&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rhmacziivxfjs8chklcbm37p59wih6sw-nixos-help&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/7rjs1gm1377hsbd5yqg5bii3ay3f75q7-etc-bashrc&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/s674qd2b7v163k38imvnp3zafzh0585n-50-coredump.conf&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m4qaar099vcj0dgq4xdvhlbc8z4v9m22-getty&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rr6bdh3pdsvwjrm5wd32p2yzsz16q6z2-security-wrapper-mount-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/2q4yksm7gqgszl9axs95ylwakwk9yb8w-security-wrapper-umount-x86_64-unknown-linux-musl&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/gmydihdyaskbwkqwkn5w8yjh9nzjz56p-udev-path&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/yaz54h00w6qv85lw40g0s0dw3s4s53ws-unit-script-nixos-activation-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9jp02i4p4lrxz51sxiyhz71shr9vb6bc-mount-pstore.sh&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/csm3q68n81162ykn3wibzh0fs4fm0dhk-nixos-container&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/cflf8pxlaapivg98457bwh0nh1hasf5h-nixos-rebuild&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rajz07kxw9xj94bi90yy0m2ksgh3wprf-reload-container&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/c2c364mdd4qj6c51bjs6s3g4hb42c0ia-getty&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/298j97sm5jr2x5z8w5q8s3mzzpb3rjjw-unit-script-suid-sgid-wrappers-start&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/p1j5bc30pbq6bqpw2d676azqdv4whdi5-udev-rules&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/rrnq5j9c6j39k4pk9xkk4913h4zsqf5b-php-with-extensions-8.1.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/m2rqmjkvdd86lb4i8mi3rafxggf9l2py-X-Restart-Triggers-systemd-udevd&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/805a5wv1cyah5awij184yfad1ksmbh9f-git-2.49.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/wk8wmjhlak6vgc29clcfr1dpwv06j2hn-mailutils-3.18&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/kmwlm9nmvszrcacs69fj7hwpvd7wwb5w-emacs-30.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y0aw1y9ggb4pyvhwk97whmwyjadivxny-linux-6.12.30-modules&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1i4iz3z0b4f4qmbd9shs5slgfihs88vc-firmware&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/18f0xc0gid1ma6yjjx5afny9lnji3hf0-etc-modprobe.d-firmware.conf&#39; from &#39;https://cache.nixos.org&#39;...
### Installing NixOS ###
Pseudo-terminal will not be allocated because stdin is not a terminal.
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.
installing the boot loader...
setting up /etc...
Created &#34;/boot/EFI&#34;.
Created &#34;/boot/EFI/systemd&#34;.
Created &#34;/boot/EFI/BOOT&#34;.
Created &#34;/boot/loader&#34;.
Created &#34;/boot/loader/keys&#34;.
Created &#34;/boot/loader/entries&#34;.
Created &#34;/boot/EFI/Linux&#34;.
Copied &#34;/nix/store/if9z6wmzmb07j63c02mvfkhn1mw1w5p4-systemd-257.5/lib/systemd/boot/efi/systemd-bootx64.efi&#34; to &#34;/boot/EFI/systemd/systemd-bootx64.efi&#34;.
Copied &#34;/nix/store/if9z6wmzmb07j63c02mvfkhn1mw1w5p4-systemd-257.5/lib/systemd/boot/efi/systemd-bootx64.efi&#34; to &#34;/boot/EFI/BOOT/BOOTX64.EFI&#34;.
Random seed file /boot/loader/random-seed successfully written (32 bytes).
Created EFI boot entry &#34;Linux Boot Manager&#34;.
installation finished!
### Rebooting ###
Pseudo-terminal will not be allocated because stdin is not a terminal.
Warning: Permanently added &#39;10.25.0.87&#39; (ED25519) to the list of known hosts.
### Waiting for the machine to become unreachable due to reboot ###
kex_exchange_identification: read: Connection reset by peer
Connection reset by 10.25.0.87 port 22
### Done! ###
nix run github:nix-community/nixos-anywhere -- --flake .#wiki    --target-hos  6,58s user 2,98s system 17% cpu 55,063 total
</code></pre></details>
<h2 id="post-installation-steps">Post-Installation Steps</h2>
<p>Now that the declarative part of the system is in place, we need to take care of
the stateful part.</p>
<p>In my case, the only stateful part that needs setting up is the Tailscale mesh VPN.</p>
<p>To set up Tailscale, I log in via SSH and run <code>sudo tailscale up</code>. Then, I add
the new node to my network by following the link. Afterwards, in the <a href="https://login.tailscale.com/admin/machines">Tailscale
Machines console</a>, I disable key
expiration and add ACL tags.</p>
<h2 id="making-changes">Making Changes</h2>
<p>Now, after I changed something in my configuration file, I use <code>nixos-rebuild</code>
remotely to roll out the change to my NixOS system:</p>
<pre tabindex="0"><code>% nix run nixpkgs#nixos-rebuild -- \
  --target-host michael@zammadn \
  --use-remote-sudo \
  switch \
  --flake .#zammadn
</code></pre><p>Note that not all changes are fully applied as part of <code>nixos-rebuild switch</code>:
while systemd services are generally restarted, newly required kernel modules
are not automatically loaded (e.g. after enabling the <code>edgetpu</code> coral hardware
accelerator in Frigate).</p>
<p>So, to be sure everything took effect, <code>reboot</code> your system after deploying
changes.</p>
<p>One of the advantages of NixOS is that in the boot menu, you can select which
generation of the system you want to run. If the latest change broke something,
you can quickly reboot into the previous generation to undo that change. Of
course, you can also undo the configuration change and deploy a new generation —
whichever is more convenient in the situation.</p>
<h2 id="conclusion">Conclusion</h2>
<p>With this article, I hope I could convey what I wish someone would have told me
when I started using Nix and NixOS:</p>
<ol>
<li>Enable flakes and the new CLI.</li>
<li>Use nixos-anywhere to install remotely.
<ul>
<li>Build a custom installer if you want, it’s easy!</li>
</ul>
</li>
<li>Use <code>nixos-rebuild</code>’s builtin <code>--target-host</code> flag for remote deployment.</li>
</ol>
<p>Where do you go from here?</p>
<ul>
<li>Read through all documentation on <a href="https://nixos.org/learn/">nixos.org →
Learn</a>.</li>
<li>Here are a couple of posts from people in and around my bubble that I looked
at for inspiration / reference, in no particular order:
<ul>
<li>Michael Lynch wrote about <a href="https://mtlynch.io/notes/nix-oracle-cloud/">setting up an Oracle Cloud VM with
NixOS</a> and about <a href="https://mtlynch.io/notes/zig-vscode-nix/">managing his
Zig configuration</a>.</li>
<li>Nelson Elhage wrote about using Nix to test dozens of Python interpreters
as part of his <a href="https://blog.nelhage.com/post/cpython-tail-call/">performance investigation into Python 3.14 tail-call
interpreter
performance</a>.</li>
<li>Vincent Bernat wrote about <a href="https://vincent.bernat.ch/en/blog/2025-offline-pki-yubikeys">using Nix to build an SD card image for an ARM
single board
computer</a>.</li>
<li>Mitchell Hashimoto shared <a href="https://github.com/mitchellh/nixos-config/">his extensive NixOS
configs</a>.</li>
<li>Wolfgang has a <a href="https://www.youtube.com/watch?v=f-x5cB6qCzA&amp;t=1036s&amp;pp=ygUOV29sZmdhbmcgbml4b3M%3D">YouTube video about using NixOS for his Home
Server</a>
(<a href="https://github.com/notthebee/nix-config">→ his configs</a>)</li>
</ul>
</li>
<li>Contact your local Nix community! I recently attended the <a href="https://zurich.nix.ug/">“Zero Hydra
Failures” event of the Nix Zürich group</a> and the kind
people there were happy to talk about all things Nix :)</li>
</ul>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[My 2025 high-end Linux PC 🐧]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/"/>
    <id>https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/</id>
    <published>2025-05-15T15:44:24+02:00</published>
    <content type="html"><![CDATA[<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Update (2025-09-07):</strong> The replacement CPU also died and I have given up on
Intel. See <a href="/posts/2025-09-07-bye-intel-hi-amd-9950x3d/">Bye Intel, hi AMD!</a> for
more details on the AMD 9950X3D.</div>
  </div>
</aside>

<p>Turns out <a href="/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/">my previous attempt at this build had a faulty
CPU!</a> With
the CPU replaced, the machine now is stable and fast! 🚀 In this article, I’ll
go into a lot more detail about the component selection, but in a nutshell, I
picked an Intel 285K CPU for low idle power, chose a 4TB SSD so I don’t have to
worry about running out of storage quickly, and a capable nvidia graphics card
to <a href="/posts/2017-12-11-dell-up3218k/">drive my Dell UP3218K 8K monitor</a>.</p>















<a href="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4795_featured.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4795_featured_hu_3a23d743855185e1.jpg 2x,https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4795_featured_hu_5752380bee188abe.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4795_featured_hu_90d519e68715b8d6.jpg"
  
  width="600"
  height="800"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h2 id="components">Components</h2>
<p>Which components did I pick for this build? Here’s the full list:</p>
<table>
  <thead>
      <tr>
          <th>Price</th>
          <th>Type</th>
          <th>Article</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>140 CHF</td>
          <td>Case</td>
          <td><a href="https://www.digitec.ch/de/s1/product/fractal-define-7-compact-black-solid-atx-matx-mini-itx-pc-gehaeuse-13220301">Fractal Define 7 Compact Black Solid</a></td>
      </tr>
      <tr>
          <td>155 CHF</td>
          <td>Power Supply</td>
          <td><a href="https://www.digitec.ch/de/s1/product/corsair-rm850x-850-w-pc-netzteil-47356173?supplier=8560040">Corsair RM850x</a></td>
      </tr>
      <tr>
          <td>233 CHF</td>
          <td>Mainboard</td>
          <td><a href="https://www.digitec.ch/de/s1/product/asus-prime-z890-p-lga-1851-intel-z890-atx-mainboard-50252296">ASUS PRIME Z890-P</a></td>
      </tr>
      <tr>
          <td>620 CHF</td>
          <td>CPU</td>
          <td><a href="https://www.digitec.ch/de/s1/product/intel-core-ultra-9-285k-lga-1851-370-ghz-24-core-prozessor-49734792">Intel Core Ultra 9 285K</a></td>
      </tr>
      <tr>
          <td>120 CHF</td>
          <td>CPU fan</td>
          <td><a href="https://www.digitec.ch/de/s1/product/noctua-nh-d15-g2-168-mm-cpu-kuehler-46985628">Noctua NH-D15 G2</a></td>
      </tr>
      <tr>
          <td>39 CHF</td>
          <td>Case fan</td>
          <td><a href="https://www.digitec.ch/de/s1/product/noctua-nf-a14-pwm-140-mm-1-x-pc-luefter-657800?supplier=3204073">Noctua NF-A14 PWM (140 mm)</a></td>
      </tr>
      <tr>
          <td>209 CHF</td>
          <td>RAM</td>
          <td><a href="https://www.digitec.ch/de/s1/product/corsair-vengeance-2-x-32gb-6400-mhz-ddr5-ram-dimm-ram-24473300">64 GB DDR5-6400 Corsair Vengeance (2 x 32GB)</a></td>
      </tr>
      <tr>
          <td>280 CHF</td>
          <td>Disk</td>
          <td><a href="https://www.digitec.ch/de/s1/product/samsung-990-pro-4000-gb-m2-2280-ssd-37073751?supplier=406802">4000 GB Samsung 990 Pro</a></td>
      </tr>
      <tr>
          <td>554 CHF</td>
          <td>GPU</td>
          <td><a href="https://www.digitec.ch/de/s1/product/msi-geforce-rtx-3060-ti-gaming-x-trio-8gb-grafikkarte-14365529">MSI GeForce RTX 3060 Ti GAMING X TRIO</a></td>
      </tr>
  </tbody>
</table>
<p>Total: 2350 CHF</p>
<p>…and the next couple of sections go into detail on how I selected these components.</p>
<h3 id="case">Case</h3>
<p>I have been a fan of Fractal cases for a couple of generations. In particular, I
realized that the “Compact” series offers plenty of space even for large
graphics cards and CPU coolers, so that’s now my go-to case: the Fractal Define
7 Compact (Black Solid).</p>
<p>My general requirements for a PC case are as follows:</p>
<ol>
<li>No extra effort should be required for the case to be as quiet as possible.</li>
<li>The case should not have any sharp corners (no danger of injury!).</li>
<li>The case should provide just enough space for easy access to your components.</li>
<li>The more support the case has to encourage clean cable routing, the better.</li>
<li>USB3 front panel headers should be included.</li>
</ol>
<p>I really like building components into the case and working with the case. There
are no sharp edges, the mechanisms are a pleasure to use and the
cable-management is well thought-out.</p>
<p>The only thing that wasn’t top-notch is that Fractal ships the case screws in
sealed plastic packages that you need to cut open. I would have wished for a
re-sealable plastic baggie so that one can keep the unused screws instead of
losing them.</p>
<p>With this build, I have standardized all my PCs into Fractal Define 7 Compact
Black cases!</p>
<h3 id="power-supply">Power Supply</h3>
<p>I wanted to keep my options open regarding upgrading to an nvidia 50xx series
graphics card at a later point. Those models have a TGP (“Total Graphics Power”)
of 575 watts, so I needed a power supply that delivers enough power for the
whole system even at peak power usage in all dimensions.</p>
<p>I ended up selecting the Corsair RM850x, which <a href="https://www.tomshardware.com/reviews/corsair-rm850x-2021-power-supply-review">reviews favorably (“leader in
the 850W gold
category”)</a>
and was available at my electronics store of choice.</p>
<p>This was a good choice: the PSU indeed runs quiet, and I really like the power
cables (e.g. the GPU cable) that they include: they are very flexible, which
makes them easy to cable-manage.</p>
<p>One interesting realization was that it’s more convenient to not use the PSU’s
12VHPWR cable, but instead stick to the older 8-pin power connectors for the GPU
in combination with a 12VHPWR-to-8-pin adapter. The reason is that the 12VHPWR
connector’s locking mechanism is very hard to unlock, so when swapping out the
GPU (as I had to do a number of times while trouble-shooting), unlocking an
8-pin connector is much easier…</p>
<h3 id="ssd-disk">SSD disk</h3>
<p>I have been avoiding PCIe 5 SSDs so far because they consume a lot more power
compared to PCIe 4 SSDs. While bulk streaming data transfer rates are higher on
PCIe 5 SSDs, random transfers are not significantly faster. Most of my compute
workload are random transfers, not large bulk transfers.</p>
<p>The power draw situation with PCIe 5 SSDs seems to be getting better lately,
with the Phison E31T being the first controller that implements power saving. A
disk that uses the E31T controller is the Corsair Force Series MP700
Elite. Unfortunately, said disk was unavailable when I ordered.</p>
<p>Instead, I picked the Samsung 990 Pro with 4 TB. I have had good experiences
with the Samsung Pro series over the years (never had one die or degrade
performance), and my previous 2 TB disk was starting to fill up, so the extra
storage space is appreciated.</p>
<h3 id="onboard-25gbe-network-card">Onboard 2.5GbE Network Card</h3>
<p>One annoying realization is that most mainboard vendors seem to have moved to
2.5 GbE (= 2.5 Gbit/s ethernet) onboard network cards. I would have been
perfectly happy to play it safe and buy another Intel I225 1 GbE network card,
as long as it <em>just works</em> with Linux.</p>
<p>In the 2.5 GbE space, the main players seem to be Realtek and Intel. Most
mainboard vendors opted for Realtek as far as I could see.</p>
<p>Linux includes the <code>r8169</code> driver for Realtek network cards, but whether the
card will work out of the box depends on the exact revision of the network card!
For example:</p>
<ul>
<li>The AsRock Z890 Pro-A has rev 8125B. lshw:
<code>firmware=rtl8125b-2_0.0.2 07/13/20</code></li>
<li>The ASUS PRIME Z890-P has rev 8125<strong>D</strong>. lshw:
<code>firmware=rtl8125d-1_0.0.7 10/15/24</code></li>
</ul>
<p>For revision 8125D, you need a recent-enough Linux version (6.13+) that includes
commit “<a href="https://github.com/torvalds/linux/commit/f75d1fbe7809bc5ed134204b920fd9e2fc5db1df">r8169: add support for
RTL8125D</a>”,
accompanied by a recent-enough linux-firmware package.</p>
<p>Even with the latest firmware, there is some concern around stability and ASPM
support. See for example <a href="https://serverfault.com/a/1169558">this ServerFault
post</a> by someone working on the <code>r8169</code>
driver. But, despite the Intel 1 GbE options being well-supported at this point,
Intel’s 2.5 GbE options might not fare any better than the Realtek ones: I found
<a href="https://www.reddit.com/r/HomeServer/comments/1cc0yuq/are_intel_25_gbe_nics_i225v_i226v_stable_now/">reports of instability with Intel’s 2.5 GbE network
cards</a>.</p>
<p>That said, aside from the annoying firmware requirements, the Realtek 2.5 GbE
card seems to work fine for me in practice.</p>
<h3 id="mainboard">Mainboard</h3>
<p>Despite the suboptimal network card choice, I decided to stick to the ASUS PRIME
series of mainboards, as I made good experiences with those in my past few
builds. Here are a couple of thoughts on the ASUS PRIME Z890-P mainboard I went
with:</p>
<ul>
<li>I like the quick-release PCIe mechanism: ASUS understood that people had
trouble unlocking large graphics cards from their PCIe slot, so they added a
lever-like mechanism that is easily reachable. In my couple of usages, this
worked pretty well!</li>
<li>I wrote about <a href="/posts/2022-01-15-high-end-linux-pc/#slow-boot">slow boot times with my 2022 PC
build</a> that were caused by
time-consuming memory training. On this ASUS board, I noticed that the board
blinks the Power LED to signal that memory training is in progress. Very nice!
It hadn’t occurred to me previously that the various phases of the boot could
be signaled by different Power LED blinking patterns :)
<ul>
<li>The downside of this feature is: While the machine is in suspend-to-RAM, the
Power LED also blinks! This is annoying, so I might just disconnect the
Power LED entirely.</li>
</ul>
</li>
<li>The UEFI firmware includes what they call a Q-Dashboard: An overview of what
is installed/connected in which slot. Quite nice:</li>
</ul>















<a href="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4809.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4809_hu_9cc794b41e61aa66.jpg 2x,https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4809_hu_2e30747227d38ee6.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4809_hu_2bc1c1ff7fd21242.jpg"
  
  width="600"
  height="357"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>One surprising difference between the two mainboards I tested was that the
AsRock Z890 Pro-A does not seem to report the correct DIMM clock in <code>lshw</code>,
whereas the ASUS does:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#a00000">--- lshw-intel-285k-asrock.txt	2025-04-30 20:35:24 +0200
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+++ lshw-intel-285k-asus.txt		2025-04-30 21:39:52 +0200
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>      *-firmware
</span></span><span style="display:flex;"><span>           description: BIOS
</span></span><span style="display:flex;"><span><span style="color:#a00000">-          vendor: American Megatrends International, LLC.
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+          vendor: American Megatrends Inc.
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>           physical id: 0
</span></span><span style="display:flex;"><span><span style="color:#a00000">-          version: 2.25
</span></span></span><span style="display:flex;"><span><span style="color:#a00000">-          date: 03/24/2025
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+          version: 1601
</span></span></span><span style="display:flex;"><span><span style="color:#00a000">+          date: 02/07/2025
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>           size: 64KiB
</span></span><span style="display:flex;"><span><span style="color:#a00000">-          capacity: 32MiB
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+          capacity: 16MiB
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>           capabilities: pci upgrade shadowing cdboot bootselect socketedrom edd acpi biosbootspecification uefi
</span></span><span style="display:flex;"><span>[…]
</span></span><span style="display:flex;"><span>      *-memory
</span></span><span style="display:flex;"><span>           description: System Memory
</span></span><span style="display:flex;"><span><span style="color:#a00000">-          physical id: 9
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+          physical id: e
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>           slot: System board or motherboard
</span></span><span style="display:flex;"><span>           size: 64GiB
</span></span><span style="display:flex;"><span>         *-bank:0
</span></span><span style="display:flex;"><span><span style="color:#a00000">-             description: DIMM [empty]
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+             description: [empty]
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>              physical id: 0
</span></span><span style="display:flex;"><span>              slot: Controller0-ChannelA-DIMM0
</span></span><span style="display:flex;"><span>         *-bank:1
</span></span><span style="display:flex;"><span><span style="color:#a00000">-             description: DIMM Synchronous 4800 MHz (0,2 ns)
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+             description: DIMM Synchronous 6400 MHz (0,2 ns)
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>              product: CMK64GX5M2B6400C32
</span></span><span style="display:flex;"><span>              vendor: Corsair
</span></span><span style="display:flex;"><span>              physical id: 1
</span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -40,13 +42,13 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span>              slot: Controller0-ChannelA-DIMM1
</span></span><span style="display:flex;"><span>              size: 32GiB
</span></span><span style="display:flex;"><span>              width: 64 bits
</span></span><span style="display:flex;"><span><span style="color:#a00000">-             clock: 505MHz (2.0ns)
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+             clock: 2105MHz (0.5ns)
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>         *-bank:2
</span></span><span style="display:flex;"><span><span style="color:#a00000">-             description: DIMM [empty]
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+             description: [empty]
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>              physical id: 2
</span></span><span style="display:flex;"><span>              slot: Controller0-ChannelB-DIMM0
</span></span><span style="display:flex;"><span>         *-bank:3
</span></span><span style="display:flex;"><span><span style="color:#a00000">-             description: DIMM Synchronous 4800 MHz (0,2 ns)
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+             description: DIMM Synchronous 6400 MHz (0,2 ns)
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>              product: CMK64GX5M2B6400C32
</span></span><span style="display:flex;"><span>              vendor: Corsair
</span></span><span style="display:flex;"><span>              physical id: 3
</span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold">@@ -54,7 +56,7 @@
</span></span></span><span style="display:flex;"><span><span style="color:#800080;font-weight:bold"></span>              slot: Controller0-ChannelB-DIMM1
</span></span><span style="display:flex;"><span>              size: 32GiB
</span></span><span style="display:flex;"><span>              width: 64 bits
</span></span><span style="display:flex;"><span><span style="color:#a00000">-             clock: 505MHz (2.0ns)
</span></span></span><span style="display:flex;"><span><span style="color:#a00000"></span><span style="color:#00a000">+             clock: 2105MHz (0.5ns)
</span></span></span><span style="display:flex;"><span><span style="color:#00a000"></span>[…]
</span></span></code></pre></div><p>I haven’t checked if there are measurable performance differences (e.g. if the
XMP profile is truly active), but at least you now know to not necessarily trust
what <code>lshw</code> can show you.</p>
<h3 id="cpu-fan">CPU fan</h3>
<p>I am a long-time fan of Noctua’s products: This company makes silent fans with
great cooling capacity that work reliably! For many years, I have swapped out
all the fans of each of my PCs with Noctua fans, and it was always an
upgrade. Highly recommended.</p>
<p>Hence, it is no question that I picked the latest and greatest Noctua CPU cooler
for this build: the Noctua NH-D15 G2. There are a couple of things to pay
attention to with this cooler:</p>
<ul>
<li>I decided to configure it with one fan instead of two fans: Using only one fan
will be the quietest setup, yet still have plenty of cooling capacity for this
setup.</li>
<li>There are 3 different versions that differ in how their base plate is
shaped. Noctua recommends: “For LGA1851, we generally recommend the regular
standard version with medium base convexity”
(<a href="https://noctua.at/en/intel-lga1851-all-you-need-to-know">https://noctua.at/en/intel-lga1851-all-you-need-to-know</a>)</li>
<li>With a height of 168 mm, this cooler fits well into the Fractal Define 7
Compact Black.</li>
</ul>
<h3 id="cpu-and-gpu-idle-power-vs-peak-performance">CPU and GPU: Idle Power vs. Peak Performance</h3>
<h4 id="cpu-choice-intel-over-amd">CPU choice: Intel over AMD</h4>
<p>Probably the point that raises most questions about this build is why I selected
an Intel CPU over an AMD CPU. The primary reason is that Intel CPUs are so much
better at power saving!</p>
<p>Let me explain: Most benchmarks online are for gamers and hence measure a usage
curve that goes “start game, run PC at 100% resources for hours”. Of course,
when you never let the machine idle, you would care about <em>power efficiency</em>:
how much power do you need to use to achieve the desired result?</p>
<p>My use-case is software development, not gaming. My usage curve oscillates
between “barely any usage because Michael is reading text” to “complete this
compilation as quickly as possible with all the power available”. For me, I need
both absolute power consumption at idle, and absolute performance to be
best-of-class.</p>
<p>AMD’s CPUs offer great performance (the recently released <a href="https://www.phoronix.com/review/amd-ryzen-9-9950x3d-linux">Ryzen 9 9950X3D is
even faster</a> than the
Intel 9 285K), and have great <em>power efficiency</em>, but poor <em>power consumption</em>
at idle: With ≈35W of idle power draw, Zen 5 CPUs consume ≈3x as much power as
Intel CPUs!</p>
<p>Intel’s CPUs offer great performance (like AMD), but excellent power consumption
at idle.</p>
<p>Therefore, I can’t in good conscience buy an AMD CPU, but if you want a fast
gaming-only PC or run an always-loaded HPC cluster with those CPUs, definitely
go ahead :)</p>
<h4 id="graphics-card-nvidia-over-amd">Graphics card: nvidia over AMD</h4>
<p>I don’t necessarily recommend any particular nvidia graphics card, but I have
had to stick to nvidia cards because they are the only option that work with my
picky <a href="/posts/2017-12-11-dell-up3218k/">Dell UP3218K monitor</a>.</p>
<p>From time to time, I try out different graphics cards. Recently, I got myself an
AMD Radeon RX 9070 because I read that it works well with open source drivers.</p>
<p>While the Radeon RX 9070 works with my monitor (great!), it seems to consume 45W
in idle, which is much higher than my nvidia cards, which idle at ≈ 20W. This is
unacceptable to me: Aside from high power costs and wasting precious resources,
the high power draw also means that my room will be hotter in summer and the
fans need to spin faster and therefore louder.</p>
<p>People asked me on Social Media if this could be a measurement error (like, the
card reporting inaccurate values), so I double-checked with a <a href="https://mystrom.ch/de/wifi-switch/">myStrom WiFi
Switch</a> and confirmed that with the Radeon
card, the PC indeed draws 20-30W more from the wall socket.</p>
<h4 id="why-low-idle-power-is-so-important">Why Low Idle Power is so important</h4>
<p>In the comments for my <a href="/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/">my previous blog post about the first build of this
machine not running
stable</a>,
people were asking why it is worth it to optimize a few watts of power
usage. People calculate what higher power usage might cost, put it in relation
to the total cost of the components, and conclude that saving ±10% of the price
can’t possibly be worth the effort.</p>
<p>Let me try to illustrate the importance of low idle power with this anecdote:
For one year, I was suffering from an nvidia driver bug that meant the GPU would
not clock down to the most efficient power-saving mode (because of the high
resolution of my monitor). The 10-20W of difference should have been
insignificant. Yet, when the bug was fixed, I noticed how my PC got quieter
(fans don’t need to spin up) and my room noticeably cooled down, which was great
as it was peak temperatures in summer.</p>
<p>To me, having a whisper-quiet computing environment that does not heat up my
room is a great, actual, real-life, measurable benefit. Not wasting resources
and saving a tiny amount of money is a nice cherry on top.</p>
<p>Obviously all the factors are very dependent on your specific situation: Your
house’s thermal behavior might differ from mine, your tolerance for noise
(and/or baseline noise levels) might be different, you might put more/less
weight on resource usage, etc.</p>
<h2 id="installation">Installation</h2>
<h3 id="uefi-setup">UEFI setup</h3>
<p>On the internet, I read that there was some issue related to the Power Limits
that mainboards come with by default. Therefore, I did a <a href="https://www.asus.com/motherboards-components/motherboards/prime/prime-z890-p/helpdesk_bios?model2Name=PRIME-Z890-P">UEFI firmware
update</a>
immediately after getting the mainboard. I upgraded to version 1404 (2025/01/10)
using the provided ZIP file (<code>PRIME-Z890-P-ASUS-1404.zip</code>) on an MS-DOS
FAT-formatted USB stick with the EZ Flash tool in the UEFI firmware
interface. Tip: do not extract the ZIP file, otherwise the EZ Flash tool cannot
update the Intel ME firmware. Just put the ZIP file onto the USB disk as-is.</p>
<p>I verified that with this UEFI version, the <code>Power Limit 1 (PL1)</code> is 250W, and
<code>ICCMAX=347A</code>, which are exactly the values that Intel recommends. Great!</p>
<p>I also enabled XMP and verified that memtest86 reported no errors.</p>
<h3 id="software-setup-early-adopter-pains">Software setup: early adopter pains</h3>
<p>To copy over the data from the old disk to the new disk, I wanted to boot a live
linux distribution (specifically, <a href="https://grml.org/">grml.org</a>) and follow my
usual procedure: boot with the old disk and the new (empty) disk, then use <code>dd</code>
to copy the data. It’s nice and simple, hard to screw up.</p>
<p>Unfortunately, while grml 2024.12 technically does boot up, there are two big
problems:</p>
<ol>
<li>
<p>There is no network connectivity because the kernel and linux-firmware
versions are too old.</p>
<ul>
<li>Kernel commit <a href="https://github.com/torvalds/linux/commit/f75d1fbe7809bc5ed134204b920fd9e2fc5db1df">r8169: add support for
RTL8125D</a>
is not included.</li>
</ul>
</li>
<li>
<p>I could not get Xorg to work at all. Not with the Intel integrated GPU, nor
with the nvidia dedicated GPU. Not with <code>nomodeset</code> or any of the other
options in the grml menu. This wasn’t merely a convenience problem: I needed
to use <code>gparted</code> (the graphical version) for its partition moving/resizing
support.</p>
</li>
</ol>
<p>Ultimately, it was easier to upgrade my old PC to Linux 6.13 and linux-firmware
20250109, then put in the new disk and copy over the installation.</p>
<h3 id="trim-your-ssds">TRIM your SSDs</h3>
<p>SSD disks can degrade over time, so it is essential that the Operating System
tells the SSD firmware about freed-up blocks (for wear leveling). When using
full-disk encryption, all involved layers need to have TRIM support enabled.</p>
<p>I think I saw the effect of an incorrectly configured TRIM setup in action back
in 2022, <a href="/posts/2022-01-15-high-end-linux-pc/#copying-the-data">when I copied my data from a Force MP600 to a WD Black
SN850</a>, which
unexpectedly took many hours!</p>
<p>To make sure my disk has a long and healthy life, I double-checked that both
<a href="https://wiki.archlinux.org/title/Solid_state_drive">periodic and continuous TRIM are
enabled</a> on my Arch Linux
system: The <a href="https://manpages.debian.org/fstab.5"><code>fstab(5)</code></a>
 file contains the <code>discard</code>
option (and <a href="https://manpages.debian.org/mount.8"><code>mount(8)</code></a>
 lists the <code>discard</code> option),
and <code>fstrim.service</code> ran within the last week:</p>
<pre tabindex="0"><code>systemd[1]: Starting Discard unused blocks on filesystems from /etc/fstab...
fstrim[779617]: /boot: 10.1 GiB (10799427584 bytes) trimmed on /dev/nvme0n1p1
fstrim[779617]: /: 1.8 TiB (2018906263552 bytes) trimmed on /dev/mapper/cryptroot
systemd[1]: fstrim.service: Deactivated successfully.
</code></pre><p>Speaking of copying data: the transfer from my WD Black SN850 to my Samsung 990
PRO ran at 856 MB/s and took about 40 minutes in total.</p>
<h2 id="performance">Performance</h2>
<p>Here are the total times for a couple of typical workloads I run:</p>
<table>
  <thead>
      <tr>
          <th>Workload</th>
          <th><a href="/posts/2022-01-15-high-end-linux-pc/">12900K (2022)</a></th>
          <th>285K (2025)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://go.dev/dl/">build Go 1.24.3 (<code>cd src; ./make.bash</code>)</a></td>
          <td>≈35s</td>
          <td>≈26s</td>
      </tr>
      <tr>
          <td><a href="https://github.com/gokrazy/rsync/tree/0c5ac23ecf8b337dd5672c2ae9f945defa5d0b7f">gokrazy/rsync tests (<code>make test</code>)</a></td>
          <td>≈0.5s</td>
          <td>≈0.4s</td>
      </tr>
      <tr>
          <td><a href="https://github.com/gokrazy/gokrazy/">gokrazy UEFI test (<code>go test ./integration/...</code>)</a></td>
          <td>≈30s</td>
          <td>≈10s</td>
      </tr>
      <tr>
          <td><a href="https://github.com/gokrazy/kernel/tree/699ad7a064b8702dbe91b801ea21c2da2f0e9737">gokrazy Linux compile (<code>gokr-rebuild-kernel -cross=arm64</code>)</a></td>
          <td>3m 13s</td>
          <td>2m 7s</td>
      </tr>
  </tbody>
</table>
<p>The performance boost is great! Building Linux kernels a whole minute faster is
really nice.</p>
<h2 id="stability-issues">Stability issues</h2>
<p>In March, I published <a href="/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/">an article about how the first build of this machine was
not stable</a>,
in which you can read in detail about the various crashes I ran into.</p>
<p>Now, in early May, I know for sure that the CPU was defective, after a lengthy
trouble-shooting in which I swapped out <strong>all</strong> the other parts of this PC, sent
back the CPU and got a new one.</p>
<p>The CPU was the most annoying component to diagnose in this build because it’s
an LGA 1851 socket and I don’t (yet) have any other machines which uses
that same socket. AMD’s approach of sticking to each socket for a longer time
would have been better in this situation.</p>
<h3 id="stress-testing">Stress testing</h3>
<p>When I published my earlier blog post about the PC being unstable, I did not
really know how to reliably trigger the issue. Some compute-intensive tasks like
running a Django test suite seemed to trigger the issue. I <em>suspect</em> that the
problem somehow got worse, because when I started stress testing the machine,
suddenly it would crash <strong>every time</strong> when building a Linux kernel.</p>
<p>That got me curious to see if other well-known CPU stress testers like
<a href="https://en.wikipedia.org/wiki/Prime95">Prime95</a> would show problems, and
indeed: within seconds, Prime95 would report errors.</p>
<p>I figured I would use Prime95 as a quick signal: if it reports errors, the
machine is faulty. This typically happens within seconds of starting Prime95.</p>
<p>If Prime95 reported no errors, I would use Linux kernel compilation as a slow
signal: if I can successfully build a kernel, the machine is likely stable
enough.</p>
<p>The specific setup I used is to run <code>./mprime -m</code>, hit N (do not participate in
distributed computation projects), then Enter a few times to confirm the
defaults. Eventually, Prime95 starts calculating, which pushes the CPU to 100%
usage (see the <a href="https://manpages.debian.org/dstat.1"><code>dstat(1)</code></a>
-like output by my
<a href="https://github.com/gokrazy/stat"><code>gokrazy/stat</code></a> implementation) and draws the
expected ≈300W of power from the wall:</p>















<a href="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_5271.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_5271_hu_a483aa202805316.jpg 2x,https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_5271_hu_1496b18130c8ac83.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_5271_hu_bba47b0510adbc9c.jpg"
  alt="photo of running Prime95 to stress-test a CPU" title="photo of running Prime95 to stress-test a CPU"
  width="600"
  height="339"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>


















<a href="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/2025-05-04-power.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/2025-05-04-power_hu_40bc570775db33a0.jpg 2x,https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/2025-05-04-power_hu_528a10bbc1d5b8a1.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/2025-05-04-power_hu_f6e7bcb349fedc2f.jpg"
  alt="screenshot of PC power usage" title="screenshot of PC power usage"
  width="600"
  height="216"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>In addition, I also ran <a href="https://en.wikipedia.org/wiki/MemTest86">MemTest86</a> for
a few hours:</p>















<a href="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4821.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4821_hu_1bd21d248405fe7.jpg 2x,https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4821_hu_cc06de0f056aaed9.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_4821_hu_5b10ba64b206ef86.jpg"
  alt="photo of running memtest86" title="photo of running memtest86"
  width="600"
  height="306"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>To be clear: I also successfully ran MemTest86 on the previous, unstable build,
so only running MemTest86 is not good enough if you are dealing with a faulty
CPU.</p>
<h3 id="rma-timeline">RMA timeline</h3>
<ul>
<li>Jan 15th: I receive the components for my new PC
<ul>
<li>In January and February, the PC crashes occasionally.</li>
</ul>
</li>
<li>Mar 4th: I switch back to my old PC and start writing my blog post</li>
<li>Mar 19th: I publish my blog post about the machine not being stable
<ul>
<li>The online discussion does not result in any interesting tips or leads.</li>
</ul>
</li>
<li>Mar 20th: I order the AsRock Z890 Pro-A mainboard to ensure the mainboard is OK</li>
<li>Mar 24th: the AsRock Z890 Pro-A arrives</li>
<li>Apr 5th (Sat): started an RMA for the CPU
<ul>
<li>They ask me to send the CPU to orderflow, which is the merchant that fulfilled my order.</li>
<li>Typically, I prefer buying directly at digitec, but many PC components seem
to only be available from orderflow on digitec nowadays.</li>
</ul>
</li>
<li>Apr 9th (Wed): package arrives at orderflow (digitec gave me a non-priority return label)</li>
<li>Apr 14th (Mon): I got the following mail from digitec’s customer support and
had to explain that I have thoroughly diagnosed the CPU as defective (a link
to my blog post was sufficient):</li>
</ul>
<blockquote>
<p>Händler hat dies beim Hersteller angemeldet und dieser hat folgende Fragen:</p>
<p>Um sicherzugehen, dass wir Sie richtig verstehen: Sie haben die CPU auf zwei
verschiedenen Motherboards getestet und das gleiche Problem besteht weiterhin?</p>
<p>Könnten Sie uns mitteilen, welche Marke und welches Modell die beiden
verwendeten Motherboards sind?</p>
<p>Wurde auf beiden Motherboards die neueste BIOS-Version verwendet?</p>
<p>Bestand das Problem von Anfang an oder trat es erst später auf?</p>
<p>Wurde der Prozessor übertaktet? (Bitte beachten Sie, dass durch Übertakten die
Garantie erlischt.)</p>
</blockquote>
<ul>
<li>Apr 25th (Fri): orderflow hands the replacement CPU to Swiss Post</li>
<li>May 1st (Thu): the machine successfully passes stress tests; I start using it</li>
</ul>
<p>In summary, I spent March without a working PC, but that was because I didn’t
have much time to pursue the project. Then, I spent April without a working PC
because RMA&rsquo;ing an Intel CPU through digitec seems pretty slow. I would have
wished for a little more trust and a replacement CPU right away.</p>
<h2 id="conclusion">Conclusion</h2>
<p>What a rollercoaster and time sink this build was! I have never received a
faulty-on-arrival CPU in my entire life before. How did the CPU I first received
pass Intel’s quality control? Or did it pass QC, but was damaged in transport? I
will probably never know.</p>
<p>From now on, I know to extensively stress test new PC builds for stability to
detect such issues quicker. Should the CPU be faulty, unfortunately getting it
replaced is a month-long process — it’s very annoying to have such a costly
machine just gather dust for a month.</p>
<p>But, once the faulty component was replaced, this is my best PC build yet:</p>















<a href="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_5278.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_5278_hu_f29f24e40749dfe6.jpg 2x,https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_5278_hu_16b3f50e4be80194.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-05-15-my-2025-high-end-linux-pc/IMG_5278_hu_cc3023c1203ab816.jpg"
  
  width="600"
  height="800"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>The case is the perfect size for the components and offers incredibly convenient
access to all components throughout the entire lifecycle of this PC, including
the troubleshooting period, and the later stages of its life when this PC will
be rotated into its “lab machine” period before I sell it second-hand to someone
who will hopefully use the machine for another few years.</p>
<p>The machine is quiet, draws little power (for such a powerful machine) and
really packs a punch!</p>
<p>As usual, I run Linux on this PC and haven’t noticed any problems in my
day-to-day usage. I use <a href="/posts/2025-05-10-grobi-x11-monitor-autoconfig/#zleep">suspend-to-RAM multiple times a
day</a> without any issues.</p>
<p>I hope some of these details were interesting and useful to you in your own PC
builds!</p>
<p>If you want to learn about which peripherals I use aside from my 8K monitor
(e.g. the Kinesis Advantage keyboard, Logitech MX Ergo trackball, etc.), check
out my post <a href="/posts/2020-05-23-desk-setup/">stapelberg uses this: my 2020 desk
setup</a>. I might publish an updated version at
some point :)</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[In praise of grobi for auto-configuring X11 monitors]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-05-10-grobi-x11-monitor-autoconfig/"/>
    <id>https://michael.stapelberg.ch/posts/2025-05-10-grobi-x11-monitor-autoconfig/</id>
    <published>2025-05-10T08:24:00+02:00</published>
    <content type="html"><![CDATA[<p>I have recently started using the <a href="https://github.com/fd0/grobi/"><code>grobi</code> program by Alexander
Neumann</a> again and was delighted to discover that
it makes using my fiddly (but wonderful) <a href="/posts/2017-12-11-dell-up3218k/">Dell 32-inch 8K monitor
(UP3218K)</a> monitor much more convenient — I get
a signal more quickly than with my previous, sleep-based approach.</p>
<p>Previously, when my PC woke up from suspend-to-RAM, there were two scenarios:</p>
<ol>
<li>The monitor was connected. My <a href="#zleep">sleep program</a> would power on the
monitor (if needed), sleep a little while and then run <a href="https://manpages.debian.org/xrandr.1"><code>xrandr(1)</code></a>
 to (hopefully) configure the monitor correctly.</li>
<li>The monitor was not connected, for example because it was still connected to
my work PC.</li>
</ol>
<p>In scenario ②, or if the one-shot configuration attempt in scenario ① fails, I
would need to SSH in from a different computer and run <code>xrandr</code> manually so that
the monitor would show a signal:</p>
<pre tabindex="0"><code>% DISPLAY=:0 xrandr \
  --output DP-4 --mode 3840x4320 --panning 0x0+0+0 \
  --output DP-2 --right-of DP-4 --mode 3840x4320 --panning 0x0+3840+0
</code></pre><h2 id="automatic-monitor-configuration-with-grobi">Automatic monitor configuration with grobi</h2>
<p>I have now completely solved this problem by creating the following
<code>~/.config/grobi.conf</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#062873;font-weight:bold">rules</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">  </span>- <span style="color:#062873;font-weight:bold">name</span>:<span style="color:#bbb"> </span>UP3218K<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">outputs_connected</span>:<span style="color:#bbb"> </span>[DP-2, DP-4]<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic"># DP-4 is left, DP-2 is right</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">configure_row</span>:<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">        </span>- DP-4@3840x4320<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">        </span>- DP-2@3840x4320<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#60a0b0;font-style:italic"># atomic instructs grobi to only call xrandr once and configure all the</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#60a0b0;font-style:italic"># outputs. This does not always work with all graphic cards, but is</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic"># needed to successfully configure the UP3218K monitor.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">    </span><span style="color:#062873;font-weight:bold">atomic</span>:<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">true</span><span style="color:#bbb">
</span></span></span></code></pre></div><p>…and installing / enabling <code>grobi</code> (on Arch Linux) using:</p>
<pre tabindex="0"><code>% sudo pacman -S grobi
% systemctl --user enable --now grobi
</code></pre><p>Whenever <code>grobi</code> detects that my monitor is connected (it listens for <a href="https://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt">X11
RandR</a>
output change events), it will run <a href="https://manpages.debian.org/xrandr.1"><code>xrandr(1)</code></a>
 to
configure the monitor resolution and positioning.</p>
<p>To check what <code>grobi</code> is seeing/doing, you can use:</p>
<pre tabindex="0"><code>% systemctl --user status grobi
% journalctl --user -u grob
</code></pre><p>For example, on my system, I see:</p>
<pre tabindex="0"><code>grobi: 18:31:48.823765 outputs: [HDMI-0 (primary) DP-0 DP-1 DP-2 (connected) 3840x2160+ [DEL-16711-808727372-DELL UP3218K-D2HP805I043L] DP-3 DP-4 (connected) 3840x21&gt;
grobi: 18:31:48.823783 new rule found: UP3218K
grobi: 18:31:48.823785 enable outputs: [DP-4@3840x4320 DP-2@3840x4320]
grobi: 18:31:48.823789 using one atomic call to xrandr
grobi: 18:31:48.823806 running command /usr/bin/xrandr xrandr --output DP-4 --mode 3840x4320 --output DP-2 --mode 3840x4320 --right-of DP-4
grobi: 18:31:49.285944 new RANDR change event received
</code></pre><p>Notably, the instructions for getting out of a bad state (no signal) are now to
power off the monitor and power it back on again. This will result in RandR
output change events, which will trigger <code>grobi</code>, which will run <code>xrandr</code>, which
configures the monitor. Nice!</p>
<h2 id="why-not-autorandr">Why not autorandr?</h2>
<p>No particular reason. I knew <code>grobi</code>.</p>
<p>If nothing else, <code>grobi</code> is written in Go, so it’s likely to keep working
smoothly over the years.</p>
<h2 id="does-grobi-work-on-wayland">Does grobi work on Wayland?</h2>
<p>Probably not. There is no mention of Wayland over on the <a href="https://github.com/fd0/grobi/">grobi
repository</a>.</p>
<h2 id="zleep">Bonus: my Suspend-to-RAM setup</h2>
<p>As a bonus, this section describes the other half of my monitor-related
automation.</p>
<p>When I suspend my PC to RAM, I either want to wake it up manually later, for
example by pressing a key on the keyboard or by sending a Wake-on-LAN packet, or
I want it to wake up automatically each morning at 6:50 — that way, daily cron
jobs have some time to run before I start using the computer.</p>
<p>To accomplish this, I use <code>zleep</code>, a wrapper program around <a href="https://manpages.debian.org/rtcwake.8"><code>rtcwake(8)</code></a>
 and <code>systemctl suspend</code> that integrates with the
myStrom switch smart plug to turn off power to the monitor entirely. This is
worthwhile because the monitor draws 30W even in standby!</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">package</span><span style="color:#bbb"> </span>main<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">import</span><span style="color:#bbb"> </span>(<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;context&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;flag&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;fmt&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;log&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;net/http&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;net/url&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;os&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;os/exec&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#4070a0">&#34;time&#34;</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">var</span><span style="color:#bbb"> </span>(<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>resume<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>flag.<span style="color:#06287e">Bool</span>(<span style="color:#4070a0">&#34;resume&#34;</span>,<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#4070a0">&#34;run resume behavior only (turn on monitor via smart plug)&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>noMonitor<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>flag.<span style="color:#06287e">Bool</span>(<span style="color:#4070a0">&#34;no_monitor&#34;</span>,<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">false</span>,<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#4070a0">&#34;disable turning off/on monitor&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">monitorPower</span>(ctx<span style="color:#bbb"> </span>context.Context,<span style="color:#bbb"> </span>method,<span style="color:#bbb"> </span>cmnd<span style="color:#bbb"> </span><span style="color:#902000">string</span>)<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span><span style="color:#666">*</span>noMonitor<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;[monitor power] skipping because -no_monitor flag is set&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;[monitor power] command: %v&#34;</span>,<span style="color:#bbb"> </span>cmnd)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>u,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>url.<span style="color:#06287e">Parse</span>(<span style="color:#4070a0">&#34;http://myStrom-Switch-A46FD0/&#34;</span><span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span>cmnd)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">for</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>ctx.<span style="color:#06287e">Err</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>req,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>http.<span style="color:#06287e">NewRequest</span>(method,<span style="color:#bbb"> </span>u.<span style="color:#06287e">String</span>(),<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>ctx,<span style="color:#bbb"> </span>canc<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>context.<span style="color:#06287e">WithTimeout</span>(ctx,<span style="color:#bbb"> </span><span style="color:#40a070">5</span><span style="color:#666">*</span>time.Second)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span><span style="color:#06287e">canc</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>req<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>req.<span style="color:#06287e">WithContext</span>(ctx)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>resp,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>http.DefaultClient.<span style="color:#06287e">Do</span>(req)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>log.<span style="color:#06287e">Print</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>time.<span style="color:#06287e">Sleep</span>(<span style="color:#40a070">1</span><span style="color:#bbb"> </span><span style="color:#666">*</span><span style="color:#bbb"> </span>time.Second)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">continue</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>resp.StatusCode<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span>http.StatusOK<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;unexpected HTTP status code: got %v, want %v&#34;</span>,<span style="color:#bbb"> </span>resp.Status,<span style="color:#bbb"> </span>http.StatusOK)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>time.<span style="color:#06287e">Sleep</span>(<span style="color:#40a070">1</span><span style="color:#bbb"> </span><span style="color:#666">*</span><span style="color:#bbb"> </span>time.Second)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">continue</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;[monitor power] request succeeded&#34;</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">nextWakeup</span>(now<span style="color:#bbb"> </span>time.Time)<span style="color:#bbb"> </span>time.Time<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>midnight<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>time.<span style="color:#06287e">Date</span>(now.<span style="color:#06287e">Year</span>(),<span style="color:#bbb"> </span>now.<span style="color:#06287e">Month</span>(),<span style="color:#bbb"> </span>now.<span style="color:#06287e">Day</span>(),<span style="color:#bbb"> </span><span style="color:#40a070">0</span>,<span style="color:#bbb"> </span><span style="color:#40a070">0</span>,<span style="color:#bbb"> </span><span style="color:#40a070">0</span>,<span style="color:#bbb"> </span><span style="color:#40a070">0</span>,<span style="color:#bbb"> </span>time.Local)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>now.<span style="color:#06287e">Hour</span>()<span style="color:#bbb"> </span>&lt;<span style="color:#bbb"> </span><span style="color:#40a070">6</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#60a0b0;font-style:italic">// wake up today</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>midnight.<span style="color:#06287e">Add</span>(<span style="color:#40a070">6</span><span style="color:#666">*</span>time.Hour<span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span><span style="color:#40a070">50</span><span style="color:#666">*</span>time.Minute)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// wake up tomorrow</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>midnight.<span style="color:#06287e">Add</span>(<span style="color:#40a070">24</span><span style="color:#bbb"> </span><span style="color:#666">*</span><span style="color:#bbb"> </span>time.Hour).<span style="color:#06287e">Add</span>(<span style="color:#40a070">6</span><span style="color:#666">*</span>time.Hour<span style="color:#bbb"> </span><span style="color:#666">+</span><span style="color:#bbb"> </span><span style="color:#40a070">50</span><span style="color:#666">*</span>time.Minute)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">runResume</span>()<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// Retry for up to one minute to give the network some time to come up</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>ctx,<span style="color:#bbb"> </span>canc<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>context.<span style="color:#06287e">WithTimeout</span>(context.<span style="color:#06287e">Background</span>(),<span style="color:#bbb"> </span><span style="color:#40a070">1</span><span style="color:#666">*</span>time.Minute)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">defer</span><span style="color:#bbb"> </span><span style="color:#06287e">canc</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">monitorPower</span>(ctx,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;GET&#34;</span>,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;relay?state=1&#34;</span>);<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>log.<span style="color:#06287e">Print</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">zleep</span>()<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>ctx<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>context.<span style="color:#06287e">Background</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>now<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>time.<span style="color:#06287e">Now</span>().<span style="color:#06287e">Truncate</span>(<span style="color:#40a070">1</span><span style="color:#bbb"> </span><span style="color:#666">*</span><span style="color:#bbb"> </span>time.Second)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>wakeup<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">nextWakeup</span>(now)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;now   : %v&#34;</span>,<span style="color:#bbb"> </span>now)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;wakeup: %v&#34;</span>,<span style="color:#bbb"> </span>wakeup)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>log.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;wakeup: %v (timestamp)&#34;</span>,<span style="color:#bbb"> </span>wakeup.<span style="color:#06287e">Unix</span>())<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// assumes hwclock is running in UTC (see timedatectl | grep local)</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// Power the monitor off in 15 seconds.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// mode=on is intentional: https://api.mystrom.ch/#e532f952-36ea-40fb-a180-a57b835f550e</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// - the switch will be turned on (already on, so this is a no-op)</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// - the switch will wait for 15 seconds</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// - the switch will be turned off</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">monitorPower</span>(ctx,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;POST&#34;</span>,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;timer?mode=on&amp;time=15&#34;</span>);<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>log.<span style="color:#06287e">Print</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>sleep<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>exec.<span style="color:#06287e">Command</span>(<span style="color:#4070a0">&#34;sh&#34;</span>,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;-c&#34;</span>,<span style="color:#bbb"> </span>fmt.<span style="color:#06287e">Sprintf</span>(<span style="color:#4070a0">&#34;sudo rtcwake -m no --verbose --utc -t %v &amp;&amp; sudo systemctl suspend&#34;</span>,<span style="color:#bbb"> </span>wakeup.<span style="color:#06287e">Unix</span>()))<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>sleep.Stdout<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>os.Stdout<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>sleep.Stderr<span style="color:#bbb"> </span>=<span style="color:#bbb"> </span>os.Stderr<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>fmt.<span style="color:#06287e">Printf</span>(<span style="color:#4070a0">&#34;running %v\n&#34;</span>,<span style="color:#bbb"> </span>sleep.Args)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>sleep.<span style="color:#06287e">Run</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>fmt.<span style="color:#06287e">Errorf</span>(<span style="color:#4070a0">&#34;%v: %v&#34;</span>,<span style="color:#bbb"> </span>sleep.Args,<span style="color:#bbb"> </span>err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">main</span>()<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>flag.<span style="color:#06287e">Parse</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span><span style="color:#666">*</span>resume<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">runResume</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>log.<span style="color:#06287e">Fatal</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">else</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#06287e">zsleep</span>();<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span>log.<span style="color:#06287e">Fatal</span>(err)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>To turn power to the monitor on after resuming, I placed the following shell
script in <code>/lib/systemd/system-sleep/zleep.sh</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#007020">#!/bin/sh
</span></span></span><span style="display:flex;"><span><span style="color:#007020"></span>
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">case</span> <span style="color:#4070a0">&#34;</span><span style="color:#bb60d5">$1</span><span style="color:#4070a0">&#34;</span> in
</span></span><span style="display:flex;"><span>	pre<span style="color:#666">)</span>	<span style="color:#007020">exit</span> <span style="color:#40a070">0</span>
</span></span><span style="display:flex;"><span>		;;
</span></span><span style="display:flex;"><span>	post<span style="color:#666">)</span>	/usr/local/bin/zleep -resume
</span></span><span style="display:flex;"><span>		<span style="color:#007020">exit</span> <span style="color:#40a070">0</span>
</span></span><span style="display:flex;"><span>		;;
</span></span><span style="display:flex;"><span> 	*<span style="color:#666">)</span>	<span style="color:#007020">exit</span> <span style="color:#40a070">1</span>
</span></span><span style="display:flex;"><span>		;;
</span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">esac</span>
</span></span></code></pre></div><p>Once power is on, grobi will detect and configure the monitor.</p>
<p>Here is the program in action:</p>
<pre tabindex="0"><code>2025/05/06 21:58:32 now   : 2025-05-06 21:58:32 +0200 CEST
2025/05/06 21:58:32 wakeup: 2025-05-07 06:50:00 +0200 CEST
2025/05/06 21:58:32 wakeup: 1746593400 (timestamp)
2025/05/06 21:58:32 [monitor power] command: timer?mode=on&amp;time=15
2025/05/06 21:58:32 [monitor power] request succeeded
running [sh -c sudo rtcwake -m no --verbose --utc -t 1746593400 &amp;&amp; sudo systemctl suspend]
Using UTC time.
	delta   = 0
	tzone   = 0
	tzname  = UTC
	systime = 1746561512, (UTC) Tue May  6 19:58:32 2025
	rtctime = 1746561512, (UTC) Tue May  6 19:58:32 2025
alarm 1746593400, sys_time 1746561512, rtc_time 1746561512, seconds 0
rtcwake: wakeup using /dev/rtc0 at Wed May  7 04:50:00 2025
suspend mode: no; leaving
</code></pre>]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Intel 9 285K on ASUS Z890: not stable!]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/"/>
    <id>https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/</id>
    <published>2025-03-19T17:35:38+01:00</published>
    <content type="html"><![CDATA[<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Update (2025-05-15):</strong> Turns out the CPU was faulty! See <a href="/posts/2025-05-15-my-2025-high-end-linux-pc/">My 2025 high-end
Linux PC</a> for a new article on
this build, now with a working CPU.</div>
  </div>
</aside>

<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Update (2025-09-07):</strong> The replacement CPU also died and I have given up on
Intel. See <a href="/posts/2025-09-07-bye-intel-hi-amd-9950x3d/">Bye Intel, hi AMD!</a> for
more details on the AMD 9950X3D.</div>
  </div>
</aside>

<p>In January I ordered the components for a new PC and expected that I would
publish a successor to my <a href="/posts/2022-01-15-high-end-linux-pc/">2022 high-end Linux PC
🐧</a> article. Instead, I am now sitting on
a PC which regularly encounters crashes of the worst-to-debug kind, so I am
publishing this article as a warning for others in case you wanted to buy the
same hardware.</p>















<a href="https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/IMG_4799.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/IMG_4799_hu_7aa96d59a8a9f316.jpg 2x,https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/IMG_4799_hu_db2dc0cc8ca1d4af.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/IMG_4799_hu_25f81b95203a769b.jpg"
  
  width="600"
  height="450"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h2 id="components">Components</h2>
<p>Which components did I pick for this build? Here’s the full list:</p>
<table>
  <thead>
      <tr>
          <th>Price</th>
          <th>Type</th>
          <th>Article</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>140 CHF</td>
          <td>Case</td>
          <td><a href="https://www.digitec.ch/de/s1/product/fractal-define-7-compact-black-solid-atx-matx-mini-itx-pc-gehaeuse-13220301">Fractal Define 7 Compact Black Solid</a></td>
      </tr>
      <tr>
          <td>155 CHF</td>
          <td>Power Supply</td>
          <td><a href="https://www.digitec.ch/de/s1/product/corsair-rm850x-850-w-pc-netzteil-47356173?supplier=8560040">Corsair RM850x</a></td>
      </tr>
      <tr>
          <td>233 CHF</td>
          <td>Mainboard</td>
          <td><a href="https://www.digitec.ch/de/s1/product/asus-prime-z890-p-lga-1851-intel-z890-atx-mainboard-50252296">ASUS PRIME Z890-P</a></td>
      </tr>
      <tr>
          <td>620 CHF</td>
          <td>CPU</td>
          <td><a href="https://www.digitec.ch/de/s1/product/intel-core-ultra-9-285k-lga-1851-370-ghz-24-core-prozessor-49734792">Intel Core Ultra 9 285k</a></td>
      </tr>
      <tr>
          <td>120 CHF</td>
          <td>CPU fan</td>
          <td><a href="https://www.digitec.ch/de/s1/product/noctua-nh-d15-g2-168-mm-cpu-kuehler-46985628">Noctua NH-D15 G2</a></td>
      </tr>
      <tr>
          <td>39 CHF</td>
          <td>Case fan</td>
          <td><a href="https://www.digitec.ch/de/s1/product/noctua-nf-a14-pwm-140-mm-1-x-pc-luefter-657800?supplier=3204073">Noctua NF-A14 PWM (140 mm)</a></td>
      </tr>
      <tr>
          <td>209 CHF</td>
          <td>RAM</td>
          <td><a href="https://www.digitec.ch/de/s1/product/corsair-vengeance-2-x-32gb-6400-mhz-ddr5-ram-dimm-ram-24473300">64 GB DDR5-6400 Corsair Vengeance (2 x 32GB)</a></td>
      </tr>
      <tr>
          <td>280 CHF</td>
          <td>Disk</td>
          <td><a href="https://www.digitec.ch/de/s1/product/samsung-990-pro-4000-gb-m2-2280-ssd-37073751?supplier=406802">4000 GB Samsung 990 Pro</a></td>
      </tr>
      <tr>
          <td>940 CHF</td>
          <td>GPU</td>
          <td><a href="https://www.digitec.ch/de/s1/product/inno3d-geforce-rtx-4070-ti-x3-oc-12-gb-gddr6x-1-x-hdmi-3-x-dp-12-gb-grafikkarte-23664346?supplier=406802">Inno3D GeForce RTX4070 Ti</a></td>
      </tr>
  </tbody>
</table>
<p>Total: ≈1800 CHF, excluding the Graphics Card I re-used from a previous build.</p>
<p>…and the next couple of sections go into detail on how I selected these components.</p>
<h3 id="case">Case</h3>
<p>I have been a fan of Fractal cases for a couple of generations. In particular, I
realized that the “Compact” series offers plenty of space even for large
graphics cards and CPU coolers, so that’s now my go-to case: the Fractal Define
7 Compact (Black Solid).</p>
<p>I really like building components into the case and working with the case. There
are no sharp edges, the mechanisms are a pleasure to use and the
cable-management is well thought-out.</p>
<p>The only thing that wasn’t top-notch is that Fractal ships the case screws in
sealed plastic packages that you need to cut open. I would have wished for a
re-sealable plastic baggie so that one can keep the unused screws instead of
losing them.</p>
<h3 id="power-supply">Power Supply</h3>
<p>I wanted to keep my options open regarding upgrading to an nVidia 50xx series
graphics card at a later point. Those models have a TGP (“Total Graphics Power”)
of 575 watts, so I needed a power supply that delivers enough power for the
whole system even at peak power usage in all dimensions.</p>
<p>I ended up selecting the Corsair RM850x, which <a href="https://www.tomshardware.com/reviews/corsair-rm850x-2021-power-supply-review">reviews favoribly (“leader in
the 850W gold
category”)</a>
and was available at my electronics store of choice.</p>
<p>This was a good choice: the PSU indeed runs quiet, and I really like the power
cables (e.g. the GPU cable) that they include: they are very flexible, which
makes them easy to cable-manage.</p>
<h3 id="ssd-disk">SSD disk</h3>
<p>I have been avoiding PCIe 5 SSDs so far because they consume a lot more power
compared to PCIe 4 SSDs. While bulk streaming data transfer rates are higher on
PCIe 5 SSDs, random transfers are not significantly faster. Most of my compute
workload are random transfers, not large bulk transfers.</p>
<p>The power draw situation with PCIe 5 SSDs seems to be getting better lately,
with the Phison E31T being the first controller that implements power saving. A
disk that uses the E31T controller is the Corsair Force Series MP700
Elite. Unfortunately, said disk was unavailable when I ordered.</p>
<p>Instead, I picked the Samsung 990 Pro with 4 TB. I made good experiences with
the Samsung Pro series over the years (never had one die or degrade
performance), and my previous 2 TB disk is starting to fill up, so the extra
storage space is appreciated.</p>
<h3 id="mainboard">Mainboard</h3>
<p>One annoying realization is that most mainboard vendors seem to have moved to
2.5 GbE (= 2.5 Gbit/s ethernet) onboard network cards. I would have been
perfectly happy to play it safe and buy another Intel I225 1 GbE network card,
as long as it <em>just works</em> with Linux.</p>
<p>In the 2.5 GbE space, the main players seem to be Realtek and Intel. Most
mainboard vendors opted for Realtek as far as I could see.</p>
<p>Linux includes the <code>r8169</code> driver for Realtek network cards, but you need a
recent-enough Linux version (6.13+) that includes commit “<a href="https://github.com/torvalds/linux/commit/f75d1fbe7809bc5ed134204b920fd9e2fc5db1df">r8169: add support
for
RTL8125D</a>”,
accompanied by a recent-enough linux-firmware package. Even then, there is some
concern around stability and ASPM support. See for example <a href="https://serverfault.com/a/1169558">this ServerFault
post</a> by someone working on the <code>r8169</code>
driver.</p>
<p>Despite the Intel 1 GbE options being well-supported at this point, Intel’s 2.5
GbE options might not fare any better than the Realtek ones: I found <a href="https://www.reddit.com/r/HomeServer/comments/1cc0yuq/are_intel_25_gbe_nics_i225v_i226v_stable_now/">reports of
instability with Intel’s 2.5 GbE network
cards</a>.</p>
<p>Aside from the network cards, I decided to stick to the ASUS prime series of
mainboards, as I made good experiences with those in my past few builds. Here
are a couple of thoughts on the ASUS PRIME Z890-P mainboard I went with:</p>
<ul>
<li>I like the quick-release PCIe mechanism: ASUS understood that people had
trouble unlocking large graphics cards from their PCIe slot, so they added a
lever-like mechanism that is easily reachable. In my couple of usages, this
worked pretty well!</li>
<li>I wrote about <a href="/posts/2022-01-15-high-end-linux-pc/#slow-boot">slow boot times with my 2022 PC
build</a> that were caused by
time-consuming memory training. On this ASUS board, I noticed that they blink
the Power LED to signal that memory training is in progress. Very nice! It
hadn’t occurred to me previously that the various phases of the boot could be
signaled by different Power LED blinking patterns :)
<ul>
<li>The downside of this feature is: While the machine is in suspend-to-RAM, the
Power LED also blinks! This is annoying, so I might just disconnect the
Power LED entirely.</li>
</ul>
</li>
<li>The UEFI firmware includes what they call a Q-Dashboard: An overview of what
is installed/connected in which slot. Quite nice:</li>
</ul>















<a href="https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/IMG_4809.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/IMG_4809_hu_9cc794b41e61aa66.jpg 2x,https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/IMG_4809_hu_2e30747227d38ee6.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-03-19-intel-core-ultra-9-285k-on-asus-z890-not-stable/IMG_4809_hu_2bc1c1ff7fd21242.jpg"
  
  width="600"
  height="357"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<h3 id="cpu-fan">CPU fan</h3>
<p>I am a long-time fan of Noctua’s products: This company makes silent fans with
great cooling capacity that work reliably! For many years, I have swapped out
every of my PC’s fans with Noctua fans, and it was always an upgrade. Highly
recommended.</p>
<p>Hence, it is no question that I picked the latest and greatest Noctua CPU cooler
for this build: the Noctua NH-D15 G2. There are a couple of things to pay
attention to with this cooler:</p>
<ul>
<li>I decided to configure it with one fan instead of two fans: Using only one fan
will be the quietest setup, yet still have plenty of cooling capacity for this
setup.</li>
<li>There are 3 different versions that differ in how their base plate is
shaped. Noctua recommends: “For LGA1851, we generally recommend the regular
standard version with medium base convexity”
(<a href="https://noctua.at/en/intel-lga1851-all-you-need-to-know">https://noctua.at/en/intel-lga1851-all-you-need-to-know</a>)</li>
<li>The height of this cooler is 168 mm. This fits well into the Fractal Define 7
Compact Black.</li>
</ul>
<h3 id="cpu">CPU</h3>
<p>Probably the point that raises most questions about this build is why I selected
an Intel CPU over an AMD CPU. The primary reason is that Intel CPUs are so much
better at power saving!</p>
<p>Let me explain: Most benchmarks online are for gamers and hence measure a usage
curve that goes “start game, run PC at 100% resources for hours”. Of course,
when you never let the machine idle, you would care about <em>power efficiency</em>:
how much power do you need to use to achive the desired result?</p>
<p>My use-case is software development, not gaming. My usage curve oscillates
between “barely any usage because Michael is reading text” to “complete this
compilation as quickly as possible with all the power available”. For me, I need
both absolute power consumption at idle, and absolute performance to be
best-of-class.</p>
<p>AMD’s CPUs offer great performance (the recently released <a href="https://www.phoronix.com/review/amd-ryzen-9-9950x3d-linux">Ryzen 9 9950X3D is
even faster</a> than the
Intel 9 285K), and have great <em>power efficiency</em>, but poor <em>power consumption</em>
at idle: With ≈35W of idle power draw, Zen 5 CPUs consume ≈3x as much power as
Intel CPUs!</p>
<p>Intel’s CPUs offer great performance (like AMD), but excellent power consumption
at idle.</p>
<p>Therefore, I can’t in good conscience buy an AMD CPU, but if you want a fast
gaming-only PC or run an always-loaded HPC cluster with those CPUs, definitely
go ahead :)</p>
<h3 id="graphics-card">Graphics card</h3>
<p>I don’t necessarily recommend any particular nVidia graphics card, but I have
had to stick to nVidia cards because they are the only option that work with my
picky <a href="/posts/2017-12-11-dell-up3218k/">Dell UP3218K monitor</a>.</p>
<p>From time to time, I try out different graphics cards. Recently, I got myself an
AMD Radeon RX 9070 because I read that it works well with open source drivers.</p>
<p>While the Radeon RX 9070 works with my monitor (great!), it seems to consume 45W
in idle, which is much higher than my nVidia cards, which idle at ≈ 20W. This is
unacceptable to me: Aside from high power costs and wasting precious resources,
the high power draw also means that my room will be hotter in summer and the
fans need to spin faster and therefore louder.</p>
<p>Maybe I’ll write a separate article about the Radeon RX 9070.</p>
<h2 id="installation">Installation</h2>
<h3 id="uefi-setup">UEFI setup</h3>
<p>On the internet, I read that there was some issue related to the Power Limits
that mainboards come with by default. Therefore, I did a <a href="https://www.asus.com/motherboards-components/motherboards/prime/prime-z890-p/helpdesk_bios?model2Name=PRIME-Z890-P">UEFI firmware
update</a>
first thing after getting the mainboard. I upgraded to version 1404 (2025/01/10)
using the provided ZIP file (<code>PRIME-Z890-P-ASUS-1404.zip</code>) on an MS-DOS
FAT-formatted USB stick with the EZ Flash tool in the UEFI firmware
interface. Tip: do not extract the ZIP file, otherwise the EZ Flash tool cannot
update the Intel ME firmware. Just put the ZIP file onto the USB disk as-is.</p>
<p>I verified that with this UEFI version, the <code>Power Limit 1 (PL1)</code> is 250W, and
<code>ICCMAX=347A</code>, which are exactly the values that Intel recommends. Great!</p>
<p>I also enabled XMP and verified that memtest86 reported no errors.</p>
<h3 id="software-setup-early-adopter-pains">Software setup: early adopter pains</h3>
<p>To copy over the data from the old disk to the new disk, I wanted to boot a live
linux distribution (specifically, <a href="https://grml.org/">grml.org</a>) and follow my
usual procedure: boot with the old disk and the new (empty) disk, then use <code>dd</code>
to copy the data. It’s nice and simple, hard to screw up.</p>
<p>Unfortunately, while grml 2024.12 technically does boot up, there are two big
problems:</p>
<ol>
<li>
<p>There is no network connectivity because the kernel and linux-firmware
versions are too old.</p>
<ul>
<li><a href="https://github.com/torvalds/linux/commit/f75d1fbe7809bc5ed134204b920fd9e2fc5db1df">r8169: add support for RTL8125D</a></li>
</ul>
</li>
<li>
<p>I could not get Xorg to work at all. Not with the Intel integrated GPU, nor
with the nVidia dedicated GPU. Not with <code>nomodeset</code> or any of the other
options in the grml menu. This wasn’t merely a convenience problem: I needed
to use <code>gparted</code> (the graphical version) for its partition moving/resizing
support.</p>
</li>
</ol>
<p>Ultimately, it was easier to upgrade my old PC to Linux 6.13 and linux-firmware
20250109, then put in the new disk and copy over the installation.</p>
<h2 id="stability-issues">Stability issues</h2>
<p>At this point (early February), I switched to this new machine as my main PC.</p>
<p>Unfortunately, I could never get it to run stable! This journal shows you some
of the issues I faced and what I tried to troubleshoot them.</p>
<h3 id="xorg-dying-after-resume-from-suspend">Xorg dying after resume-from-suspend</h3>
<p>One of the first issues I encountered with this system was that after resuming
from suspend-to-RAM, I was greeted with a login window instead of my X11
session. The logs say:</p>
<pre tabindex="0"><code>(EE) NVIDIA(GPU-0): Failed to acquire modesetting permission.
(EE) Fatal server error:
(EE) EnterVT failed for screen 0
(EE) 
(EE) 
(EE) Please also check the log file at &#34;/var/log/Xorg.0.log&#34; for additional information.
(EE) 
(WW) NVIDIA(0): Failed to set the display configuration
(WW) NVIDIA(0):  - Setting a mode on head 0 failed: Insufficient permissions
(WW) NVIDIA(0):  - Setting a mode on head 1 failed: Insufficient permissions
(WW) NVIDIA(0):  - Setting a mode on head 2 failed: Insufficient permissions
(WW) NVIDIA(0):  - Setting a mode on head 3 failed: Insufficient permissions
(EE) Server terminated with error (1). Closing log file.
</code></pre><p>I couldn’t find any good tips online for this error message, so I figured I’d
wait and see how frequently this happens before investigating further.</p>
<h3 id="feb-18-xhci-host-controller-dying">Feb 18: xHCI host controller dying</h3>
<p>On Feb 18th, after resume-from-suspend, none of my USB peripherals would work
anymore! This affected <em>all USB ports</em> of the machine and could not be fixed,
not even by a reboot, until I fully killed power to the machine! In the kernel
log, I saw the following messages:</p>
<pre tabindex="0"><code>xhci_hcd 0000:80:14.0: xHCI host not responding to stop endpoint command
xhci_hcd 0000:80:14.0: xHCI host controller not responding, assume dead
xhci_hcd 0000:80:14.0: HC died; cleaning up
</code></pre><h3 id="feb-24-xhci-host-controller-dying">Feb 24: xHCI host controller dying</h3>
<p>The HC dying issue happened again when I was writing an SD card in my USB card
reader:</p>
<pre tabindex="0"><code>xhci_hcd 0000:80:14.0: HC died; cleaning up
</code></pre><h3 id="feb-24--uefi-update-disable-xmpp">Feb 24: → UEFI update, disable XMPP</h3>
<p>To try and fix the host controller dying issue, I updated the UEFI firmware to
version <code>1601</code> and disabled the XMPP RAM profile.</p>
<h3 id="feb-26--switch-back-from-geforce-4070-ti-to-3060-ti">Feb 26: → switch back from GeForce 4070 Ti to 3060 Ti</h3>
<p>To rule out any GPU-specific issues, I decided to switch back from the Inno3D
GeForce RTX4070 Ti to my older MSI GeForce RTX 3060 Ti.</p>
<h3 id="feb-28-pc-dying-on-suspend-to-ram">Feb 28: PC dying on suspend-to-RAM</h3>
<p>On Feb 28th, my PC did not resume from suspend-to-RAM. It would not even react
to a ping, I had to hard-reset the machine. When checking the syslog afterwards,
there are no entries.</p>
<p>I checked my power monitoring and saw that the machine consumed 50W (well above
idle power, and far above suspend-to-RAM power) throughout the entire
night. Hence, I suspect that the suspend-to-RAM did not work correctly and the
machine never actually suspended.</p>
<h3 id="mar-4th-pc-dying-when-running-django-tests">Mar 4th: PC dying when running django tests</h3>
<p>On March 4th, I was running the test suite for a medium-sized Django project (=
100% CPU usage) when I encountered a really hard crash: The machine stopped
working entirely, meaning all peripherals like keyboard and mouse stopped
responding, and the machine even did not respond to a network ping anymore.</p>
<p>At this point, I had enough and switched back to my 2022 PC.</p>
<h2 id="conclusion">Conclusion</h2>
<p>What use is a computer that doesn’t work? My hierarchy of needs contains
stability as the foundation, then speed and convenience. This machine exhausted
my tolerance for frustration with its frequent crashes.</p>
<p>Manawyrm <a href="https://chaos.social/@manawyrm/113772325172878092">actually warned me about the ASUS board</a>:</p>
<blockquote>
<p>ASUS boards are a typical gamble as always &ndash; they fired their firmware
engineers about 10 years ago, so you might get a nightmare of ACPI
troubleshooting hell now (or it&rsquo;ll just work). ASRock is worth a look as a
replacement if that happens. Electronics are usually solid, though&hellip;</p>
</blockquote>
<p>I didn’t expect that this PC would crash so hard, though. Like, if it couldn’t
suspend/resume that would be one thing (a dealbreaker, but somewhat expected and
understandable, probably fixable), but a machine that runs into a hard-lockup
when compiling/testing software? No thanks.</p>
<p>I will buy a different mainboard to see if that helps, likely the ASRock Z890
Pro-A. If you have any recommendations for a Z890 mainboard that actually works
reliably, please let me know!</p>
<p><strong>Update 2025-04-17:</strong> I have received the ASRock Z890 Pro-A, but the machine
shows exactly the same symptoms! I also swapped the power supply, which also did
not help. Running Prime95 crashed almost immediately. At this point, I have to
assume the CPU itself is defective and have started an RMA. I will post another
update once (if?) I get a replaced CPU.</p>
<p><strong>Update 2025-05-11:</strong> The CPU was faulty indeed! See <a href="/posts/2025-05-15-my-2025-high-end-linux-pc/">My 2025 high-end Linux
PC</a> for a new article on this
build, now with a working CPU.</p>
]]></content>
  </entry>
</feed>
