<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Michael Stapelbergs Website (Feed without AI posts)</title>
  <link href="https://michael.stapelberg.ch/feed-no-ai.xml" rel="self"/>
  <link href="https://michael.stapelberg.ch/"/>
  <id>https://michael.stapelberg.ch/feed-no-ai.xml</id>
  <generator>Hugo -- gohugo.io</generator>
  <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>
  <entry>
    <title type="html"><![CDATA[Tips to debug hanging Go programs]]></title>
    <link href="https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/"/>
    <id>https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/</id>
    <published>2025-02-27T17:51:38+01:00</published>
    <content type="html"><![CDATA[<p>I was helping someone get my <a href="https://github.com/gokrazy/rsync">gokrazy/rsync</a>
implementation set up to synchronize <a href="https://en.wikipedia.org/wiki/Resource_Public_Key_Infrastructure">RPKI
data</a> (used
for securing BGP routing infrastructure), when we discovered that with the right
invocation, my rsync receiver would just hang indefinitely.</p>
<p>This was a quick problem to solve, but in the process, I realized that I should
probably write down a few Go debugging tips I have come to appreciate over the
years!</p>
<h2 id="scenario-hanging-go-program">Scenario: hanging Go program</h2>
<p>If you want to follow along, you can reproduce the issue by building an older
version of gokrazy/rsync, just before the bug fix commit (you’ll need <a href="https://go.dev/dl/">Go 1.22
or newer</a>):</p>
<pre tabindex="0"><code>git clone https://github.com/gokrazy/rsync
cd rsync
git reset --hard 6c89d4dda3be055f19684c0ed56d623da458194e^
go install ./cmd/...
</code></pre><p>Now we can try to sync the repository:</p>
<pre tabindex="0"><code>% gokr-rsync \
  -rtO \
  --delete \
  rsync://rsync.paas.rpki.ripe.net/repository/ \
  /tmp/rpki-repo
[…]
2025/02/08 09:35:10 Opening TCP connection to rsync.paas.rpki.ripe.net:873
2025/02/08 09:35:10 rsync module &#34;repo&#34;, path &#34;repo/&#34;
2025/02/08 09:35:10 (Client) Protocol versions: remote=31, negotiated=27
2025/02/08 09:35:10 Client checksum: md4
2025/02/08 09:35:10 sending daemon args: [--server --sender -tr . repo/]
2025/02/08 09:35:10 exclusion list sent
2025/02/08 09:35:10 receiving file list
2025/02/08 09:35:11 [Receiver] i=0 ? . mode=40755 len=4096 uid=0 gid=0 flags=?
[…]
2025/02/08 09:35:11 [Receiver] i=89 ? clonoth/1/3139332e33322e3130302e302f32342d3234203d3e203537313936.roa mode=100644 len=1747 uid=0 gid=0 flags=?
</code></pre><p>…and then the program just sits there.</p>
<h2 id="sigquit-stack-trace">Tip 1: Press Ctrl+\ (SIGQUIT) to print a stack trace</h2>
<p>The easiest way to look at where a Go program is hanging is to press <code>Ctrl+\</code>
(backslash) to <a href="https://en.wikipedia.org/wiki/Signal_(IPC)#SIGQUIT">make the terminal send it a <code>SIGQUIT</code>
signal</a>. When the Go runtime
receives <code>SIGQUIT</code>, it prints a stack trace to the terminal before exiting the
process. This behavior is enabled by default and can be customized via the
<code>GOTRACEBACK</code> environment variable, see the <a href="https://pkg.go.dev/runtime"><code>runtime</code> package
docs</a>.</p>
<p>Here is what the output looks like in our case. I have made the font small so
that you can recognize the shape of the output (the details are not important,
continue reading below):</p>
<div style="font-size: 60%">
<pre tabindex="0"><code>^\SIGQUIT: quit
PC=0x47664e m=0 sigcode=128

goroutine 0 gp=0x6e6020 m=0 mp=0x6e6ec0 [idle]:
internal/runtime/syscall.Syscall6()
	/home/michael/sdk/go1.23.0/src/internal/runtime/syscall/asm_linux_amd64.s:36 +0xe fp=0x7ffc58665090 sp=0x7ffc58665088 pc=0x47664e
internal/runtime/syscall.EpollWait(0x586651e0?, {0x7ffc5866511c?, 0x3000000018?, 0x7ffc586651f0?}, 0x58665110?, 0x7ffc?)
	/home/michael/sdk/go1.23.0/src/internal/runtime/syscall/syscall_linux.go:32 +0x45 fp=0x7ffc586650e0 sp=0x7ffc58665090 pc=0x4765e5
runtime.netpoll(0xc0000000c0?)
	/home/michael/sdk/go1.23.0/src/runtime/netpoll_epoll.go:116 +0xd2 fp=0x7ffc58665768 sp=0x7ffc586650e0 pc=0x432332
runtime.findRunnable()
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:3580 +0x8c5 fp=0x7ffc586658e0 sp=0x7ffc58665768 pc=0x43f045
runtime.schedule()
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:3995 +0xb1 fp=0x7ffc58665918 sp=0x7ffc586658e0 pc=0x4405b1
runtime.park_m(0xc0000061c0)
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:4102 +0x1eb fp=0x7ffc58665970 sp=0x7ffc58665918 pc=0x4409cb
runtime.mcall()
	/home/michael/sdk/go1.23.0/src/runtime/asm_amd64.s:459 +0x4e fp=0x7ffc58665988 sp=0x7ffc58665970 pc=0x470e2e

goroutine 1 gp=0xc0000061c0 m=nil [IO wait]:
runtime.gopark(0x452658?, 0x0?, 0x98?, 0xb3?, 0xb?)
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:424 +0xce fp=0xc0000eb358 sp=0xc0000eb338 pc=0x46bc0e
runtime.netpollblock(0x4a01b8?, 0x4058e6?, 0x0?)
	/home/michael/sdk/go1.23.0/src/runtime/netpoll.go:575 +0xf7 fp=0xc0000eb390 sp=0xc0000eb358 pc=0x4318f7
internal/poll.runtime_pollWait(0x7ef586628808, 0x72)
	/home/michael/sdk/go1.23.0/src/runtime/netpoll.go:351 +0x85 fp=0xc0000eb3b0 sp=0xc0000eb390 pc=0x46af05
internal/poll.(*pollDesc).wait(0xc0000ce180?, 0xc00020e99c?, 0x0)
	/home/michael/sdk/go1.23.0/src/internal/poll/fd_poll_runtime.go:84 +0x27 fp=0xc0000eb3d8 sp=0xc0000eb3b0 pc=0x4b0ce7
internal/poll.(*pollDesc).waitRead(...)
	/home/michael/sdk/go1.23.0/src/internal/poll/fd_poll_runtime.go:89
internal/poll.(*FD).Read(0xc0000ce180, {0xc00020e99c, 0x4, 0x4})
	/home/michael/sdk/go1.23.0/src/internal/poll/fd_unix.go:165 +0x27a fp=0xc0000eb470 sp=0xc0000eb3d8 pc=0x4b17da
net.(*netFD).Read(0xc0000ce180, {0xc00020e99c?, 0x6eeea0?, 0x1?})
	/home/michael/sdk/go1.23.0/src/net/fd_posix.go:55 +0x25 fp=0xc0000eb4b8 sp=0xc0000eb470 pc=0x4f7e85
net.(*conn).Read(0xc000206000, {0xc00020e99c?, 0xc000212000?, 0x6e6ec0?})
	/home/michael/sdk/go1.23.0/src/net/net.go:189 +0x45 fp=0xc0000eb500 sp=0xc0000eb4b8 pc=0x5001a5
net.(*TCPConn).Read(0x0?, {0xc00020e99c?, 0xc0000eb568?, 0x46d449?})
	&lt;autogenerated&gt;:1 +0x25 fp=0xc0000eb530 sp=0xc0000eb500 pc=0x50bb25
io.ReadAtLeast({0x5d9640, 0xc000206000}, {0xc00020e99c, 0x4, 0x4}, 0x4)
	/home/michael/sdk/go1.23.0/src/io/io.go:335 +0x90 fp=0xc0000eb578 sp=0xc0000eb530 pc=0x4957d0
io.ReadFull(...)
	/home/michael/sdk/go1.23.0/src/io/io.go:354
encoding/binary.Read({0x5d9640, 0xc000206000}, {0x5da8b0, 0x7059a0}, {0x55e7c0, 0xc0000eb6a0})
	/home/michael/sdk/go1.23.0/src/encoding/binary/binary.go:244 +0xa5 fp=0xc0000eb670 sp=0xc0000eb578 pc=0x5102a5
github.com/gokrazy/rsync/internal/rsyncwire.(*MultiplexReader).ReadMsg(0xc00020a100)
	/home/michael/kr/rsync/internal/rsyncwire/wire.go:50 +0x48 fp=0xc0000eb6e8 sp=0xc0000eb670 pc=0x514428
github.com/gokrazy/rsync/internal/rsyncwire.(*MultiplexReader).Read(0x7ef5869b9a68?, {0xc000280000, 0x40000, 0x4dd4fb?})
	/home/michael/kr/rsync/internal/rsyncwire/wire.go:72 +0x2f fp=0xc0000eb788 sp=0xc0000eb6e8 pc=0x5145af
bufio.(*Reader).Read(0xc0002020c0, {0xc00020e998, 0x4, 0x40ece5?})
	/home/michael/sdk/go1.23.0/src/bufio/bufio.go:241 +0x197 fp=0xc0000eb7c0 sp=0xc0000eb788 pc=0x4d5a57
io.ReadAtLeast({0x5d93e0, 0xc0002020c0}, {0xc00020e998, 0x4, 0x4}, 0x4)
	/home/michael/sdk/go1.23.0/src/io/io.go:335 +0x90 fp=0xc0000eb808 sp=0xc0000eb7c0 pc=0x4957d0
io.ReadFull(...)
	/home/michael/sdk/go1.23.0/src/io/io.go:354
github.com/gokrazy/rsync/internal/rsyncwire.(*Conn).ReadInt32(0xc000208060)
	/home/michael/kr/rsync/internal/rsyncwire/wire.go:163 +0x4a fp=0xc0000eb850 sp=0xc0000eb808 pc=0x51490a
github.com/gokrazy/rsync/internal/receiver.(*Transfer).recvIdMapping1(0xc000202120, 0x5a9b58)
	/home/michael/kr/rsync/internal/receiver/uidlist.go:16 +0x3d fp=0xc0000eb8c0 sp=0xc0000eb850 pc=0x51fc7d
github.com/gokrazy/rsync/internal/receiver.(*Transfer).RecvIdList(0xc000202120)
	/home/michael/kr/rsync/internal/receiver/uidlist.go:52 +0x1dd fp=0xc0000eba08 sp=0xc0000eb8c0 pc=0x51ffbd
github.com/gokrazy/rsync/internal/receiver.(*Transfer).ReceiveFileList(0xc000202120)
	/home/michael/kr/rsync/internal/receiver/flist.go:229 +0x378 fp=0xc0000ebb10 sp=0xc0000eba08 pc=0x51c5b8
github.com/gokrazy/rsync/internal/receivermaincmd.clientRun({{0x5d9280, 0xc000078058}, {0x5d92a0, 0xc000078060}, {0x5d92a0, 0xc000078068}}, 0xc0000d0d90, {0x7ef53d47efc8, 0xc000206000}, {0x7ffc5866600e, ...}, ...)
	/home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:341 +0x5cd fp=0xc0000ebc10 sp=0xc0000ebb10 pc=0x550c2d
github.com/gokrazy/rsync/internal/receivermaincmd.socketClient({{0x5d9280, 0xc000078058}, {0x5d92a0, 0xc000078060}, {0x5d92a0, 0xc000078068}}, 0xc0000d0d90, {0x7ffc58665ff4?, 0x1?}, {0x7ffc5866600e, ...})
	/home/michael/kr/rsync/internal/receivermaincmd/clientserver.go:44 +0x425 fp=0xc0000ebcd0 sp=0xc0000ebc10 pc=0x54c205
github.com/gokrazy/rsync/internal/receivermaincmd.rsyncMain({{0x5d9280, 0xc000078058}, {0x5d92a0, 0xc000078060}, {0x5d92a0, 0xc000078068}}, 0xc0000d0d90, {0xc00007e440, 0x1, 0x2}, ...)
	/home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:160 +0x5d7 fp=0xc0000ebdf0 sp=0xc0000ebcd0 pc=0x54f697
github.com/gokrazy/rsync/internal/receivermaincmd.Main({0xc0000160a0, 0x5, 0x5}, {0x5d9280?, 0xc000078058?}, {0x5d92a0?, 0xc000078060?}, {0x5d92a0?, 0xc000078068?})
	/home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:394 +0x272 fp=0xc0000ebee8 sp=0xc0000ebdf0 pc=0x5510d2
main.main()
	/home/michael/kr/rsync/cmd/gokr-rsync/rsync.go:12 +0x4e fp=0xc0000ebf50 sp=0xc0000ebee8 pc=0x5515ae
runtime.main()
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:272 +0x28b fp=0xc0000ebfe0 sp=0xc0000ebf50 pc=0x438d4b
runtime.goexit({})
	/home/michael/sdk/go1.23.0/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc0000ebfe8 sp=0xc0000ebfe0 pc=0x472e61

goroutine 2 gp=0xc000006c40 m=nil [force gc (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:424 +0xce fp=0xc000074fa8 sp=0xc000074f88 pc=0x46bc0e
runtime.goparkunlock(...)
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:430
runtime.forcegchelper()
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:337 +0xb3 fp=0xc000074fe0 sp=0xc000074fa8 pc=0x439093
runtime.goexit({})
	/home/michael/sdk/go1.23.0/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc000074fe8 sp=0xc000074fe0 pc=0x472e61
created by runtime.init.7 in goroutine 1
	/home/michael/sdk/go1.23.0/src/runtime/proc.go:325 +0x1a
</code></pre></div>
<p>Phew! This output is pretty dense.</p>
<p>We can use the <a href="https://github.com/maruel/panicparse">https://github.com/maruel/panicparse</a> program to present this
stack trace in a more colorful and much shorter version:</p>















<a href="https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/2025-02-08-panicparse.jpg"><img
  srcset="https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/2025-02-08-panicparse_hu_c04249176648ab38.jpg 2x,https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/2025-02-08-panicparse_hu_8da191a897747329.jpg 3x"
  src="https://michael.stapelberg.ch/posts/2025-02-27-debug-hanging-go-programs/2025-02-08-panicparse_hu_e61da31f617a2260.jpg"
  
  width="600"
  height="394"
  style="

border: 1px solid #000;

"
  
  loading="lazy"></a>



<p>The functions helpfully highlighted in red are where the problem lies: My rsync
receiver implementation was incorrectly expecting the server to send a uid/gid
list, despite the PreserveUid and PreserveGid options not being enabled. <a href="https://github.com/gokrazy/rsync/commit/6c89d4dda3be055f19684c0ed56d623da458194e">Commit
<code>6c89d4d</code></a>
fixes the issue.</p>
<h2 id="attach-dlv">Tip 2: Attach the delve debugger to the process</h2>
<p>If dumping the stack trace in the moment is not sufficient to diagnose the
problem, you can go one step further and reach for an interactive debugger.</p>
<p>The most well-known Linux debugger is probably GDB, but when working with Go, I
recommend using <a href="https://github.com/go-delve/delve">the delve debugger</a> instead
as it typically works better. Install delve if you haven’t already:</p>
<pre tabindex="0"><code>% go install github.com/go-delve/delve/cmd/dlv@latest
</code></pre><p>In this article, I am using delve v1.24.0.</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>Note:</strong> If you want to explore local variables, you should rebuild your
program without optimizations and inlining (see the <a href="https://github.com/go-delve/delve/blob/master/Documentation/usage/dlv_exec.md"><code>dlv exec</code>
docs</a>):</p>
<pre tabindex="0"><code>% go install -gcflags=all=&#34;-N -l&#34; ./cmd/...
</code></pre></div>
  </div>
</aside>

<p>While you can run a new child process in a debugger (use <code>dlv exec</code>) without any
special permissions, attaching existing processes in a debugger is <a href="https://www.kernel.org/doc/Documentation/security/Yama.txt">disabled by
default in Linux</a>
for security reasons. We can allow this feature (remember to turn it off later!)
using:</p>
<pre tabindex="0"><code>% sudo sysctl -w kernel.yama.ptrace_scope=0
kernel.yama.ptrace_scope = 0
</code></pre><p>…and then we can just <code>dlv attach</code> to the hanging <code>gokr-rsync</code> process:</p>
<pre tabindex="0"><code>% dlv attach $(pidof gokr-rsync)
Type &#39;help&#39; for list of commands.
(dlv)
</code></pre><p>Great. But if we just print a stack trace, we only see functions from the
<code>runtime</code> package:</p>
<pre tabindex="0"><code>(dlv) bt
0  0x000000000047bb83 in runtime.futex
   at /home/michael/sdk/go1.23.6/src/runtime/sys_linux_amd64.s:558
1  0x00000000004374d0 in runtime.futexsleep
   at /home/michael/sdk/go1.23.6/src/runtime/os_linux.go:69
2  0x000000000040d89d in runtime.notesleep
   at /home/michael/sdk/go1.23.6/src/runtime/lock_futex.go:170
3  0x000000000044123e in runtime.mPark
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:1866
4  0x000000000044290d in runtime.stopm
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:2886
5  0x00000000004433d0 in runtime.findRunnable
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:3623
6  0x0000000000444e1d in runtime.schedule
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:3996
7  0x00000000004451cb in runtime.park_m
   at /home/michael/sdk/go1.23.6/src/runtime/proc.go:4103
8  0x0000000000477eee in runtime.mcall
   at /home/michael/sdk/go1.23.6/src/runtime/asm_amd64.s:459
</code></pre><p>The reason is that no goroutine is running (the program is waiting indefinitely
to receive data from the server), so we see one of the OS threads waiting in the
Go scheduler.</p>
<p>We first need to switch to the goroutine we are interested in (<code>grs</code> prints all
goroutines), and then the stack trace looks like what we expect:</p>
<pre tabindex="0"><code>(dlv) gr 1
Switched from 0 to 1 (thread 414327)
(dlv) bt
 0  0x0000000000474ebc in runtime.gopark
    at /home/michael/sdk/go1.23.6/src/runtime/proc.go:425
 1  0x000000000043819e in runtime.netpollblock
    at /home/michael/sdk/go1.23.6/src/runtime/netpoll.go:575
 2  0x000000000047435c in internal/poll.runtime_pollWait
    at /home/michael/sdk/go1.23.6/src/runtime/netpoll.go:351
 3  0x00000000004ed15a in internal/poll.(*pollDesc).wait
    at /home/michael/sdk/go1.23.6/src/internal/poll/fd_poll_runtime.go:84
 4  0x00000000004ed1f1 in internal/poll.(*pollDesc).waitRead
    at /home/michael/sdk/go1.23.6/src/internal/poll/fd_poll_runtime.go:89
 5  0x00000000004ee351 in internal/poll.(*FD).Read
    at /home/michael/sdk/go1.23.6/src/internal/poll/fd_unix.go:165
 6  0x0000000000569bb3 in net.(*netFD).Read
    at /home/michael/sdk/go1.23.6/src/net/fd_posix.go:55
 7  0x000000000057a025 in net.(*conn).Read
    at /home/michael/sdk/go1.23.6/src/net/net.go:189
 8  0x000000000058fcc5 in net.(*TCPConn).Read
    at &lt;autogenerated&gt;:1
 9  0x00000000004b72e8 in io.ReadAtLeast
    at /home/michael/sdk/go1.23.6/src/io/io.go:335
10  0x00000000004b74d3 in io.ReadFull
    at /home/michael/sdk/go1.23.6/src/io/io.go:354
11  0x0000000000598d5f in encoding/binary.Read
    at /home/michael/sdk/go1.23.6/src/encoding/binary/binary.go:244
12  0x00000000005a0b7a in github.com/gokrazy/rsync/internal/rsyncwire.(*MultiplexReader).ReadMsg
    at /home/michael/kr/rsync/internal/rsyncwire/wire.go:50
13  0x00000000005a0f17 in github.com/gokrazy/rsync/internal/rsyncwire.(*MultiplexReader).Read
    at /home/michael/kr/rsync/internal/rsyncwire/wire.go:72
14  0x0000000000528de8 in bufio.(*Reader).Read
    at /home/michael/sdk/go1.23.6/src/bufio/bufio.go:241
15  0x00000000004b72e8 in io.ReadAtLeast
    at /home/michael/sdk/go1.23.6/src/io/io.go:335
16  0x00000000004b74d3 in io.ReadFull
    at /home/michael/sdk/go1.23.6/src/io/io.go:354
17  0x00000000005a19ef in github.com/gokrazy/rsync/internal/rsyncwire.(*Conn).ReadInt32
    at /home/michael/kr/rsync/internal/rsyncwire/wire.go:163
18  0x00000000005b77d2 in github.com/gokrazy/rsync/internal/receiver.(*Transfer).recvIdMapping1
    at /home/michael/kr/rsync/internal/receiver/uidlist.go:16
19  0x00000000005b7ea8 in github.com/gokrazy/rsync/internal/receiver.(*Transfer).RecvIdList
    at /home/michael/kr/rsync/internal/receiver/uidlist.go:52
20  0x00000000005b18db in github.com/gokrazy/rsync/internal/receiver.(*Transfer).ReceiveFileList
    at /home/michael/kr/rsync/internal/receiver/flist.go:229
21  0x0000000000605390 in github.com/gokrazy/rsync/internal/receivermaincmd.clientRun
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:341
22  0x00000000005fe572 in github.com/gokrazy/rsync/internal/receivermaincmd.socketClient
    at /home/michael/kr/rsync/internal/receivermaincmd/clientserver.go:44
23  0x0000000000602f10 in github.com/gokrazy/rsync/internal/receivermaincmd.rsyncMain
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:160
24  0x0000000000605e7e in github.com/gokrazy/rsync/internal/receivermaincmd.Main
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:394
25  0x0000000000606653 in main.main
    at /home/michael/kr/rsync/cmd/gokr-rsync/rsync.go:12
26  0x000000000043fa47 in runtime.main
    at /home/michael/sdk/go1.23.6/src/runtime/proc.go:272
27  0x000000000047bd01 in runtime.goexit
    at /home/michael/sdk/go1.23.6/src/runtime/asm_amd64.s:1700
</code></pre><h2 id="save-core-dump">Tip 3: Save a core dump for later</h2>
<p>If you don’t have time to poke around in the debugger now, you can save a core
dump for later.</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"><strong>Tip:</strong> Check out my <a href="/posts/2024-10-22-debug-go-core-dumps-delve-export-bytes/">debugging Go core dumps with
delve</a> blog post from
2024 for more details! This section just explains how to collect core dumps.</div>
  </div>
</aside>

<p>In addition to printing the stack trace on <code>SIGQUIT</code>, we can make the Go runtime
crash the program, which in turn makes the Linux kernel write a core dump, by
running our program with the environment variable
<a href="https://pkg.go.dev/runtime"><code>GOTRACEBACK=crash</code></a>.</p>
<p>Modern Linux systems typically include <a href="https://manpages.debian.org/systemd-coredump.8"><code>systemd-coredump(8)</code></a>
 (but you might need to explicitly install it, for example on
Ubuntu) to collect core dumps (and remove old ones). You can use <a href="https://manpages.debian.org/coredumpctl.1"><code>coredumpctl(1)</code></a>
 to list and work with them. On macOS,
<a href="https://developer.apple.com/forums/thread/694233#695943022">collecting cores is more
involved</a>. I don’t
know about Windows.</p>
<p>In case your Linux system does not use <code>systemd-coredump</code>, you can use <code>ulimit -c unlimited</code> and set the kernel’s <code>kernel.core_pattern</code> sysctl setting. You can
find more details and options in the <a href="https://go.dev/wiki/CoreDumpDebugging">CoreDumpDebugging page of the Go
wiki</a>. For this article, we will stick to
<code>coredumpctl</code>:</p>
<pre tabindex="0"><code>% GOTRACEBACK=crash gokr-rsync -rtO --delete rsync://rsync.paas.rpki.ripe.net/repo/ /tmp/rpki-repo
[…]
^\SIGQUIT: quit
[…]
zsh: IOT instruction (core dumped)  GOTRACEBACK=crash gokr-rsync -rtO […]
</code></pre><p>The last line is what we want to see: it should say “core dumped”.</p>
<p>This core should now show up in <a href="https://manpages.debian.org/coredumpctl.1"><code>coredumpctl(1)</code></a>
:</p>
<pre tabindex="0"><code>% coredumpctl info
           PID: 414607 (gokr-rsync)
           UID: 1000 (michael)
           GID: 1000 (michael)
        Signal: 6 (ABRT)
     Timestamp: Sat 2025-02-08 10:18:27 CET (12s ago)
  Command Line: gokr-rsync -rtO --delete rsync://rsync.paas.rpki.ripe.net/repo/ /tmp/rpki-repo
    Executable: /bin/gokr-rsync
 Control Group: /user.slice/user-1000.slice/session-1.scope
          Unit: session-1.scope
         Slice: user-1000.slice
       Session: 1
     Owner UID: 1000 (michael)
       Boot ID: 6158dd3b52af4b8384c103a8a336fc02
    Machine ID: ecb5a44f1a5846ad871566e113bf8937
      Hostname: midna
       Storage: /var/lib/systemd/coredump/core.gokr-rsync.1000.6158dd3b52af4b8384c103a8a336fc02.414607.1739006307000000.zst (present)
  Size on Disk: 158.3K
       Message: Process 414607 (gokr-rsync) of user 1000 dumped core.
                
    Module [dso] without build-id.
    Module [dso]
    Stack trace of thread 1604447:
    #0  0x0000000000475a41 runtime.raise.abi0 (/bin/gokr-rsync + 0x75a41)
    #1  0x0000000000451d85 runtime.dieFromSignal (/bin/gokr-rsync + 0x51d85)
    #2  0x00000000004522e6 runtime.sigfwdgo (/bin/gokr-rsync + 0x522e6)
    #3  0x0000000000450c45 runtime.sigtrampgo (/bin/gokr-rsync + 0x50c45)
    #4  0x0000000000475d26 runtime.sigtramp.abi0 (/bin/gokr-rsync + 0x75d26)
    #5  0x0000000000475e20 n/a (/bin/gokr-rsync + 0x75e20)
    ELF object binary architecture: AMD x86-64
</code></pre><p>If you see only hexadecimal addresses followed by <code>n/a (n/a + 0x0)</code>, that means
<code>systemd-coredump</code> could not symbolize (= resolve addresses to function names)
your core dump. Here are a few possible reasons for missing symbolization:</p>
<ul>
<li>Linux 6.12 and 6.13 <a href="https://sourceware.org/bugzilla/show_bug.cgi?id=32713">produced core dumps that elfutils cannot
symbolize</a>. <code>systemd-coredump</code>
uses elfutils for symbolization, so avoid 6.12/6.13 in favor of using 6.14 or
newer.</li>
<li>With systemd v234-v256, <code>systemd-coredump</code> did not have permission to look
into programs living in the <code>/home</code> directory (fixed with <a href="https://github.com/systemd/systemd/commit/4ac1755be2d6c141fae7e57c42936e507c5b54e3">commit
<code>4ac1755</code></a>
in systemd v257+).
<ul>
<li>Similarly, <code>systemd-coredump</code> runs with
<a href="http://manpages.debian.org/systemd.exec"><code>PrivateTmp=yes</code></a>, meaning it
won’t be able to access programs you place in <code>/tmp</code>.</li>
</ul>
</li>
<li>Go builds with debug symbols by default, but maybe you are explicitly
stripping debug symbols in your build, by building with <code>-ldflags=-w</code>?</li>
</ul>
<p>We can now use <a href="https://manpages.debian.org/coredumpctl.1"><code>coredumpctl(1)</code></a>
 to launch delve for
this program + core dump:</p>
<pre tabindex="0"><code>% coredumpctl debug --debugger=dlv --debugger-arguments=core
[…]
Type &#39;help&#39; for list of commands.
(dlv) gr 1
Switched from 0 to 1 (thread 414607)
(dlv) bt
[…]
16  0x00000000004b74d3 in io.ReadFull
    at /home/michael/sdk/go1.23.6/src/io/io.go:354
17  0x00000000005a19ef in github.com/gokrazy/rsync/internal/rsyncwire.(*Conn).ReadInt32
    at /home/michael/kr/rsync/internal/rsyncwire/wire.go:163
18  0x00000000005b77d2 in github.com/gokrazy/rsync/internal/receiver.(*Transfer).recvIdMapping1
    at /home/michael/kr/rsync/internal/receiver/uidlist.go:16
19  0x00000000005b7ea8 in github.com/gokrazy/rsync/internal/receiver.(*Transfer).RecvIdList
    at /home/michael/kr/rsync/internal/receiver/uidlist.go:52
20  0x00000000005b18db in github.com/gokrazy/rsync/internal/receiver.(*Transfer).ReceiveFileList
    at /home/michael/kr/rsync/internal/receiver/flist.go:229
21  0x0000000000605390 in github.com/gokrazy/rsync/internal/receivermaincmd.clientRun
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:341
22  0x00000000005fe572 in github.com/gokrazy/rsync/internal/receivermaincmd.socketClient
    at /home/michael/kr/rsync/internal/receivermaincmd/clientserver.go:44
23  0x0000000000602f10 in github.com/gokrazy/rsync/internal/receivermaincmd.rsyncMain
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:160
24  0x0000000000605e7e in github.com/gokrazy/rsync/internal/receivermaincmd.Main
    at /home/michael/kr/rsync/internal/receivermaincmd/receivermaincmd.go:394
25  0x0000000000606653 in main.main
    at /home/michael/kr/rsync/cmd/gokr-rsync/rsync.go:12
26  0x000000000043fa47 in runtime.main
    at /home/michael/sdk/go1.23.6/src/runtime/proc.go:272
27  0x000000000047bd01 in runtime.goexit
    at /home/michael/sdk/go1.23.6/src/runtime/asm_amd64.s:1700
</code></pre><h2 id="conclusion">Conclusion</h2>
<p>In my experience, in the medium to long term, it always pays off to set up your
environment such that you can debug your programs conveniently. I strongly
encourage every programmer (and even users!) to invest time into your
development and debugging setup.</p>
<p>Luckily, Go comes with stack printing functionality by default (just press
<code>Ctrl+\</code>) and we can easily get a core dump out of our Go programs by running
them with <code>GOTRACEBACK=crash</code> — provided the system is set up to collect core
dumps.</p>
<p>Together with the delve debugger, this gives us all we need to effectively and
efficiently diagnose problems in Go programs.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Go Protobuf: The new Opaque API]]></title>
    <link href="https://michael.stapelberg.ch/posts/2024-12-21-go-protobuf-opaque/"/>
    <id>https://michael.stapelberg.ch/posts/2024-12-21-go-protobuf-opaque/</id>
    <published>2024-12-21T11:06:00+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">I originally published this post in <a href="https://go.dev/blog">the Go blog</a>, but am
publishing this copy of it in my own blog as well for readers who don’t follow
the Go blog.</div>
  </div>
</aside>

<p>[<a href="https://en.wikipedia.org/wiki/Protocol_Buffers">Protocol Buffers (Protobuf)</a>
is Google&rsquo;s language-neutral data interchange format. See
<a href="https://protobuf.dev/">protobuf.dev</a>.]</p>
<p>Back in March 2020, we released <a href="https://go.dev/blog/protobuf-apiv2">a major overhaul of the Go Protobuf
API</a>. The <code>google.golang.org/protobuf</code>
package introduced first-class <a href="https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect">support for
reflection</a>,
a <a href="https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb"><code>dynamicpb</code></a>
implementation and the
<a href="https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp"><code>protocmp</code></a>
package for easier testing.</p>
<p>That release introduced a new protobuf module with a new API. Today, we are
releasing an additional API for generated code, meaning the Go code in the
<code>.pb.go</code> files created by the protocol compiler (<code>protoc</code>). This blog post
explains our motivation for creating a new API and shows you how to use it in
your projects.</p>
<p>To be clear: We are not removing anything. We will continue to support the
existing API for generated code, just like we still support the older protobuf
module (by wrapping the <code>google.golang.org/protobuf</code> implementation). Go is
<a href="https://go.dev/blog/compat">committed to backwards compatibility</a> and this
applies to Go Protobuf, too!</p>
<h2 id="background">Background: the (existing) Open Struct API</h2>
<p>We now call the existing API the Open Struct API, because generated struct types
are open to direct access. In the next section, we will see how it differs from
the new Opaque API.</p>
<p>To work with protocol buffers, you first create a <code>.proto</code> definition file like
this one:</p>
<pre><code>edition = &quot;2023&quot;;  // successor to proto2 and proto3

package log;

message LogEntry {
  string backend_server = 1;
  uint32 request_size = 2;
  string ip_address = 3;
}
</code></pre>
<p>Then, you <a href="https://protobuf.dev/getting-started/gotutorial/">run the protocol compiler
(<code>protoc</code>)</a> to generate code
like the following (in a <code>.pb.go</code> file):</p>
<pre><code>package logpb

type LogEntry struct {
  BackendServer *string
  RequestSize   *uint32
  IPAddress     *string
  // …internal fields elided…
}

func (l *LogEntry) GetBackendServer() string { … }
func (l *LogEntry) GetRequestSize() uint32   { … }
func (l *LogEntry) GetIPAddress() string     { … }
</code></pre>
<p>Now you can import the generated <code>logpb</code> package from your Go code and call
functions like
<a href="https://pkg.go.dev/google.golang.org/protobuf/proto#Marshal"><code>proto.Marshal</code></a>
to encode <code>logpb.LogEntry</code> messages into protobuf wire format.</p>
<p>You can find more details in the <a href="https://protobuf.dev/reference/go/go-generated/">Generated Code API
documentation</a>.</p>
<h3 id="presence">(Existing) Open Struct API: Field Presence</h3>
<p>An important aspect of this generated code is how <em>field presence</em> (whether a
field is set or not) is modeled. For instance, the above example models presence
using pointers, so you could set the <code>BackendServer</code> field to:</p>
<ol>
<li><code>proto.String(&quot;zrh01.prod&quot;)</code>: the field is set and contains &ldquo;zrh01.prod&rdquo;</li>
<li><code>proto.String(&quot;&quot;)</code>: the field is set (non-<code>nil</code> pointer) but contains an
empty value</li>
<li><code>nil</code> pointer: the field is not set</li>
</ol>
<p>If you are used to generated code not having pointers, you are probably using
<code>.proto</code> files that start with <code>syntax = &quot;proto3&quot;</code>. The field presence behavior
changed over the years:</p>
<ul>
<li><code>syntax = &quot;proto2&quot;</code> uses <em>explicit presence</em> by default</li>
<li><code>syntax = &quot;proto3&quot;</code> used <em>implicit presence</em> by default (where cases 2 and 3
cannot be distinguished and are both represented by an empty string), but was
later extended to allow <a href="https://protobuf.dev/programming-guides/proto3/#field-labels">opting into explicit presence with the <code>optional</code>
keyword</a></li>
<li><code>edition = &quot;2023&quot;</code>, the <a href="https://protobuf.dev/editions/overview/">successor to both proto2 and
proto3</a>, uses <a href="https://protobuf.dev/programming-guides/field_presence/"><em>explicit
presence</em></a> by default</li>
</ul>
<h2 id="opaqueapi">The new Opaque API</h2>
<p>We created the new <em>Opaque API</em> to uncouple the <a href="https://protobuf.dev/reference/go/go-generated/">Generated Code
API</a> from the underlying
in-memory representation. The (existing) Open Struct API has no such separation:
it allows programs direct access to the protobuf message memory. For example,
one could use the <code>flag</code> package to parse command-line flag values into protobuf
message fields:</p>
<pre><code>var req logpb.LogEntry
flag.StringVar(&amp;req.BackendServer, &quot;backend&quot;, os.Getenv(&quot;HOST&quot;), &quot;…&quot;)
flag.Parse() // fills the BackendServer field from -backend flag
</code></pre>
<p>The problem with such a tight coupling is that we can never change how we lay
out protobuf messages in memory. Lifting this restriction enables many
implementation improvements, which we&rsquo;ll see below.</p>
<p>What changes with the new Opaque API? Here is how the generated code from the
above example would change:</p>
<pre><code>package logpb

type LogEntry struct {
  xxx_hidden_BackendServer *string // no longer exported
  xxx_hidden_RequestSize   uint32  // no longer exported
  xxx_hidden_IPAddress     *string // no longer exported
  // …internal fields elided…
}

func (l *LogEntry) GetBackendServer() string { … }
func (l *LogEntry) HasBackendServer() bool   { … }
func (l *LogEntry) SetBackendServer(string)  { … }
func (l *LogEntry) ClearBackendServer()      { … }
// …
</code></pre>
<p>With the Opaque API, the struct fields are hidden and can no longer be
directly accessed. Instead, the new accessor methods allow for getting, setting,
or clearing a field.</p>
<h3 id="lessmemory">Opaque structs use less memory</h3>
<p>One change we made to the memory layout is to model field presence for
elementary fields more efficiently:</p>
<ul>
<li>The (existing) Open Struct API uses pointers, which adds a 64-bit word to the
space cost of the field.</li>
<li>The Opaque API uses <a href="https://en.wikipedia.org/wiki/Bit_field">bit
fields</a>, which require one bit per
field (ignoring padding overhead).</li>
</ul>
<p>Using fewer variables and pointers also lowers load on the allocator and on the
garbage collector.</p>
<p>The performance improvement depends heavily on the shapes of your protocol
messages: The change only affects elementary fields like integers, bools, enums,
and floats, but not strings, repeated fields, or submessages (because it is
<a href="https://protobuf.dev/reference/go/opaque-faq/#memorylayout">less
profitable</a>
for those types).</p>
<p>Our benchmark results show that messages with few elementary fields exhibit
performance that is as good as before, whereas messages with more elementary
fields are decoded with significantly fewer allocations:</p>
<pre><code>             │ Open Struct API │             Opaque API             │
             │    allocs/op    │  allocs/op   vs base               │
Prod#1          360.3k ± 0%       360.3k ± 0%  +0.00% (p=0.002 n=6)
Search#1       1413.7k ± 0%       762.3k ± 0%  -46.08% (p=0.002 n=6)
Search#2        314.8k ± 0%       132.4k ± 0%  -57.95% (p=0.002 n=6)
</code></pre>
<p>Reducing allocations also makes decoding protobuf messages more efficient:</p>
<pre><code>             │ Open Struct API │             Opaque API            │
             │   user-sec/op   │ user-sec/op  vs base              │
Prod#1         55.55m ± 6%        55.28m ± 4%  ~ (p=0.180 n=6)
Search#1       324.3m ± 22%       292.0m ± 6%  -9.97% (p=0.015 n=6)
Search#2       67.53m ± 10%       45.04m ± 8%  -33.29% (p=0.002 n=6)
</code></pre>
<p>(All measurements done on an AMD Castle Peak Zen 2. Results on ARM and Intel
CPUs are similar.)</p>
<p>Note: proto3 with implicit presence similarly does not use pointers, so you will
not see a performance improvement if you are coming from proto3. If you were
using implicit presence for performance reasons, forgoing the convenience of
being able to distinguish empty fields from unset ones, then the Opaque API now
makes it possible to use explicit presence without a performance penalty.</p>
<h3 id="lazydecoding">Motivation: Lazy Decoding</h3>
<p>Lazy decoding is a performance optimization where the contents of a submessage
are decoded when first accessed instead of during
<a href="https://pkg.go.dev/google.golang.org/protobuf/proto#Unmarshal"><code>proto.Unmarshal</code></a>. Lazy
decoding can improve performance by avoiding unnecessarily decoding fields which
are never accessed.</p>
<p>Lazy decoding can&rsquo;t be supported safely by the (existing) Open Struct API. While
the Open Struct API provides getters, leaving the (un-decoded) struct fields
exposed would be extremely error-prone. To ensure that the decoding logic runs
immediately before the field is first accessed, we must make the field private
and mediate all accesses to it through getter and setter functions.</p>
<p>This approach made it possible to implement lazy decoding with the Opaque
API. Of course, not every workload will benefit from this optimization, but for
those that do benefit, the results can be spectacular: We have seen logs
analysis pipelines that discard messages based on a top-level message condition
(e.g. whether <code>backend_server</code> is one of the machines running a new Linux kernel
version) and can skip decoding deeply nested subtrees of messages.</p>
<p>As an example, here are the results of the micro-benchmark we included,
demonstrating how lazy decoding saves over 50% of the work and over 87% of
allocations!</p>
<pre><code>                  │   nolazy    │                lazy                │
                  │   sec/op    │   sec/op     vs base               │
Unmarshal/lazy-24   6.742µ ± 0%   2.816µ ± 0%  -58.23% (p=0.002 n=6)

                  │    nolazy    │                lazy                 │
                  │     B/op     │     B/op      vs base               │
Unmarshal/lazy-24   3.666Ki ± 0%   1.814Ki ± 0%  -50.51% (p=0.002 n=6)

                  │   nolazy    │               lazy                │
                  │  allocs/op  │ allocs/op   vs base               │
Unmarshal/lazy-24   64.000 ± 0%   8.000 ± 0%  -87.50% (p=0.002 n=6)
</code></pre>
<h3 id="pointercomparison">Motivation: reduce pointer comparison mistakes</h3>
<p>Modeling field presence with pointers invites pointer-related bugs.</p>
<p>Consider an enum, declared within the <code>LogEntry</code> message:</p>
<pre><code>message LogEntry {
  enum DeviceType {
    DESKTOP = 0;
    MOBILE = 1;
    VR = 2;
  };
  DeviceType device_type = 1;
}
</code></pre>
<p>A simple mistake is to compare the <code>device_type</code> enum field like so:</p>
<pre><code>if cv.DeviceType == logpb.LogEntry_DESKTOP.Enum() { // incorrect!
</code></pre>
<p>Did you spot the bug? The condition compares the memory address instead of the
value. Because the <code>Enum()</code> accessor allocates a new variable on each call, the
condition can never be true. The check should have read:</p>
<pre><code>if cv.GetDeviceType() == logpb.LogEntry_DESKTOP {
</code></pre>
<p>The new Opaque API prevents this mistake: Because fields are hidden, all access
must go through the getter.</p>
<h3 id="accidentalsharing">Motivation: reduce accidental sharing mistakes</h3>
<p>Let&rsquo;s consider a slightly more involved pointer-related bug. Assume you are
trying to stabilize an RPC service that fails under high load. The following
part of the request middleware looks correct, but still the entire service goes
down whenever just one customer sends a high volume of requests:</p>
<pre><code>logEntry.IPAddress = req.IPAddress
logEntry.BackendServer = proto.String(hostname)
// The redactIP() function redacts IPAddress to 127.0.0.1,
// unexpectedly not just in logEntry *but also* in req!
go auditlog(redactIP(logEntry))
if quotaExceeded(req) {
	// BUG: All requests end up here, regardless of their source.
	return fmt.Errorf(&quot;server overloaded&quot;)
}
</code></pre>
<p>Did you spot the bug? The first line accidentally copied the pointer (thereby
sharing the pointed-to variable between the <code>logEntry</code> and <code>req</code> messages)
instead of its value. It should have read:</p>
<pre><code>logEntry.IPAddress = proto.String(req.GetIPAddress())
</code></pre>
<p>The new Opaque API prevents this problem as the setter takes a value
(<code>string</code>) instead of a pointer:</p>
<pre><code>logEntry.SetIPAddress(req.GetIPAddress())
</code></pre>
<h3 id="reflection">Motivation: Fix Sharp Edges: reflection</h3>
<p>To write code that works not only with a specific message type
(e.g. <code>logpb.LogEntry</code>), but with any message type, one needs some kind of
reflection. The previous example used a function to redact IP addresses. To work
with any type of message, it could have been defined as <code>func redactIP(proto.Message) proto.Message { … }</code>.</p>
<p>Many years ago, your only option to implement a function like <code>redactIP</code> was to
reach for <a href="https://go.dev/blog/laws-of-reflection">Go&rsquo;s <code>reflect</code> package</a>,
which resulted in very tight coupling: you had only the generator output and had
to reverse-engineer what the input protobuf message definition might have looked
like. The <a href="https://go.dev/blog/protobuf-apiv2"><code>google.golang.org/protobuf</code> module
release</a> (from March 2020) introduced
<a href="https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect">Protobuf
reflection</a>,
which should always be preferred: Go&rsquo;s <code>reflect</code> package traverses the data
structure&rsquo;s representation, which should be an implementation detail. Protobuf
reflection traverses the logical tree of protocol messages without regard to its
representation.</p>
<p>Unfortunately, merely <em>providing</em> protobuf reflection is not sufficient and
still leaves some sharp edges exposed: In some cases, users might accidentally
use Go reflection instead of protobuf reflection.</p>
<p>For example, encoding a protobuf message with the <code>encoding/json</code> package (which
uses Go reflection) was technically possible, but the result is not <a href="https://protobuf.dev/programming-guides/proto3/#json">canonical
Protobuf JSON
encoding</a>. Use the
<a href="https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson"><code>protojson</code></a>
package instead.</p>
<p>The new Opaque API prevents this problem because the message struct fields are
hidden: accidental usage of Go reflection will see an empty message. This is
clear enough to steer developers towards protobuf reflection.</p>
<h3 id="idealmemory">Motivation: Making the ideal memory layout possible</h3>
<p>The benchmark results from the <a href="#lessmemory">More Efficient Memory
Representation</a> section have already shown that protobuf
performance heavily depends on the specific usage: How are the messages defined?
Which fields are set?</p>
<p>To keep Go Protobuf as fast as possible for <em>everyone</em>, we cannot implement
optimizations that help only one program, but hurt the performance of other
programs.</p>
<p>The Go compiler used to be in a similar situation, up until <a href="https://go.dev/blog/go1.20">Go 1.20 introduced
Profile-Guided Optimization (PGO)</a>. By recording the
production behavior (through <a href="https://go.dev/blog/pprof">profiling</a>) and feeding
that profile back to the compiler, we allow the compiler to make better
trade-offs <em>for a specific program or workload</em>.</p>
<p>We think using profiles to optimize for specific workloads is a promising
approach for further Go Protobuf optimizations. The Opaque API makes those
possible: Program code uses accessors and does not need to be updated when the
memory representation changes, so we could, for example, move rarely set fields
into an overflow struct.</p>
<h2 id="migration">Migration</h2>
<p>You can migrate on your own schedule, or even not at all—the (existing) Open
Struct API will not be removed. But, if you’re not on the new Opaque API, you
won’t benefit from its improved performance, or future optimizations that target
it.</p>
<p>We recommend you select the Opaque API for new development. Protobuf Edition
2024 (see <a href="https://protobuf.dev/editions/overview/">Protobuf Editions Overview</a>
if you are not yet familiar) will make the Opaque API the default.</p>
<h3 id="hybridapi">The Hybrid API</h3>
<p>Aside from the Open Struct API and Opaque API, there is also the Hybrid API,
which keeps existing code working by keeping struct fields exported, but also
enabling migration to the Opaque API by adding the new accessor methods.</p>
<p>With the Hybrid API, the protobuf compiler will generate code on two API levels:
the <code>.pb.go</code> is on the Hybrid API, whereas the <code>_protoopaque.pb.go</code> version is
on the Opaque API and can be selected by building with the <code>protoopaque</code> build
tag.</p>
<h3 id="rewriting">Rewriting Code to the Opaque API</h3>
<p>See the <a href="https://protobuf.dev/reference/go/opaque-migration/">migration
guide</a>
for detailed instructions. The high-level steps are:</p>
<ol>
<li>Enable the Hybrid API.</li>
<li>Update existing code using the <code>open2opaque</code> migration tool.</li>
<li>Switch to the Opaque API.</li>
</ol>
<h3 id="publishing">Advice for published generated code: Use Hybrid API</h3>
<p>Small usages of protobuf can live entirely within the same repository, but
usually, <code>.proto</code> files are shared between different projects that are owned by
different teams. An obvious example is when different companies are involved: To
call Google APIs (with protobuf), use the <a href="https://github.com/googleapis/google-cloud-go">Google Cloud Client Libraries for
Go</a> from your project. Switching
the Cloud Client Libraries to the Opaque API is not an option, as that would be
a breaking API change, but switching to the Hybrid API is safe.</p>
<p>Our advice for such packages that publish generated code (<code>.pb.go</code> files) is to
switch to the Hybrid API please! Publish both the <code>.pb.go</code> and the
<code>_protoopaque.pb.go</code> files, please. The <code>protoopaque</code> version allows your
consumers to migrate on their own schedule.</p>
<h3 id="enablelazy">Enabling Lazy Decoding</h3>
<p>Lazy decoding is available (but not enabled) once you migrate to the Opaque API!
🎉</p>
<p>To enable: in your <code>.proto</code> file, annotate your message-typed fields with the
<code>[lazy = true]</code> annotation.</p>
<p>To opt out of lazy decoding (despite <code>.proto</code> annotations), the <a href="https://pkg.go.dev/google.golang.org/protobuf/runtime/protolazy"><code>protolazy</code>
package
documentation</a>
describes the available opt-outs, which affect either an individual Unmarshal
operation or the entire program.</p>
<h2 id="nextsteps">Next Steps</h2>
<p>By using the open2opaque tool in an automated fashion over the last few years,
we have converted the vast majority of Google’s <code>.proto</code> files and Go code to
the Opaque API. We continuously improved the Opaque API implementation as we
moved more and more production workloads to it.</p>
<p>Therefore, we expect you should not encounter problems when trying the Opaque
API. In case you do encounter any issues after all, please <a href="https://github.com/golang/protobuf/issues/">let us know on the
Go Protobuf issue tracker</a>.</p>
<p>Reference documentation for Go Protobuf can be found on <a href="https://protobuf.dev/reference/go/">protobuf.dev → Go
Reference</a>.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Get a solar panel for your balcony now ☀️]]></title>
    <link href="https://michael.stapelberg.ch/posts/2024-12-10-solar-panel-for-your-balcony/"/>
    <id>https://michael.stapelberg.ch/posts/2024-12-10-solar-panel-for-your-balcony/</id>
    <published>2024-12-10T17:13:00+01:00</published>
    <content type="html"><![CDATA[<p>A year ago, I got a solar panel for my balcony — an easy way to vote with your
wallet to convert more of the world’s energy usage to solar power. That was a
great decision and I would recommend everyone get a solar panel (or two)!</p>
<h2 id="its-called-plug-in-solar-panel-because-you-can-just-plug-it-in">It’s called plug-in solar panel because you can just plug it in</h2>
<p>In my experience, many people are surprised about the basics of how power works:
You do not need to connect devices to a battery in order to enjoy solar
power. You can just plug in the solar panel into your household electricity
setup. Any of your consumers (like a TV, or electric cooktop) will now use the
power that your solar panel produces before consuming power from the grid.</p>
<p>Here’s the panel I have (Weber barbecue for scale). As you can see, the panel is
not yet mounted at an angle, just hung over the balcony. The black box at the
back of the panel is the inverter (“Wechselrichter”). You connect the panel on
one side and get electricity out the other side.</p>
<p><img src="IMG_2882.jpg" alt=""></p>
<h2 id="which-solar-panel-to-buy">Which solar panel to buy?</h2>
<p>There are two big questions to answer when chosing a solar panel: what peak
capacity should your panel(s) have and which company / seller do you buy from?</p>
<p>Regarding panel capacity: When I look at my energy usage, I see about 100 watts
of baseline load. This includes always-on servers and other home automation
devices. During working hours, running a PC and (power-hungry) monitor adds
another 100 watts or so. Around noon, there is quite a spike in usage when
cooking with my induction cooktop.</p>
<p>Hence, I figured a plug &amp; play solar panel with the smallest size of 385 Wp
would be well equipped to cover baseline usage, compared to the next bigger unit
with 780 Wp, which seems oversized for my usage. Note that a peak capacity of
385 Wp will not necessarily mean that you will measure 380W of output. I did
repeatedly measure energy production exceeding 300W.</p>
<p>Regarding the company, the best offer I found in Switzerland was a small company
called <a href="https://www.erneuer.bar/">erneuer.bar</a>, which means “renewable” in
German. They ship the panels with barely any packaging in fully electric
vehicles and their offer is eligible for <a href="https://www.topten.ch/private/page/ewz">the topten bonus program from
EWZ</a>, meaning you’ll get back 200 CHF if
you fill in a form.</p>
<p>The specific model I ordered was called “385 Wp Plug &amp; Play Solar (DE)”. Here’s
the bill:</p>
<table>
  <thead>
      <tr>
          <th>Produkt</th>
          <th>Preis</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>385 Wp Plug &amp; Play Solar (DE)</td>
          <td>CHF 520.00</td>
      </tr>
      <tr>
          <td>Mounting kit: balcony, 1 panel</td>
          <td>CHF 75.00</td>
      </tr>
      <tr>
          <td>Pre-mount: mounting kit balcony</td>
          <td>CHF 60.00</td>
      </tr>
      <tr>
          <td>WiFi measurement myStrom</td>
          <td>CHF 55.00</td>
      </tr>
      <tr>
          <td>Shipping</td>
          <td>CHF 68.00</td>
      </tr>
      <tr>
          <td>Total</td>
          <td>CHF 778.00</td>
      </tr>
  </tbody>
</table>
<p>Of course, you can save some money in various ways. For example, the measurement
device and pre-mount option are both not required, but convenient. Similarly,
you can probably find solar panels for cheaper, but the offer that erneuer.bar
has put together truly is very convenient and works well, and to me that’s worth
some money.</p>
<p>One mistake I made when ordering is selecting a 5m cable. It turned out I needed
a 10m cable, so I recommend you measure better than I did (or just select the
longer cable). On the plus side, customer service was excellent: I quickly
received an email response and could just send back my cable in exchange for a
new one.</p>
<h2 id="amortization-who-cares">Amortization? Who cares!</h2>
<p>Many people seem to consider <strong>only the financial aspect</strong> of buying a solar
panel and calculate when the solar panel will have paid for itself. I don’t
care. My goal is to convert more energy usage to green energy, not to save
money.</p>
<p>Similarly, some people install batteries so that they can use “their” energy for
themselves, in case the solar panel produces more than they use at that
moment. I couldn’t care less who uses the energy I produce — as long as it’s
green energy, anyone is welcome to consume it.</p>
<p>(Of course I understand these questions become more important the larger a solar
installation gets. But we’re talking about one balcony and one solar panel (or
two) covering someone’s baseline residential household electricity load. Don’t
overthink it!)</p>
<h2 id="requirement-balcony-power-socket">Requirement: balcony power socket</h2>
<p>Aside from having a balcony, there is only one hard requirement: you need a
power socket.</p>
<p>This requirement is either trivially fulfilled if you already have an outdoor
power socket on your balcony (lucky you!), or might turn out to be the most
involved part of the project. Either way, because an electrician needs to
install power sockets, all you can do is get permission from your landlord and
make an appointment with your electrician of choice.</p>
<p>In terms of cost, you will probably spend a few hundred bucks, depending on your
area’s cost of living. A good idea that did not occur to me back then: Ask
around in your house if any neighbors would be interested in getting a balcony
power socket, too, and do it all in one go (for cheaper).</p>
<h2 id="bureaucracy">Bureaucracy</h2>
<p>One can easily find stories online about electricity providers and landlords not
permitting the installation of solar panels for… rather questionable
reasons. For example, some claimed that solar panels could overload the house
electricity infrastructure! A drastic-sounding claim, but nonsense in
practice. Luckily, law makers are recognizing this and are removing barriers.</p>
<h3 id="electricity-provider-and-the-law">Electricity provider and the law</h3>
<p>In Switzerland 🇨🇭, you can connect panels producing up to 600W without an
electrician, but you need to notify your electricity provider.</p>
<p>In Germany 🇩🇪, you can connect panels producing up to 800W (as of May 16th 2024)
without an electrician, but <a href="https://www.adac.de/rund-ums-haus/energie/versorgung/balkonkraftwerk-anmelden/">you need to register with the
Bundesnetzagentur</a>.</p>
<p>Be sure to check your country’s laws and your electricity provider’s rules and
processes.</p>
<h3 id="landlord-and-neighbors">Landlord and neighbors</h3>
<p>In Switzerland 🇨🇭, you need to <a href="https://www.mieterverband.ch/mv/politik-positionen/news/2024/mustervereinbarung-fuer-bewilligung-von-balkonsolaranlagen.html">ask your landlord for
permission</a>
because if your solar panel were to fall down from the balcony, the landlord
would be liable. Usually, the landlord insists on proper mounting and the tenant
taking over liability. In my case, the landlord also asked me to ensure the
neighbors wouldn’t mind. I put up a letter, nobody complained, the landlord
accepted.</p>
<p>In Germany 🇩🇪, you do need to ask your landlord for permission, but the landlord
pretty much has to agree (<a href="https://www.computerbild.de/artikel/cb-News-Energie-Bundeskabinett-Balkonkraftwerk-Wohnung-Privileg-36684829.html">as of October 17th
2024</a>). The
question is not “if”, but “how” the landlord wants you to install the solar
panel.</p>
<h2 id="optimizing-the-installation-angle">Optimizing the installation angle</h2>
<p>Earlier I wrote that you can just hang the solar panel onto your balcony and
plug it in. While this is true, there is one factor that is worth optimizing (as
time permits): the installation angle.</p>
<p>If you want more details about the physics background and various considerations
that go into chasing the optimal angle, check out these (German) articles about
<a href="https://www.golem.de/news/balkonkraftwerke-was-bringt-der-neigungswinkel-fuer-den-stromertrag-2405-185599-2.html">optimizing the installation angle (at
Golem)</a>
or <a href="https://www.heise.de/ratgeber/Photovoltaik-Anlagen-dimensionieren-Wirtschaftlichkeit-berechnen-9688108.html?seite=2">sizing solar installations (at
Heise)</a>. I’ll
summarize: the angle is important and can result in twice as much energy
production! Any angle is usually better than no angle.</p>
<p>In my case, I first “installed” the solar panel (no angle) at 2023-09-30.</p>
<p>Then, about a month later, I installed it at an angle at 2023-10-28.</p>
<p><img src="IMG_2996.jpg" alt=""></p>
<p>I unfortunately don’t have a great before/after graph because after I installed
the proper angle mount, there were almost no sunny days.</p>
<p>Instead, I will show you data from a comparable time range (early October) in
2023 (before mounting the panel at an angle) and in 2024 (with a properly
mounted panel). As you can see, the difference is not that huge, but clearly
visible: without an angle mount, I could never exceed 300 Wh per day. With a
proper mount, a number of days exceed 300 Wh:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>1st</th>
          <th>2nd</th>
          <th>3rd</th>
          <th>4th</th>
          <th>5th</th>
          <th>6th</th>
          <th>7th</th>
          <th>8th</th>
          <th>9th</th>
          <th>10th</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2023 🌞 Wh</td>
          <td>133</td>
          <td>268</td>
          <td>262</td>
          <td>208</td>
          <td>271</td>
          <td>255</td>
          <td>274</td>
          <td>277</td>
          <td>275</td>
          <td>194</td>
      </tr>
      <tr>
          <td>2024 🌞 Wh</td>
          <td>529</td>
          <td>119</td>
          <td>246</td>
          <td>205</td>
          <td>160</td>
          <td>324</td>
          <td>265</td>
          <td>335</td>
          <td>73</td>
          <td>444</td>
      </tr>
  </tbody>
</table>
<h2 id="how-much-electricity-does-my-panel-generate">How much electricity does my panel generate?</h2>
<p>The exact electricity production numbers depend on how much sun ends up on the
solar panel. This in turn depends on the weather and how obstructed the solar
panel is (neighbors, trees, …).</p>
<p>I like measuring things, so I will share some measurements to give you a rough
idea. But note that measuring your solar panel is strictly optional.</p>
<p>On the best recorded day, my panel produced about 1.680 kWh of energy:</p>
<p><img src="2024-06-05-1680Wh.jpg" alt=""></p>
<p>The missing parts before 14:00 are caused by the neighbor’s house blocking the sun.</p>
<p>Now, compare this best case with the worst case, a January day with little sun
(&lt; 50 Wh):</p>
<p><img src="2024-12-04-solar-january-14th.jpg" alt=""></p>
<p>Let’s zoom out a bit and consider an entire year instead.</p>
<p>In 2024, the panel produced over 177 kWh so far, or, averaged to the daily
value, ≈0.5 kWh/day:</p>
<p><img src="2024-12-04-solar-whole-year.jpg" alt=""></p>
<p>Or, in numeric form (all numbers in kWh):</p>
<table>
  <thead>
      <tr>
          <th>Jan</th>
          <th>Feb</th>
          <th>Mar</th>
          <th>Apr</th>
          <th>May</th>
          <th>Jun</th>
          <th>Jul</th>
          <th>Aug</th>
          <th>Sep</th>
          <th>Oct</th>
          <th>Nov</th>
          <th>Dec</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2.9</td>
          <td>6.6</td>
          <td>10.9</td>
          <td>18.4</td>
          <td>29.1</td>
          <td>27.7</td>
          <td>37.6</td>
          <td>22.1</td>
          <td>12.0</td>
          <td>6.5</td>
          <td>3.3</td>
          <td>n/a</td>
      </tr>
  </tbody>
</table>
<!--

% for f in *.csv; do head -2 "$f"; (IFS=$'\n'; for measure in $(cut -d, -f2 "$f" |grep Wh); do units -t "$measure" Wh; done | tr '\n' '+'; echo 0) | bc; done 

| Month   | Solar energy (kWh) |
|---------|--------------------|
| 2024-01 | 2.901              |
| 2024-02 | 6.614              |
| 2024-03 | 10.956             |
| 2024-04 | 18.451             |
| 2024-05 | 29.150             |
| 2024-06 | 27.743             |
| 2024-07 | 37.683             |
| 2024-08 | 22.128             |
| 2024-09 | 12.074             |
| 2024-10 | 6.521              |
| 2024-11 | 3.361              |

-->
<h2 id="conclusion">Conclusion</h2>
<p>A solar panel is a great project to make incremental progress on. It’s just 3 to
4 simple steps, each of which is valuable on its own:</p>
<ol>
<li>Check with your landlord that installing an outdoor power socket and solar panel is okay.
<ul>
<li>Even if you personally do not go any further with your project, you can
share the result with your neighbors, who might…</li>
</ul>
</li>
<li>Order an outdoor power socket from your (or your landlord’s) preferred electrician.
<ul>
<li>Power will come in handy for lighting when spending summer evenings on the
balcony.</li>
</ul>
</li>
<li>Order a solar panel and plug it in.</li>
<li>Optional, but recommended: Optimize the mounting angle later.</li>
</ol>
<p>That’s it! Come on, get started right away 🌞</p>
]]></content>
  </entry>
</feed>
