<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>dev@iostream.pl</title><link>https://lvajxi03.github.io/</link><description>Recent content on dev@iostream.pl</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 05 Oct 2026 15:05:00 +0200</lastBuildDate><atom:link href="https://lvajxi03.github.io/index.xml" rel="self" type="application/rss+xml"/><item><title>Escape from the Tropics</title><link>https://lvajxi03.github.io/posts/2026-10-05/</link><pubDate>Mon, 05 Oct 2026 15:05:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-10-05/</guid><description>&lt;p&gt;&lt;strong&gt;KDE Plasma&lt;/strong&gt; is a remarkably pleasant environment for work — and for pretty much everything else — so there is very little to complain about.&lt;/p&gt;
&lt;p&gt;However, since I have already taken the first steps toward moving completely to &lt;strong&gt;FVWM3&lt;/strong&gt;, a little consistency seems appropriate.&lt;/p&gt;
&lt;p&gt;It is time to start replacing some of the applications from the KDE ecosystem as well.&lt;/p&gt;
&lt;p&gt;Not all at once.&lt;/p&gt;
&lt;p&gt;No need to panic.&lt;/p&gt;
&lt;p&gt;First, we need a survival kit.&lt;/p&gt;
&lt;h1 id="minimal-version"&gt;Minimal Version
&lt;/h1&gt;&lt;p&gt;The absolute minimum I would find difficult to live without is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;another terminal → &lt;strong&gt;Alacritty&lt;/strong&gt;, because why not,&lt;/li&gt;
&lt;li&gt;another file manager → &lt;strong&gt;PCManFM-Qt&lt;/strong&gt; — I used its GTK ancestor about twenty years ago, so now the Qt version gets a chance,&lt;/li&gt;
&lt;li&gt;Qt appearance configuration → &lt;strong&gt;qt6ct&lt;/strong&gt; — without it, surprises await,&lt;/li&gt;
&lt;li&gt;somewhat more deliberate wallpaper handling → &lt;strong&gt;feh&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;a launcher slightly more sophisticated than &lt;code&gt;dmenu&lt;/code&gt; → &lt;strong&gt;rofi&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;display configuration management → &lt;strong&gt;autorandr&lt;/strong&gt; and &lt;strong&gt;arandr&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is one additional detail with &lt;code&gt;qt6ct&lt;/code&gt;: Qt applications need to know that they are supposed to use it.&lt;/p&gt;
&lt;p&gt;An FVWM session will therefore eventually need something along the lines of:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;export QT_QPA_PLATFORMTHEME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;qt6ct
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Simply installing &lt;code&gt;qt6ct&lt;/code&gt; does not magically make it take control of every Qt application.&lt;/p&gt;
&lt;p&gt;Unfortunately.&lt;/p&gt;
&lt;p&gt;The magic comes later.&lt;/p&gt;
&lt;h1 id="standard-version"&gt;Standard Version
&lt;/h1&gt;&lt;p&gt;The standard version contains everything from the minimal one, plus:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bluetooth device management → &lt;strong&gt;blueman&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;display brightness control → &lt;strong&gt;brightnessctl&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;notifications → &lt;strong&gt;dunst&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;a PolicyKit authentication agent → &lt;strong&gt;lxqt-policykit&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;screenshots → &lt;strong&gt;flameshot&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;NetworkManager connection management → &lt;strong&gt;network-manager-applet&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;an audio mixer → &lt;strong&gt;pavucontrol&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;an X11 compositor → &lt;strong&gt;picom&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;media-player control → &lt;strong&gt;playerctl&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;automatic removable-media mounting through UDisks → &lt;strong&gt;udiskie&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;a volume-control applet → &lt;strong&gt;volumeicon&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;a second terminal, for variety → &lt;strong&gt;qterminal&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;a system tray → &lt;strong&gt;stalonetray&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At this point, an innocent little window manager starts looking suspiciously like a desktop environment.&lt;/p&gt;
&lt;p&gt;But that is fine.&lt;/p&gt;
&lt;p&gt;We still know exactly what it consists of.&lt;/p&gt;
&lt;h1 id="full-version"&gt;Full Version
&lt;/h1&gt;&lt;p&gt;Naturally, the &lt;em&gt;full&lt;/em&gt; version requires the standard version and all of its dependencies, and adds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;yet another terminal, just to prevent boredom → &lt;strong&gt;kitty&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;another file manager → &lt;strong&gt;Thunar&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three terminals may sound excessive.&lt;/p&gt;
&lt;p&gt;They are not.&lt;/p&gt;
&lt;h1 id="rpm-packages"&gt;RPM Packages
&lt;/h1&gt;&lt;p&gt;Since &lt;strong&gt;Fedora&lt;/strong&gt; is now my main operating system, there is an obvious next step:&lt;/p&gt;
&lt;p&gt;package all of this as RPMs.&lt;/p&gt;
&lt;p&gt;Right?&lt;/p&gt;
&lt;p&gt;Right.&lt;/p&gt;
&lt;p&gt;The packages do not even need to contain any programs or configuration files of their own.&lt;/p&gt;
&lt;p&gt;What we need are &lt;strong&gt;metapackages&lt;/strong&gt; — almost empty RPM packages whose main purpose is to depend on other packages.&lt;/p&gt;
&lt;p&gt;Instead of remembering fifteen package names, I should eventually be able to type:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install kde-refugee-pack-standard
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and be done with it.&lt;/p&gt;
&lt;h2 id="tools"&gt;Tools
&lt;/h2&gt;&lt;p&gt;First, install the basic RPM-building tools:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install rpm-build rpmdevtools rpmlint
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create the standard build tree:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rpmdev-setuptree
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and generate a fresh spec template:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rpmdev-newspec my-package.spec
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A typical spec template contains fields such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Name&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Version&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Release&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Summary&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;License&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;URL&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Source&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BuildRequires&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Requires&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also contains sections such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%description&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%prep&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%build&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%install&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%files&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%changelog&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a package consisting entirely of dependencies, most of these are unnecessary.&lt;/p&gt;
&lt;p&gt;There are no sources.&lt;/p&gt;
&lt;p&gt;There is nothing to compile.&lt;/p&gt;
&lt;p&gt;There is not even anything to install into the filesystem.&lt;/p&gt;
&lt;h2 id="minimal"&gt;Minimal
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;kde-refugee-pack-minimal.spec&lt;/code&gt; file can therefore look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-rpm" data-lang="rpm"&gt;Name: kde-refugee-pack-minimal
Version: 0.0.1
Release: 1%{?dist}
Summary: KDE refugee pack, minimal version

License: MIT
BuildArch: noarch

Requires: alacritty
Requires: qt6ct
Requires: pcmanfm-qt
Requires: feh
Requires: rofi
Requires: autorandr
Requires: arandr

%description
Mandatory metapackage for everyone who leaves KDE for fvwm3
(minimal version)

%files

%changelog
* Sat Oct 03 2026 Marcin Bielewicz &amp;lt;marcin.bielewicz@gmail.com&amp;gt;
- initial version
&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;
 &lt;p&gt;Important: keep the empty &lt;code&gt;%files&lt;/code&gt; section.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;The package does not contain any files, but we still want &lt;code&gt;rpmbuild&lt;/code&gt; to produce a binary RPM.&lt;/p&gt;
&lt;p&gt;That is its entire purpose:&lt;/p&gt;
&lt;p&gt;to exist and require other packages.&lt;/p&gt;
&lt;h2 id="standard"&gt;Standard
&lt;/h2&gt;&lt;p&gt;The next level depends on the first one:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-rpm" data-lang="rpm"&gt;Name: kde-refugee-pack-standard
Version: 0.0.1
Release: 1%{?dist}
Summary: KDE refugee pack, standard version

License: MIT
BuildArch: noarch

Requires: kde-refugee-pack-minimal = %{version}
Requires: blueman
Requires: brightnessctl
Requires: dunst
Requires: flameshot
Requires: lxqt-policykit
Requires: network-manager-applet
Requires: pavucontrol
Requires: picom
Requires: playerctl
Requires: udiskie
Requires: volumeicon
Requires: qterminal
Requires: stalonetray

%description
Mandatory metapackage for everyone who leaves KDE for fvwm3
(standard version)

%files

%changelog
* Sat Oct 03 2026 Marcin Bielewicz &amp;lt;marcin.bielewicz@gmail.com&amp;gt;
- initial version
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The important line is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-rpm" data-lang="rpm"&gt;Requires: kde-refugee-pack-minimal = %{version}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;which means:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;I require &lt;code&gt;kde-refugee-pack-minimal&lt;/code&gt; in the same version as this package.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Of course, this could be written explicitly:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-rpm" data-lang="rpm"&gt;Requires: kde-refugee-pack-minimal = 0.0.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;but &lt;code&gt;%{version}&lt;/code&gt; saves us from having to remember to change it in the next release.&lt;/p&gt;
&lt;p&gt;Laziness.&lt;/p&gt;
&lt;p&gt;Once again, the driving force behind computer science.&lt;/p&gt;
&lt;h2 id="full"&gt;Full
&lt;/h2&gt;&lt;p&gt;The final level:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-rpm" data-lang="rpm"&gt;Name: kde-refugee-pack-full
Version: 0.0.1
Release: 1%{?dist}
Summary: KDE refugee pack, full version

License: MIT
BuildArch: noarch

Requires: kde-refugee-pack-standard = %{version}
Requires: kitty
Requires: Thunar

%description
Mandatory metapackage for everyone who leaves KDE for fvwm3
(full version)

%files

%changelog
* Sat Oct 03 2026 Marcin Bielewicz &amp;lt;marcin.bielewicz@gmail.com&amp;gt;
- initial version
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This gives us a dependency chain:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;full
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── standard
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── minimal
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;with each level adding its own collection of tools.&lt;/p&gt;
&lt;h1 id="building-the-packages"&gt;Building the Packages
&lt;/h1&gt;&lt;p&gt;A binary RPM can be built with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rpmbuild -bb package.spec
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The result will normally appear under:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;~/rpmbuild/RPMS/noarch/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;-bb&lt;/code&gt; option builds the binary package only.&lt;/p&gt;
&lt;p&gt;If we also want an SRPM:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rpmbuild -ba package.spec
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That is what I will use in CI.&lt;/p&gt;
&lt;p&gt;If the computer is doing the work anyway, there is little reason to stop it.&lt;/p&gt;
&lt;h1 id="installing-the-packages"&gt;Installing the Packages
&lt;/h1&gt;&lt;p&gt;A local RPM can be installed through DNF:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install ./package.rpm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;./&lt;/code&gt; matters: it tells DNF that this is a local file rather than a package name to search for in configured repositories.&lt;/p&gt;
&lt;p&gt;DNF then resolves and installs the dependencies.&lt;/p&gt;
&lt;p&gt;Which was the whole point of this exercise.&lt;/p&gt;
&lt;h1 id="github-actions"&gt;GitHub Actions
&lt;/h1&gt;&lt;p&gt;Once the spec files live in a repository, rebuilding them manually every time starts looking suspiciously like a task that could be automated.&lt;/p&gt;
&lt;p&gt;Tasks like that are dangerous.&lt;/p&gt;
&lt;p&gt;They tend to produce CI pipelines.&lt;/p&gt;
&lt;p&gt;The workflow can look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build RPM packages&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;paths&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;specs/**&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;.github/workflows/rpm.yml&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;pull_request&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;paths&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;specs/**&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;.github/workflows/rpm.yml&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;permissions&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;contents&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;read&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build ${{ matrix.package }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;strategy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;fail-fast&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;matrix&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;package&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;kde-refugee-pack-minimal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;kde-refugee-pack-standard&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;kde-refugee-pack-full&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;fedora:44&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Install build tools&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; dnf install -y \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; findutils \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rpm-build \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rpmdevtools \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rpmlint&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Checkout repository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Prepare rpmbuild tree&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;rpmdev-setuptree&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Copy spec&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; cp specs/${{ matrix.package }}.spec ~/rpmbuild/SPECS/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Lint spec&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rpmlint specs/${{ matrix.package }}.spec&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build RPM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rpmbuild -ba ~/rpmbuild/SPECS/${{ matrix.package }}.spec&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Show result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; find ~/rpmbuild/RPMS ~/rpmbuild/SRPMS -type f -print&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Collect build artifacts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mkdir -p artifacts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; cp ~/rpmbuild/RPMS/noarch/*.rpm artifacts/
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; cp ~/rpmbuild/SRPMS/*.src.rpm artifacts/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Upload build bundle&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/upload-artifact@v7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;bundle-${{ matrix.package }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;artifacts/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Upload binary RPM directly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/upload-artifact@v7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;~/rpmbuild/RPMS/noarch/${{ matrix.package }}-*.noarch.rpm&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;archive&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;install-test&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Test package installation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;needs&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;fedora:44&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Download kde-refugee-pack-minimal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/download-artifact@v8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;bundle-kde-refugee-pack-minimal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;rpms/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Download kde-refugee-pack-standard&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/download-artifact@v8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;bundle-kde-refugee-pack-standard&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;rpms/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Download kde-refugee-pack-full&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/download-artifact@v8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;bundle-kde-refugee-pack-full&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;rpms/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Install refugee pack&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; dnf install -y \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rpms/kde-refugee-pack-minimal-*.noarch.rpm \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rpms/kde-refugee-pack-standard-*.noarch.rpm \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rpms/kde-refugee-pack-full-*.noarch.rpm&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At first glance, this line may look odd:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We are building Fedora packages, after all.&lt;/p&gt;
&lt;p&gt;A few lines later, however:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;container&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;fedora:44&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;GitHub provides the runner, while the job itself executes in a Fedora 44 environment.&lt;/p&gt;
&lt;p&gt;There is no need for a separate Fedora runner.&lt;/p&gt;
&lt;p&gt;The matrix:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;matrix&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;package&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;kde-refugee-pack-minimal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;kde-refugee-pack-standard&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;kde-refugee-pack-full&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;creates three independent builds.&lt;/p&gt;
&lt;p&gt;Each spec file is checked with &lt;code&gt;rpmlint&lt;/code&gt;, built, and stored as an artifact.&lt;/p&gt;
&lt;p&gt;And this is where things become pleasantly convenient.&lt;/p&gt;
&lt;p&gt;A normal artifact upload creates an archive containing the RPM and SRPM.&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;actions/upload-artifact@v7&lt;/code&gt; also supports:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;archive&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;for a single file.&lt;/p&gt;
&lt;p&gt;So every binary RPM is additionally available directly as an &lt;code&gt;.rpm&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;No ZIP extraction required.&lt;/p&gt;
&lt;p&gt;Like civilized people.&lt;/p&gt;
&lt;p&gt;The second job downloads all three build bundles and tries to install the packages on a fresh Fedora environment.&lt;/p&gt;
&lt;p&gt;The workflow therefore verifies more than:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;the RPM builds.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;It also verifies:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;DNF can resolve the dependency chain and install all three levels.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;And because the workflow itself lives at:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.github/workflows/rpm.yml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;while the path filter contains:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;paths&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;specs/**&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;.github/workflows/rpm.yml&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;changing the workflow itself triggers another run.&lt;/p&gt;
&lt;p&gt;The pipeline therefore tests, among other things, itself.&lt;/p&gt;
&lt;h1 id="summary"&gt;Summary
&lt;/h1&gt;&lt;p&gt;A small joke of a task somehow grew into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a list of programs I will actually use,&lt;/li&gt;
&lt;li&gt;three levels of a KDE survival kit,&lt;/li&gt;
&lt;li&gt;three RPM metapackages,&lt;/li&gt;
&lt;li&gt;automated linting,&lt;/li&gt;
&lt;li&gt;automated builds,&lt;/li&gt;
&lt;li&gt;an installation test,&lt;/li&gt;
&lt;li&gt;artifacts available both as bundles and directly as RPM files,&lt;/li&gt;
&lt;li&gt;and a GitHub repository: &lt;a class="link" href="https://github.com/lvajxi03/specfiles/" target="_blank" rel="noopener"
 &gt;specfiles&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All because I did not want to type a dozen package names by hand.&lt;/p&gt;
&lt;p&gt;It was laziness, Your Honor.&lt;/p&gt;</description></item><item><title>Customize Me</title><link>https://lvajxi03.github.io/posts/2026-09-30/</link><pubDate>Wed, 30 Sep 2026 16:20:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-09-30/</guid><description>&lt;p&gt;The King of Editors works remarkably well with its default settings.&lt;/p&gt;
&lt;p&gt;The interface is well thought out, the functionality is all there, and most things are roughly where they ought to be.&lt;/p&gt;
&lt;p&gt;Nothing left to do but use it.&lt;/p&gt;
&lt;p&gt;And use it.&lt;/p&gt;
&lt;p&gt;And then use it some more.&lt;/p&gt;
&lt;p&gt;Emacs, however, also tries to be universal enough to accommodate almost any personal preference, just like other editors.&lt;/p&gt;
&lt;p&gt;The difference is that while most applications moved two or three steps beyond the traditional preferences dialog containing five checkboxes and a dropdown menu, Emacs ran about five kilometers farther.&lt;/p&gt;
&lt;p&gt;And it is still accelerating.&lt;/p&gt;
&lt;h1 id="at-first-glance"&gt;At First Glance
&lt;/h1&gt;&lt;p&gt;Exploring a standard Emacs frame reveals an &lt;strong&gt;Options&lt;/strong&gt; entry in the main menu.&lt;/p&gt;
&lt;p&gt;Open it and you will find a perfectly sensible collection of commonly used settings.&lt;/p&gt;
&lt;p&gt;You can change them.&lt;/p&gt;
&lt;p&gt;You can save them.&lt;/p&gt;
&lt;p&gt;Restart Emacs and, surprisingly enough, they are still there.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Options" class="gallery-image" data-flex-basis="241px" data-flex-grow="100" height="768" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-30/emacs-options.png" width="773"&gt;&lt;/p&gt;
&lt;p&gt;Simple?&lt;/p&gt;
&lt;p&gt;Simple.&lt;/p&gt;
&lt;p&gt;Anyone who has been paying attention during the previous episodes about the Best of All Editors should already suspect that this is far too straightforward for Emacs.&lt;/p&gt;
&lt;p&gt;Somewhere, the real mechanism must be waiting.&lt;/p&gt;
&lt;p&gt;Something that makes you feel alive again.&lt;/p&gt;
&lt;h1 id="customize"&gt;Customize
&lt;/h1&gt;&lt;p&gt;That secret weapon is &lt;strong&gt;Customize&lt;/strong&gt; — Emacs&amp;rsquo;s universal interface for changing everything its developers explicitly marked as customizable.&lt;/p&gt;
&lt;p&gt;Which is a very, very large portion of Emacs.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Observation based on extensive personal research: eleven out of ten users claim that absolutely everything can be configured, while approximately 107 percent of programmers insist that it is merely &lt;em&gt;almost&lt;/em&gt; everything.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;What is a setting, after all?&lt;/p&gt;
&lt;p&gt;In greatly simplified terms, it is a named value.&lt;/p&gt;
&lt;p&gt;So if literally everything could be configured, a hypothetical preferences dialog containing every possible option would need to be&amp;hellip;&lt;/p&gt;
&lt;p&gt;[several tedious calculations follow]&lt;/p&gt;
&lt;p&gt;&amp;hellip;well, I am not sure there are enough 4K monitors in my city.&lt;/p&gt;
&lt;p&gt;More seriously, &lt;strong&gt;Customize&lt;/strong&gt; approaches configuration differently.&lt;/p&gt;
&lt;p&gt;Related settings are grouped together.&lt;/p&gt;
&lt;p&gt;Groups contain subgroups.&lt;/p&gt;
&lt;p&gt;Those may contain more subgroups.&lt;/p&gt;
&lt;p&gt;The result is an enormous hierarchy that can look slightly intimidating at first, but quickly becomes logical and surprisingly easy to navigate.&lt;/p&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M-x customize
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and Emacs creates a special buffer containing the top-level customization groups.&lt;/p&gt;
&lt;p&gt;Its title?&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Customization of the One True Editor.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Excellent.&lt;/p&gt;
&lt;p&gt;I need to remember that.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Customize" class="gallery-image" data-flex-basis="304px" data-flex-grow="126" height="693" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-30/emacs-customize.png" srcset="https://lvajxi03.github.io/posts/2026-09-30/emacs-customize_hu_ef782e4de6fd9888.png 800w, https://lvajxi03.github.io/posts/2026-09-30/emacs-customize.png 880w" width="880"&gt;&lt;/p&gt;
&lt;p&gt;Among other things, the buffer contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a search field,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;Revert&amp;hellip;&lt;/strong&gt; button,&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;Apply&lt;/strong&gt; button,&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;Apply and Save&lt;/strong&gt; button,&lt;/li&gt;
&lt;li&gt;the group&amp;rsquo;s title and description,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;State&lt;/strong&gt; button,&lt;/li&gt;
&lt;li&gt;subgroups with descriptions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Group names are links.&lt;/p&gt;
&lt;p&gt;They can be activated with the mouse, or by moving point onto them and pressing &lt;code&gt;RET&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Naturally, the search field works.&lt;/p&gt;
&lt;p&gt;And with this many settings, it rapidly becomes one of the most important parts of the entire interface.&lt;/p&gt;
&lt;p&gt;There is one useful caveat: Customize searches settings belonging to features that have already been loaded into the current Emacs session.&lt;/p&gt;
&lt;h2 id="revert"&gt;Revert
&lt;/h2&gt;&lt;p&gt;The &lt;strong&gt;Revert&amp;hellip;&lt;/strong&gt; button opens a menu of reset operations.&lt;/p&gt;
&lt;p&gt;The most important ones are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Undo Edits in Customization Buffer&lt;/strong&gt; — discard edits made in this buffer that have not yet been applied,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revert This Session&amp;rsquo;s Customizations&lt;/strong&gt; — restore values from before changes made during this session,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Erase Customizations&lt;/strong&gt; — remove saved customizations and return to standard values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one becomes particularly useful after an adventurous experiment.&lt;/p&gt;
&lt;p&gt;Which means sooner or later.&lt;/p&gt;
&lt;h2 id="apply-and-apply-and-save"&gt;Apply and Apply and Save
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Apply&lt;/strong&gt; makes all edited values in the current Customize buffer effective for the current Emacs session.&lt;/p&gt;
&lt;p&gt;Exit Emacs and they are gone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Apply and Save&lt;/strong&gt; does the same thing, but also stores the settings for future sessions.&lt;/p&gt;
&lt;p&gt;Under the hood, Emacs writes Lisp code representing those settings — normally into your initialization file.&lt;/p&gt;
&lt;p&gt;You can also direct Customize output into a separate &lt;code&gt;custom-file&lt;/code&gt;, which becomes increasingly attractive as the configuration grows.&lt;/p&gt;
&lt;p&gt;We will return to that later.&lt;/p&gt;
&lt;h2 id="state"&gt;State
&lt;/h2&gt;&lt;p&gt;Individual options and groups also have their own &lt;strong&gt;State&lt;/strong&gt; buttons.&lt;/p&gt;
&lt;p&gt;These let you operate on a single setting rather than everything currently shown in the buffer.&lt;/p&gt;
&lt;p&gt;Depending on its current state, the menu can contain operations such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Set for Current Session&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save for Future Sessions&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Undo Edits&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revert This Session&amp;rsquo;s Customizations&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Erase Customization&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So one setting can be tested temporarily, another can be saved permanently, and everything else can remain untouched.&lt;/p&gt;
&lt;h1 id="going-deeper"&gt;Going Deeper
&lt;/h1&gt;&lt;p&gt;Let us open an actual group.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Environment → Dnd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What DND does is not important just yet.&lt;/p&gt;
&lt;p&gt;The point is to see several different kinds of settings.&lt;/p&gt;
&lt;p&gt;Below the search field, standard buttons, group title, and description, the actual options begin.&lt;/p&gt;
&lt;p&gt;Some are collapsed and can be expanded with the triangle button.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dnd Direct Save Remote Files&lt;/strong&gt; — a &lt;strong&gt;Choice&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dnd Indicate Insertion Point&lt;/strong&gt; — a Boolean value,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dnd Open Remote File Function&lt;/strong&gt; — a function,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dnd Protocol Alist&lt;/strong&gt; — a list.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Dnd Group" class="gallery-image" data-flex-basis="234px" data-flex-grow="97" height="969" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-30/emacs-dnd.png" srcset="https://lvajxi03.github.io/posts/2026-09-30/emacs-dnd_hu_5366811cb2c18a34.png 800w, https://lvajxi03.github.io/posts/2026-09-30/emacs-dnd.png 948w" width="948"&gt;&lt;/p&gt;
&lt;p&gt;And this is where it becomes clear why Customize is not simply another preferences window.&lt;/p&gt;
&lt;h2 id="choice"&gt;Choice
&lt;/h2&gt;&lt;p&gt;For a &lt;strong&gt;Choice&lt;/strong&gt; value, the &lt;strong&gt;Value Menu&lt;/strong&gt; button displays the permitted alternatives.&lt;/p&gt;
&lt;p&gt;You do not type arbitrary text.&lt;/p&gt;
&lt;p&gt;You select one of the values anticipated by the author of the option.&lt;/p&gt;
&lt;h2 id="boolean"&gt;Boolean
&lt;/h2&gt;&lt;p&gt;A Boolean setting provides a simple switch:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;off (nil)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;on (non-nil)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The traditional yes-or-no question, translated into Emacs.&lt;/p&gt;
&lt;h2 id="function"&gt;Function
&lt;/h2&gt;&lt;p&gt;An option whose value is a function lets you specify the name of the function that should be used.&lt;/p&gt;
&lt;p&gt;And here, for the first time, the shadow of Lisp appears.&lt;/p&gt;
&lt;p&gt;Still far away.&lt;/p&gt;
&lt;p&gt;But watching.&lt;/p&gt;
&lt;h2 id="list"&gt;List
&lt;/h2&gt;&lt;p&gt;Lists can be extended, shortened, and edited.&lt;/p&gt;
&lt;p&gt;Buttons such as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;INS
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DEL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;allow elements to be inserted or removed.&lt;/p&gt;
&lt;p&gt;In the case of &lt;code&gt;Dnd Protocol Alist&lt;/code&gt;, the value is specifically a list of pairs — an association list, or &lt;code&gt;alist&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We will return to that word as well.&lt;/p&gt;
&lt;p&gt;The important part is that Customize knows the type of each option.&lt;/p&gt;
&lt;p&gt;It does not simply throw one enormous text box at the user.&lt;/p&gt;
&lt;p&gt;It knows whether a value is Boolean, numeric, textual, a function, a choice among alternatives, or a more complex structure.&lt;/p&gt;
&lt;p&gt;Then it builds an appropriate editing widget.&lt;/p&gt;
&lt;p&gt;Now things are beginning to feel properly Emacs-like.&lt;/p&gt;
&lt;h1 id="the-shorter-route"&gt;The Shorter Route
&lt;/h1&gt;&lt;p&gt;Walking through the hierarchy is excellent when learning what Emacs can do.&lt;/p&gt;
&lt;p&gt;It is less attractive when you already know what you want.&lt;/p&gt;
&lt;p&gt;If you know the name of the relevant item, you can jump directly to it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M-x customize-option
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M-x customize-face
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M-x customize-group
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There is also:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M-x customize-apropos
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which searches for matching settings and groups.&lt;/p&gt;
&lt;p&gt;After a while, these commands tend to become much more common than opening &lt;code&gt;M-x customize&lt;/code&gt; from the very top.&lt;/p&gt;
&lt;h1 id="themes"&gt;Themes
&lt;/h1&gt;&lt;p&gt;Emacs has also provided a theme system for quite some time.&lt;/p&gt;
&lt;p&gt;To browse available themes, use:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M-x customize-themes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note the plural: &lt;code&gt;customize-themes&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A buffer containing available themes appears.&lt;/p&gt;
&lt;p&gt;Choose one, enable it, and see what happens.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Emacs Themes" class="gallery-image" data-flex-basis="255px" data-flex-grow="106" height="815" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-30/emacs-themes.png" srcset="https://lvajxi03.github.io/posts/2026-09-30/emacs-themes_hu_7ad56ae95e2bde89.png 800w, https://lvajxi03.github.io/posts/2026-09-30/emacs-themes.png 866w" width="866"&gt;&lt;/p&gt;
&lt;p&gt;An Emacs Custom theme can affect not only colors but also option values.&lt;/p&gt;
&lt;p&gt;Technically, a theme is simply an Emacs Lisp file containing the relevant settings.&lt;/p&gt;
&lt;p&gt;You can even create your own:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M-x customize-create-theme
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But that is a story for another day.&lt;/p&gt;
&lt;h1 id="faces"&gt;Faces
&lt;/h1&gt;&lt;p&gt;Another particularly entertaining area to experiment with is &lt;strong&gt;faces&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The simplest way to think of them is as named styles used to display text.&lt;/p&gt;
&lt;p&gt;A face can define attributes such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;font family,&lt;/li&gt;
&lt;li&gt;size,&lt;/li&gt;
&lt;li&gt;weight,&lt;/li&gt;
&lt;li&gt;slant,&lt;/li&gt;
&lt;li&gt;foreground color,&lt;/li&gt;
&lt;li&gt;background color,&lt;/li&gt;
&lt;li&gt;underline.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Emacs Faces" class="gallery-image" data-flex-basis="295px" data-flex-grow="123" height="559" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-30/emacs-faces.png" width="688"&gt;&lt;/p&gt;
&lt;p&gt;You can enter the &lt;strong&gt;Faces&lt;/strong&gt; group through the main Customize buffer, or jump directly to a specific one with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M-x customize-face
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and choose the face you want to modify:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Available Faces" class="gallery-image" data-flex-basis="512px" data-flex-grow="213" height="1188" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-30/emacs-available-faces.png" srcset="https://lvajxi03.github.io/posts/2026-09-30/emacs-available-faces_hu_41077e6ef4f546f3.png 800w, https://lvajxi03.github.io/posts/2026-09-30/emacs-available-faces_hu_c4ec6685b2a00631.png 1600w, https://lvajxi03.github.io/posts/2026-09-30/emacs-available-faces_hu_98b80fa37c48cbdc.png 2400w, https://lvajxi03.github.io/posts/2026-09-30/emacs-available-faces.png 2535w" width="2535"&gt;&lt;/p&gt;
&lt;p&gt;The basic face is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;default
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It provides the default appearance of displayed text, and its background color also determines the frame background.&lt;/p&gt;
&lt;p&gt;That is usually a sensible place to begin.&lt;/p&gt;
&lt;p&gt;But this is Emacs.&lt;/p&gt;
&lt;p&gt;There is no requirement to begin sensibly.&lt;/p&gt;
&lt;p&gt;Everyone is master of their own session.&lt;/p&gt;
&lt;p&gt;And later the blacksmith of their own fate, which may become unpleasant when &lt;code&gt;2&lt;/code&gt; is entered as the font size instead of &lt;code&gt;120&lt;/code&gt;.&lt;/p&gt;
&lt;h1 id="what-next"&gt;What Next?
&lt;/h1&gt;&lt;p&gt;That is probably enough for a first encounter with configuration.&lt;/p&gt;
&lt;p&gt;We now know that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Emacs has an interactive configuration interface,&lt;/li&gt;
&lt;li&gt;settings are arranged hierarchically,&lt;/li&gt;
&lt;li&gt;values have types,&lt;/li&gt;
&lt;li&gt;changes can be applied temporarily or saved,&lt;/li&gt;
&lt;li&gt;individual options, groups, and faces can be opened directly,&lt;/li&gt;
&lt;li&gt;Customize also handles themes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The next step is to look under the hood.&lt;/p&gt;
&lt;p&gt;Where exactly are these settings stored?&lt;/p&gt;
&lt;p&gt;What lands in &lt;code&gt;init.el&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;What is &lt;code&gt;custom-file&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;And why does manual Emacs configuration sooner or later lead to the standard question:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Which language would you choose for configuration, and why is it Lisp?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;That is enough for today.&lt;/p&gt;
&lt;p&gt;Nice?&lt;/p&gt;
&lt;p&gt;Nice.&lt;/p&gt;
&lt;p&gt;Easy?&lt;/p&gt;
&lt;p&gt;Easy.&lt;/p&gt;
&lt;p&gt;Future episodes will be even nicer.&lt;/p&gt;
&lt;p&gt;Easier?&lt;/p&gt;
&lt;p&gt;Well&amp;hellip;&lt;/p&gt;</description></item><item><title>A Window Manager Unlike Any Other</title><link>https://lvajxi03.github.io/posts/2026-09-28/</link><pubDate>Mon, 28 Sep 2026 19:10:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-09-28/</guid><description>&lt;p&gt;Thirty-three years is a very long time in software.&lt;/p&gt;
&lt;p&gt;Trends change.&lt;/p&gt;
&lt;p&gt;Architectures come and go.&lt;/p&gt;
&lt;p&gt;Companies collapse, occasionally taking genuinely excellent projects to the grave with them.&lt;/p&gt;
&lt;p&gt;It is long enough that, back in 1993, I was considering moving from a Commodore 64 to an Amiga 500 — which never actually happened — while in 2026 I find myself interested in software from that same era which somehow still exists.&lt;/p&gt;
&lt;p&gt;And not merely exists.&lt;/p&gt;
&lt;p&gt;Some of it is doing remarkably well.&lt;/p&gt;
&lt;p&gt;In 1993, engineer &lt;strong&gt;Robert Nation&lt;/strong&gt; needed a lightweight window manager for a 33 MHz 486 laptop with just 4 MB of RAM.&lt;/p&gt;
&lt;p&gt;At the same time, his work involved analyzing enormous spectrograms, and he needed a virtual desktop larger than a single physical screen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TWM&lt;/strong&gt; worked, but it consumed more memory than he liked and did not provide everything he needed.&lt;/p&gt;
&lt;p&gt;So he did what any reasonable programmer dissatisfied with an existing tool would do:&lt;/p&gt;
&lt;p&gt;he started taking it apart.&lt;/p&gt;
&lt;p&gt;The result was &lt;strong&gt;FVWM&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Its first public version was bundled with an &lt;code&gt;rxvt&lt;/code&gt; release on June 1, 1993. Soon afterward FVWM became an independent project, and it is still with us today.&lt;/p&gt;
&lt;p&gt;Despite more than three decades of development, it has retained much of its original character:&lt;/p&gt;
&lt;p&gt;it is lightweight, modular, extraordinarily configurable, and assumes that the user may actually know how their own desktop should behave.&lt;/p&gt;
&lt;h1 id="f-vwm"&gt;F? VWM
&lt;/h1&gt;&lt;p&gt;So what does the name mean?&lt;/p&gt;
&lt;p&gt;Today the official project calls it:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;F? Virtual Window Manager&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;where &lt;code&gt;F?&lt;/code&gt; can be expanded into almost anything beginning with the letter F.&lt;/p&gt;
&lt;p&gt;Preferably something printable.&lt;/p&gt;
&lt;p&gt;Originally, however, the &lt;code&gt;F&lt;/code&gt; had a very specific meaning:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feeble Virtual Window Manager&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Robert Nation himself confirmed this, explaining that the earliest versions offered very few user-selectable features, so &lt;em&gt;feeble&lt;/em&gt; was a fairly accurate description.&lt;/p&gt;
&lt;p&gt;The software eventually stopped being feeble in any meaningful sense, while the interpretation of the letter F drifted into folklore.&lt;/p&gt;
&lt;p&gt;The modern project page suggests words such as:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;fantastic, fabulous, famous, flexible&amp;hellip;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;and wisely leaves the rest to the user&amp;rsquo;s imagination.&lt;/p&gt;
&lt;h2 id="what-exactly-is-a-window-manager"&gt;What Exactly Is a Window Manager?
&lt;/h2&gt;&lt;p&gt;That is a reasonable question.&lt;/p&gt;
&lt;p&gt;What sort of software is this?&lt;/p&gt;
&lt;p&gt;And why should anyone care?&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;window manager&lt;/strong&gt; manages application windows within a graphical session.&lt;/p&gt;
&lt;p&gt;In the X11 world it is responsible for things such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;window placement,&lt;/li&gt;
&lt;li&gt;size,&lt;/li&gt;
&lt;li&gt;stacking order,&lt;/li&gt;
&lt;li&gt;focus,&lt;/li&gt;
&lt;li&gt;decorations,&lt;/li&gt;
&lt;li&gt;minimization,&lt;/li&gt;
&lt;li&gt;maximization,&lt;/li&gt;
&lt;li&gt;switching between windows.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An application creates a window.&lt;/p&gt;
&lt;p&gt;The window manager decides what happens to it next.&lt;/p&gt;
&lt;p&gt;And that is all?&lt;/p&gt;
&lt;p&gt;More or less.&lt;/p&gt;
&lt;p&gt;Which is exactly what makes the idea interesting.&lt;/p&gt;
&lt;p&gt;GNOME, KDE Plasma, Xfce, and LXQt are complete &lt;strong&gt;desktop environments&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;They are collections of cooperating components that usually provide things such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;panels,&lt;/li&gt;
&lt;li&gt;menus,&lt;/li&gt;
&lt;li&gt;session management,&lt;/li&gt;
&lt;li&gt;display configuration,&lt;/li&gt;
&lt;li&gt;settings tools,&lt;/li&gt;
&lt;li&gt;notifications,&lt;/li&gt;
&lt;li&gt;desktop icons,&lt;/li&gt;
&lt;li&gt;wallpapers,&lt;/li&gt;
&lt;li&gt;device integration,&lt;/li&gt;
&lt;li&gt;often an entire set of applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A window manager is only one part of that puzzle.&lt;/p&gt;
&lt;p&gt;In the old days, an X11 desktop was often assembled gradually.&lt;/p&gt;
&lt;p&gt;Need a panel?&lt;/p&gt;
&lt;p&gt;Install one.&lt;/p&gt;
&lt;p&gt;Need a menu?&lt;/p&gt;
&lt;p&gt;Add one.&lt;/p&gt;
&lt;p&gt;Want a wallpaper?&lt;/p&gt;
&lt;p&gt;Run a wallpaper setter.&lt;/p&gt;
&lt;p&gt;Need desktop icons, a notification area, media controls, or animated penguins wandering around the screen?&lt;/p&gt;
&lt;p&gt;No problem.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xpenguins&lt;/code&gt; is waiting.&lt;/p&gt;
&lt;p&gt;Today, we are much more likely to install an entire desktop environment and accept whatever came from the factory.&lt;/p&gt;
&lt;p&gt;And the factory keeps providing more.&lt;/p&gt;
&lt;p&gt;Increasingly tightly integrated.&lt;/p&gt;
&lt;p&gt;Eleven out of ten users never even change the wallpaper.&lt;/p&gt;
&lt;h2 id="what-does-fvwm-provide"&gt;What Does FVWM Provide?
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;FVWM&lt;/strong&gt; is a classic window manager in exactly that older tradition.&lt;/p&gt;
&lt;p&gt;Its primary concern is windows and how they behave.&lt;/p&gt;
&lt;p&gt;Want a panel?&lt;/p&gt;
&lt;p&gt;You can have one.&lt;/p&gt;
&lt;p&gt;Menus?&lt;/p&gt;
&lt;p&gt;Of course.&lt;/p&gt;
&lt;p&gt;A pager?&lt;/p&gt;
&lt;p&gt;Certainly.&lt;/p&gt;
&lt;p&gt;A window list?&lt;/p&gt;
&lt;p&gt;Also available.&lt;/p&gt;
&lt;p&gt;But FVWM does not assume that its developers know better than you how all of those things should look or behave.&lt;/p&gt;
&lt;p&gt;The default configuration provides enough to get started and then seems to say:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Run me.&lt;br&gt;
Use me.&lt;br&gt;
Decide what is missing.&lt;br&gt;
Then configure it yourself.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;That is what its extensive configuration system is for.&lt;/p&gt;
&lt;p&gt;The official project describes FVWM3 as a lightweight floating window manager for X11 that can be configured as anything from a small minimal setup to something resembling a complete desktop environment.&lt;/p&gt;
&lt;h1 id="installation"&gt;Installation
&lt;/h1&gt;&lt;p&gt;I assume you already have &lt;em&gt;some&lt;/em&gt; graphical environment installed.&lt;/p&gt;
&lt;p&gt;Modern Linux distributions usually come with KDE, GNOME, Xfce, LXQt, or something similar.&lt;/p&gt;
&lt;p&gt;You therefore probably also have a display manager.&lt;/p&gt;
&lt;p&gt;In my case, this is &lt;strong&gt;Fedora with KDE Plasma and SDDM&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Installation is straightforward:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install fvwm3 dmenu
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first package contains &lt;strong&gt;FVWM3&lt;/strong&gt; itself.&lt;/p&gt;
&lt;p&gt;The second provides &lt;code&gt;dmenu&lt;/code&gt;, which the default FVWM configuration uses as a simple application launcher.&lt;/p&gt;
&lt;p&gt;It is worth checking that the package installed an X session definition:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rpm -ql fvwm3 | grep xsessions
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On my system:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/share/xsessions/fvwm3.desktop
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Excellent.&lt;/p&gt;
&lt;p&gt;Log out.&lt;/p&gt;
&lt;p&gt;Select &lt;strong&gt;FVWM3&lt;/strong&gt; on the login screen.&lt;/p&gt;
&lt;p&gt;Enter the password.&lt;/p&gt;
&lt;p&gt;Press &lt;code&gt;Enter&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And&amp;hellip;&lt;/p&gt;
&lt;h1 id="hello-darkness"&gt;Hello Darkness
&lt;/h1&gt;&lt;p&gt;&amp;hellip;enjoy the majestic sight of absolutely nothing.&lt;/p&gt;
&lt;p&gt;The session did not start.&lt;/p&gt;
&lt;p&gt;The reason turned out to be simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FVWM3 is an X11 window manager&lt;/strong&gt;, while my Fedora installation only had Wayland available.&lt;/p&gt;
&lt;p&gt;FVWM3 is still primarily built around Xlib and the X Window System.&lt;/p&gt;
&lt;p&gt;So Xorg needs to be installed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install xorg-x11-server-Xorg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fedora 44 still ships Xorg separately for X11 sessions and software that depends on it.&lt;/p&gt;
&lt;p&gt;Log in once more.&lt;/p&gt;
&lt;p&gt;Ah.&lt;/p&gt;
&lt;p&gt;Much better.&lt;/p&gt;
&lt;h2 id="configuring-x11"&gt;Configuring X11
&lt;/h2&gt;&lt;p&gt;The biggest surprise after all these years is that X11 no longer needs to be configured.&lt;/p&gt;
&lt;p&gt;It simply starts.&lt;/p&gt;
&lt;p&gt;And works.&lt;/p&gt;
&lt;p&gt;There is no need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;select graphics drivers manually,&lt;/li&gt;
&lt;li&gt;describe the monitor,&lt;/li&gt;
&lt;li&gt;write &lt;code&gt;Modeline&lt;/code&gt; entries,&lt;/li&gt;
&lt;li&gt;configure the mouse,&lt;/li&gt;
&lt;li&gt;configure the keyboard,&lt;/li&gt;
&lt;li&gt;specify font directories.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I remember a time when starting X11 and getting a black screen instead of smoke already counted as progress.&lt;/p&gt;
&lt;p&gt;Today it simply works.&lt;/p&gt;
&lt;p&gt;Thank you, 2026.&lt;/p&gt;
&lt;h1 id="first-start"&gt;First Start
&lt;/h1&gt;&lt;p&gt;The default &lt;strong&gt;FVWM3&lt;/strong&gt; session looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Default session" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="1080" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-1.png" srcset="https://lvajxi03.github.io/posts/2026-09-28/fvwm-1_hu_ccb8a2f3b24802da.png 800w, https://lvajxi03.github.io/posts/2026-09-28/fvwm-1_hu_1d0e9028cb527683.png 1600w, https://lvajxi03.github.io/posts/2026-09-28/fvwm-1.png 1920w" width="1920"&gt;&lt;/p&gt;
&lt;p&gt;What exactly are we looking at?&lt;/p&gt;
&lt;p&gt;On the right is a panel constructed with the &lt;code&gt;FvwmButtons&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;It contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the FVWM logo — still maintaining the cat theme,&lt;/li&gt;
&lt;li&gt;desktop buttons &lt;code&gt;0–3&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;a pager,&lt;/li&gt;
&lt;li&gt;an area for open windows managed by &lt;code&gt;FvwmIconMan&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;a clock and date display.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is essentially what the current upstream default configuration defines.&lt;/p&gt;
&lt;p&gt;As applications are opened, the panel gradually fills up:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Top of the default panel" class="gallery-image" data-flex-basis="159px" data-flex-grow="66" height="197" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-1a.png" width="131"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="The panel filling up" class="gallery-image" data-flex-basis="135px" data-flex-grow="56" height="230" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-1b.png" width="130"&gt;&lt;/p&gt;
&lt;p&gt;That is already quite a lot.&lt;/p&gt;
&lt;p&gt;We can even launch something.&lt;/p&gt;
&lt;p&gt;Press:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Alt+Space
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and &lt;code&gt;dmenu_run&lt;/code&gt; appears.&lt;/p&gt;
&lt;p&gt;The current default FVWM configuration binds exactly that command to &lt;code&gt;Alt+Space&lt;/code&gt;, provided &lt;code&gt;dmenu&lt;/code&gt; is installed.&lt;/p&gt;
&lt;p&gt;It displays executables found through &lt;code&gt;$PATH&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Which means:&lt;/p&gt;
&lt;p&gt;a lot of things.&lt;/p&gt;
&lt;p&gt;Far too many things.&lt;/p&gt;
&lt;p&gt;Fortunately, the list is filtered as you type.&lt;/p&gt;
&lt;p&gt;&lt;img alt="dmenu" class="gallery-image" data-flex-basis="8276px" data-flex-grow="3448" height="35" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-7.png" srcset="https://lvajxi03.github.io/posts/2026-09-28/fvwm-7_hu_74944ae8d7d63df.png 800w, https://lvajxi03.github.io/posts/2026-09-28/fvwm-7.png 1207w" width="1207"&gt;&lt;/p&gt;
&lt;h2 id="windows"&gt;Windows
&lt;/h2&gt;&lt;p&gt;The first application window looks something like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="First window" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="1080" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-2.png" srcset="https://lvajxi03.github.io/posts/2026-09-28/fvwm-2_hu_b33654003e070d1b.png 800w, https://lvajxi03.github.io/posts/2026-09-28/fvwm-2_hu_1138e42d428c8304.png 1600w, https://lvajxi03.github.io/posts/2026-09-28/fvwm-2.png 1920w" width="1920"&gt;&lt;/p&gt;
&lt;p&gt;There is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a title bar,&lt;/li&gt;
&lt;li&gt;buttons on both sides,&lt;/li&gt;
&lt;li&gt;a frame,&lt;/li&gt;
&lt;li&gt;borders,&lt;/li&gt;
&lt;li&gt;corners.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Window buttons" class="gallery-image" data-flex-basis="463px" data-flex-grow="193" height="268" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-2a.png" width="518"&gt;&lt;/p&gt;
&lt;p&gt;The button on the left opens the window operations menu.&lt;/p&gt;
&lt;p&gt;The ones on the right perform more obvious jobs such as closing, maximizing, and minimizing.&lt;/p&gt;
&lt;p&gt;Drag the title bar to move the window.&lt;/p&gt;
&lt;p&gt;Drag an edge to resize it.&lt;/p&gt;
&lt;p&gt;Drag a corner to resize it in two directions at once.&lt;/p&gt;
&lt;p&gt;The technology is clearly almost ready for worldwide adoption.&lt;/p&gt;
&lt;p&gt;Click the &lt;strong&gt;left mouse button&lt;/strong&gt; on the root window and the main FVWM menu appears:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Default menu" class="gallery-image" data-flex-basis="148px" data-flex-grow="62" height="261" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-3.png" width="162"&gt;&lt;/p&gt;
&lt;p&gt;Not bad.&lt;/p&gt;
&lt;p&gt;The important commands are already present.&lt;/p&gt;
&lt;p&gt;Including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Restart&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Quit&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Restart&lt;/code&gt; will become extremely useful in future episodes.&lt;/p&gt;
&lt;p&gt;Trust me.&lt;/p&gt;
&lt;h2 id="desks-and-pages"&gt;Desks and Pages
&lt;/h2&gt;&lt;p&gt;One of FVWM&amp;rsquo;s more interesting features is its enormous virtual workspace.&lt;/p&gt;
&lt;p&gt;The default configuration provides four &lt;strong&gt;desks&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;0 1 2 3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each desk can itself contain multiple &lt;strong&gt;pages&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;FVWM therefore distinguishes between two concepts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Desk&lt;/strong&gt; — a separate desktop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Page&lt;/strong&gt; — a position within the larger virtual space of that desktop.&lt;/p&gt;
&lt;p&gt;With the normal global multi-monitor configuration, all connected displays together form the visible area of one page.&lt;/p&gt;
&lt;p&gt;This means the virtual desktop can extend beyond the complete physical monitor arrangement.&lt;/p&gt;
&lt;p&gt;Then you can have several such virtual spaces.&lt;/p&gt;
&lt;p&gt;And several desks containing those.&lt;/p&gt;
&lt;p&gt;Sounds excessive?&lt;/p&gt;
&lt;p&gt;Give it a few days.&lt;/p&gt;
&lt;p&gt;You will run out of room.&lt;/p&gt;
&lt;p&gt;This becomes especially useful when many windows are open and you want to group them by purpose.&lt;/p&gt;
&lt;p&gt;One desk for code.&lt;/p&gt;
&lt;p&gt;One for documentation.&lt;/p&gt;
&lt;p&gt;One for communication.&lt;/p&gt;
&lt;p&gt;One for terminals that have been open for so long that nobody dares close them anymore.&lt;/p&gt;
&lt;p&gt;Open a window and try dragging it beyond the current visible workspace.&lt;/p&gt;
&lt;p&gt;Near the edge, you will feel a slight resistance.&lt;/p&gt;
&lt;p&gt;Keep going.&lt;/p&gt;
&lt;p&gt;The window crosses onto the neighboring page.&lt;/p&gt;
&lt;p&gt;At that moment the physical screen stops feeling like the desktop itself.&lt;/p&gt;
&lt;p&gt;It becomes merely a viewport onto something much larger.&lt;/p&gt;
&lt;h2 id="menus-and-window-information"&gt;Menus and Window Information
&lt;/h2&gt;&lt;p&gt;Clicking the &lt;strong&gt;right mouse button&lt;/strong&gt; on the root window opens the window operations menu.&lt;/p&gt;
&lt;p&gt;At this point no particular window has been selected.&lt;/p&gt;
&lt;p&gt;First choose the operation.&lt;/p&gt;
&lt;p&gt;Then choose the target window.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Window operations menu" class="gallery-image" data-flex-basis="161px" data-flex-grow="67" height="350" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-5.png" width="236"&gt;&lt;/p&gt;
&lt;p&gt;For example, choose:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Identify&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;and then:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GNU Emacs&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FvwmIdent&lt;/code&gt; appears:&lt;/p&gt;
&lt;p&gt;&lt;img alt="FvwmIdent" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="fwvm-11.png"&gt;&lt;/p&gt;
&lt;p&gt;At first it looks like a collection of obscure X11 properties.&lt;/p&gt;
&lt;p&gt;Later, these values become extremely useful when writing configuration rules.&lt;/p&gt;
&lt;p&gt;That is precisely what &lt;code&gt;FvwmIdent&lt;/code&gt; is intended for: among other things, identifying a window&amp;rsquo;s name, resource, and class.&lt;/p&gt;
&lt;p&gt;Clicking the &lt;strong&gt;middle mouse button&lt;/strong&gt; on the root window displays a window list, including geometry information.&lt;/p&gt;
&lt;p&gt;Its main purpose is to activate another window quickly.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Alt+Tab&lt;/code&gt; produces a simpler window list without geometry.&lt;/p&gt;
&lt;p&gt;The default configuration binds this directly to FVWM&amp;rsquo;s &lt;code&gt;WindowList&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Clicking the &lt;strong&gt;right mouse button on a title bar&lt;/strong&gt; opens the current window&amp;rsquo;s operations menu:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Window menu" class="gallery-image" data-flex-basis="146px" data-flex-grow="61" height="190" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-4.png" width="116"&gt;&lt;/p&gt;
&lt;p&gt;Among the more interesting entries are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stick / Unstick&lt;/strong&gt; — make the window appear across desks, or return it to normal behavior,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shade / Unshade&lt;/strong&gt; — collapse the window until only the title bar remains, then restore it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Shade" class="gallery-image" data-flex-basis="3328px" data-flex-grow="1386" height="69" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-6a.png" srcset="https://lvajxi03.github.io/posts/2026-09-28/fvwm-6a_hu_2dc9349d2e1dbe36.png 800w, https://lvajxi03.github.io/posts/2026-09-28/fvwm-6a.png 957w" width="957"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Stick" class="gallery-image" data-flex-basis="2172px" data-flex-grow="905" height="75" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-28/fvwm-6b.png" width="679"&gt;&lt;/p&gt;
&lt;p&gt;And that is enough for the first encounter.&lt;/p&gt;
&lt;p&gt;Spend a while moving windows around, switching desks, exploring pages, and generally discovering how much space FVWM is prepared to give you.&lt;/p&gt;
&lt;p&gt;Then choose &lt;strong&gt;Quit&lt;/strong&gt; from the main menu.&lt;/p&gt;
&lt;p&gt;In the next episode, we will log in again.&lt;/p&gt;
&lt;p&gt;But this time, into &lt;strong&gt;our&lt;/strong&gt; FVWM.&lt;/p&gt;</description></item><item><title>Cold and Darkness (III)</title><link>https://lvajxi03.github.io/posts/2026-09-20/</link><pubDate>Sun, 20 Sep 2026 17:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-09-20/</guid><description>&lt;p&gt;The third and final episode of the saga about SBC cases and cooling.&lt;/p&gt;
&lt;h1 id="rock-5t"&gt;ROCK 5T
&lt;/h1&gt;&lt;p&gt;The latest SBC to find its way into my home is the &lt;strong&gt;ROCK 5T&lt;/strong&gt; — a serious contender in my Kubernetes cluster.&lt;/p&gt;
&lt;p&gt;It is based on the &lt;strong&gt;Rockchip RK3588&lt;/strong&gt;, an eight-core SoC combining:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;four Cortex-A76 cores,&lt;/li&gt;
&lt;li&gt;four Cortex-A55 cores,&lt;/li&gt;
&lt;li&gt;a Mali-G610 GPU,&lt;/li&gt;
&lt;li&gt;an NPU capable of up to 6 TOPS.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My unit has &lt;strong&gt;16 GB of RAM&lt;/strong&gt; and a &lt;strong&gt;500 GB NVMe SSD&lt;/strong&gt; — the latter being entirely my own indulgence.&lt;/p&gt;
&lt;p&gt;This configuration should remain useful for years: compiling software, collecting metrics, running heavier cluster workloads, and experimenting with relatively small language models.&lt;/p&gt;
&lt;p&gt;The ROCK 5T also provides two HDMI outputs, one HDMI input, DisplayPort over USB-C, dual 2.5 GbE, and two M.2 M-Key slots for NVMe drives.&lt;/p&gt;
&lt;p&gt;There is also a standard 40-pin GPIO header, which suggests plenty of other possible uses.&lt;/p&gt;
&lt;p&gt;Operating system?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Armbian&lt;/strong&gt;, obviously.&lt;/p&gt;
&lt;p&gt;The ROCK 5T currently has &lt;strong&gt;Platinum&lt;/strong&gt; support there, placing it among the better-supported boards in Armbian&amp;rsquo;s catalogue.&lt;/p&gt;
&lt;p&gt;But first&amp;hellip;&lt;/p&gt;
&lt;p&gt;&amp;hellip;the board needs a case, proper cooling, and a little stress testing.&lt;/p&gt;
&lt;h2 id="power"&gt;Power
&lt;/h2&gt;&lt;p&gt;The power supply must be purchased separately.&lt;/p&gt;
&lt;p&gt;And here comes the first surprise: the ROCK 5T does not use ordinary USB-C power.&lt;/p&gt;
&lt;p&gt;It accepts &lt;strong&gt;9–20 V DC&lt;/strong&gt; through a 5.5/2.5 mm barrel connector, with &lt;strong&gt;12 V recommended&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Radxa recommends a supply capable of at least &lt;strong&gt;36 W&lt;/strong&gt;, which means 12 V / 3 A or more.&lt;/p&gt;
&lt;p&gt;The USB-C port handles things such as OTG and DisplayPort, but it does &lt;strong&gt;not&lt;/strong&gt; power the board.&lt;/p&gt;
&lt;p&gt;So all those convenient USB-C power supplies already sitting on the shelf?&lt;/p&gt;
&lt;p&gt;Not this time.&lt;/p&gt;
&lt;p&gt;Apparently the marketing department decided that the purchasing experience would be more complete if several parts of a complete computer had to be bought separately.&lt;/p&gt;
&lt;h2 id="cpu-cooling"&gt;CPU Cooling
&lt;/h2&gt;&lt;p&gt;The CPU fan also has to be purchased separately.&lt;/p&gt;
&lt;p&gt;Fortunately, the official active heatsink fits perfectly and requires no improvisation.&lt;/p&gt;
&lt;p&gt;Radxa lists active cooling among the official accessories for the board.&lt;/p&gt;
&lt;p&gt;So at least this time it is clear what the designer intended.&lt;/p&gt;
&lt;h2 id="the-case"&gt;The Case
&lt;/h2&gt;&lt;p&gt;The case&amp;hellip;&lt;/p&gt;
&lt;p&gt;Yes.&lt;/p&gt;
&lt;p&gt;You guessed correctly.&lt;/p&gt;
&lt;p&gt;That must be purchased separately as well.&lt;/p&gt;
&lt;p&gt;I chose the official Radxa metal enclosure.&lt;/p&gt;
&lt;p&gt;The product description warned that the case might not provide proper access to the &lt;strong&gt;HDMI-IN&lt;/strong&gt; connector.&lt;/p&gt;
&lt;p&gt;Naturally, this sounded disappointing.&lt;/p&gt;
&lt;p&gt;When the package arrived, however, the HDMI-IN opening turned out to be perfectly usable.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;GPIO header&lt;/strong&gt;, on the other hand, was not conveniently accessible.&lt;/p&gt;
&lt;p&gt;So the documentation warns about the problem that is not there and forgets to mention the one that is.&lt;/p&gt;
&lt;p&gt;One has to admire the consistency.&lt;/p&gt;
&lt;h2 id="assembly"&gt;Assembly
&lt;/h2&gt;&lt;p&gt;The package contains two thermal pads roughly the size of an &lt;strong&gt;M.2 2280&lt;/strong&gt; drive.&lt;/p&gt;
&lt;p&gt;That makes sense, because the ROCK 5T supports two 2280 NVMe SSDs.&lt;/p&gt;
&lt;p&gt;The pad goes onto the SSD before the board is placed inside the case.&lt;/p&gt;
&lt;p&gt;Once the case is closed, the pad makes contact with the metal enclosure, effectively turning the case itself into a large heatsink for the drive.&lt;/p&gt;
&lt;p&gt;Simple.&lt;/p&gt;
&lt;p&gt;Effective.&lt;/p&gt;
&lt;p&gt;The case also includes two &lt;strong&gt;external Wi-Fi antennas&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The original antennas can therefore be disconnected and replaced with the external ones before the enclosure is closed.&lt;/p&gt;
&lt;p&gt;In my case, I removed the Wi-Fi antennas entirely.&lt;/p&gt;
&lt;p&gt;This machine has been connected exclusively through Ethernet from the beginning.&lt;/p&gt;
&lt;p&gt;When a board provides two 2.5 GbE interfaces, it becomes surprisingly difficult to justify making a cluster node send packets through the air.&lt;/p&gt;
&lt;h1 id="testing"&gt;Testing
&lt;/h1&gt;&lt;p&gt;Measurements were taken exactly as in the previous two parts.&lt;/p&gt;
&lt;p&gt;Two thermometers.&lt;/p&gt;
&lt;p&gt;For the NVMe drive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo nvme smart-log /dev/nvme0 | grep temperature
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the CPU:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;armbianmonitor -m
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then the two now-familiar stress tests.&lt;/p&gt;
&lt;p&gt;CPU and memory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;stress-ng &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --cpu &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm-bytes 60% &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --timeout 10m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --metrics-brief
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I/O:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fio &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;nvme-test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --filename&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/tmp/fio.test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;4G &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --rw&lt;span style="color:#f92672"&gt;=&lt;/span&gt;readwrite &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --bs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1M &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --iodepth&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --direct&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --runtime&lt;span style="color:#f92672"&gt;=&lt;/span&gt;5m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --time_based &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --group_reporting
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As always, &lt;code&gt;fio&lt;/code&gt; writes data to the selected file.&lt;/p&gt;
&lt;p&gt;Here, that file is &lt;code&gt;/tmp/fio.test&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Do not casually replace it with a block device unless you intend to begin writing an entirely different article immediately afterward.&lt;/p&gt;
&lt;h1 id="results"&gt;Results
&lt;/h1&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Component&lt;/th&gt;
 &lt;th style="text-align: right"&gt;&lt;code&gt;stress-ng&lt;/code&gt;&lt;/th&gt;
 &lt;th style="text-align: right"&gt;&lt;code&gt;fio&lt;/code&gt;&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;NVMe&lt;/td&gt;
 &lt;td style="text-align: right"&gt;39 °C&lt;/td&gt;
 &lt;td style="text-align: right"&gt;40 °C&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CPU&lt;/td&gt;
 &lt;td style="text-align: right"&gt;62.8 °C&lt;/td&gt;
 &lt;td style="text-align: right"&gt;55.5 °C&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Very good.&lt;/p&gt;
&lt;p&gt;The NVMe remains cool, and the CPU stays comfortably below temperatures where thermal throttling would become a concern.&lt;/p&gt;
&lt;p&gt;That means this machine can now be used regularly in the cluster in a way that actually reflects its computing power, memory capacity, and storage.&lt;/p&gt;
&lt;p&gt;In my case, that means heavier CI/CD workloads, metrics, and everything smaller SBCs would accept only with a very audible sigh.&lt;/p&gt;
&lt;h1 id="series-conclusion"&gt;Series Conclusion
&lt;/h1&gt;&lt;p&gt;After three episodes, the conclusion is fairly simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cooling is not an optional accessory.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On modern high-performance SBCs, it is part of the basic system design.&lt;/p&gt;
&lt;p&gt;Without a heatsink, fan, or properly designed enclosure, every board described in this series can reach unpleasant temperatures very quickly under sustained load.&lt;/p&gt;
&lt;p&gt;With appropriate cooling, the picture changes completely.&lt;/p&gt;
&lt;p&gt;Every machine tested here maintained reasonable temperatures during both CPU stress and intensive NVMe activity.&lt;/p&gt;
&lt;p&gt;In several cases, the results were better than I expected.&lt;/p&gt;
&lt;p&gt;So the patients survived.&lt;/p&gt;
&lt;p&gt;They are doing well.&lt;/p&gt;
&lt;p&gt;And they can safely be sent back to work.&lt;/p&gt;
&lt;p&gt;If another single-board resident appears in my fortress, this supposedly final trilogy may receive an entirely unplanned fourth episode.&lt;/p&gt;
&lt;p&gt;Because everyone always says:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;this is definitely my last SBC.&lt;/p&gt;

 &lt;/blockquote&gt;</description></item><item><title>Cold and Darkness (II)</title><link>https://lvajxi03.github.io/posts/2026-09-15/</link><pubDate>Tue, 15 Sep 2026 15:20:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-09-15/</guid><description>&lt;p&gt;In this episode, two more machines go through the usual treatment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Raspberry Pi 500+&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Odroid M2&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They are interesting for very different reasons.&lt;/p&gt;
&lt;p&gt;One is essentially a complete computer hidden inside a keyboard.&lt;/p&gt;
&lt;p&gt;The other is a traditional SBC that still needs a case, cooling, and a little persuasion before it behaves properly under sustained load.&lt;/p&gt;
&lt;h2 id="raspberry-pi-500"&gt;Raspberry Pi 500+
&lt;/h2&gt;&lt;p&gt;The next machine is the &lt;strong&gt;Raspberry Pi 500+&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I bought mine at a very good price, and it turned out to be an excellent purchase.&lt;/p&gt;
&lt;p&gt;It comes with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a mechanical keyboard,&lt;/li&gt;
&lt;li&gt;16 GB of RAM,&lt;/li&gt;
&lt;li&gt;a factory-installed &lt;strong&gt;256 GB NVMe SSD&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The mechanical keyboard obviously has no effect on CPU performance or temperature.&lt;/p&gt;
&lt;p&gt;I simply happen to be a very big fan of mechanical keyboards.&lt;/p&gt;
&lt;p&gt;Perhaps there will be an episode about those one day.&lt;/p&gt;
&lt;p&gt;That is a threat.&lt;/p&gt;
&lt;p&gt;The other two features are considerably more practical.&lt;/p&gt;
&lt;p&gt;A proper SSD is a much better place for an operating system than an SD card, and nobody has ever become ill from having too much RAM.&lt;/p&gt;
&lt;p&gt;Except perhaps after looking at the invoice.&lt;/p&gt;
&lt;p&gt;As mentioned earlier, though, I bought this particular machine at a substantial discount — considerably cheaper than any other bare SBC in my collection with comparable NVMe support.&lt;/p&gt;
&lt;p&gt;The Raspberry Pi 500+ is an all-in-one design, with all the electronics enclosed inside the keyboard case.&lt;/p&gt;
&lt;p&gt;The result looks a little like a modern, simplified &lt;strong&gt;Amiga 500&lt;/strong&gt;, or one of the classic home computers from the 1980s and 1990s.&lt;/p&gt;
&lt;p&gt;That is not accidental.&lt;/p&gt;
&lt;p&gt;Raspberry Pi itself described the 500+ as a tribute to the home computers many of us grew up with.&lt;/p&gt;
&lt;p&gt;What is less obvious without opening the case is how exactly the cooling has been implemented.&lt;/p&gt;
&lt;p&gt;Questions include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how the CPU is cooled,&lt;/li&gt;
&lt;li&gt;where thermal pads are used,&lt;/li&gt;
&lt;li&gt;whether additional heatsinks are present,&lt;/li&gt;
&lt;li&gt;how the NVMe drive is cooled.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The SSD itself is also something you mostly know about from the specification rather than from direct visual inspection.&lt;/p&gt;
&lt;p&gt;Yes, I know.&lt;/p&gt;
&lt;p&gt;I could open the case.&lt;/p&gt;
&lt;p&gt;Unfortunately, one of the screws appears to have made a personal decision to remain exactly where it is.&lt;/p&gt;
&lt;p&gt;And since the machine works perfectly well, for now I am satisfied with knowing that it works perfectly well.&lt;/p&gt;
&lt;p&gt;That is, after all, what I bought it for.&lt;/p&gt;
&lt;h3 id="testing"&gt;Testing
&lt;/h3&gt;&lt;p&gt;The same tests used earlier for the &lt;strong&gt;Orange Pi 5 Plus&lt;/strong&gt; and the &lt;strong&gt;Raspberry Pi 5&lt;/strong&gt; in the DeskPi case work perfectly well here.&lt;/p&gt;
&lt;p&gt;For the NVMe temperature:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo nvme smart-log /dev/nvme0 | grep temperature
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the Raspberry Pi itself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vcgencmd measure_temp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vcgencmd measure_clock arm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vcgencmd get_throttled
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first command reports CPU temperature.&lt;/p&gt;
&lt;p&gt;The second shows the current ARM clock frequency.&lt;/p&gt;
&lt;p&gt;The third is especially useful because it reveals whether the system has experienced thermal throttling, undervoltage, or related issues.&lt;/p&gt;
&lt;p&gt;The CPU and memory stress test:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;stress-ng &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --cpu &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm-bytes 60% &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --timeout 10m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --metrics-brief
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the I/O test:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fio &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;nvme-test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --filename&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/tmp/fio.test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;4G &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --rw&lt;span style="color:#f92672"&gt;=&lt;/span&gt;readwrite &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --bs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1M &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --iodepth&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --direct&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --runtime&lt;span style="color:#f92672"&gt;=&lt;/span&gt;5m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --time_based &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --group_reporting
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As before, &lt;code&gt;fio&lt;/code&gt; writes data to the selected file.&lt;/p&gt;
&lt;p&gt;In this example, that file is &lt;code&gt;/tmp/fio.test&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Do not spontaneously replace it with a block device unless ruining the rest of your day is part of the benchmark.&lt;/p&gt;
&lt;h3 id="results"&gt;Results
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Component&lt;/th&gt;
 &lt;th style="text-align: right"&gt;&lt;code&gt;stress-ng&lt;/code&gt;&lt;/th&gt;
 &lt;th style="text-align: right"&gt;&lt;code&gt;fio&lt;/code&gt;&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;NVMe&lt;/td&gt;
 &lt;td style="text-align: right"&gt;35 °C&lt;/td&gt;
 &lt;td style="text-align: right"&gt;31 °C&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CPU&lt;/td&gt;
 &lt;td style="text-align: right"&gt;47.7 °C&lt;/td&gt;
 &lt;td style="text-align: right"&gt;37.8 °C&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Compared with the other SBCs tested so far, these results are excellent.&lt;/p&gt;
&lt;p&gt;Especially considering that the whole computer is enclosed inside a keyboard.&lt;/p&gt;
&lt;p&gt;It looks very much as though the engineers behind the &lt;strong&gt;Raspberry Pi 500+&lt;/strong&gt; did an exceptionally good job.&lt;/p&gt;
&lt;p&gt;One might even say they did their homework.&lt;/p&gt;
&lt;p&gt;Although in Poland in 2026, that metaphor now requires a little caution.&lt;/p&gt;
&lt;h2 id="odroid-m2"&gt;Odroid M2
&lt;/h2&gt;&lt;p&gt;The &lt;strong&gt;Odroid M2&lt;/strong&gt; arrived with its standard case and cooling fan.&lt;/p&gt;
&lt;p&gt;The enclosure is made of plastic, which initially made me wonder whether it would cope well with sustained heat, especially from the NVMe drive.&lt;/p&gt;
&lt;p&gt;As it turns out, the design is quite sensible.&lt;/p&gt;
&lt;p&gt;The top of the case contains ventilation openings directly around the fan area.&lt;/p&gt;
&lt;p&gt;The underside has openings near the NVMe drive.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Top" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="288" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-15/odroid-1.jpg" width="512"&gt;
&lt;img alt="Bottom" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="288" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-15/odroid-2.jpg" width="512"&gt;&lt;/p&gt;
&lt;p&gt;This strongly suggests that the intended cooling strategy is airflow rather than pressing the SSD against the enclosure through a thermal pad.&lt;/p&gt;
&lt;p&gt;So I left it exactly as designed.&lt;/p&gt;
&lt;p&gt;Assembly took perhaps three minutes.&lt;/p&gt;
&lt;p&gt;Which is always suspicious.&lt;/p&gt;
&lt;p&gt;Usually, if assembling an SBC takes only three minutes, it means you forgot something.&lt;/p&gt;
&lt;p&gt;Not this time.&lt;/p&gt;
&lt;p&gt;Hardkernel itself notes that the Odroid M2 can generate enough heat under sustained CPU and GPU load to require active cooling, which is why a fan is included in the kit. Its speed can be controlled using PWM.&lt;/p&gt;
&lt;h3 id="testing-1"&gt;Testing
&lt;/h3&gt;&lt;p&gt;The tests were identical to the previous ones.&lt;/p&gt;
&lt;p&gt;For NVMe temperature:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo nvme smart-log /dev/nvme0 | grep temperature
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For SoC temperature under Armbian:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;armbianmonitor -m
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;stress-ng &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --cpu &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm-bytes 60% &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --timeout 10m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --metrics-brief
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fio &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;nvme-test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --filename&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/tmp/fio.test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;4G &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --rw&lt;span style="color:#f92672"&gt;=&lt;/span&gt;readwrite &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --bs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1M &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --iodepth&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --direct&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --runtime&lt;span style="color:#f92672"&gt;=&lt;/span&gt;5m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --time_based &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --group_reporting
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="results-1"&gt;Results
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Component&lt;/th&gt;
 &lt;th style="text-align: right"&gt;&lt;code&gt;stress-ng&lt;/code&gt;&lt;/th&gt;
 &lt;th style="text-align: right"&gt;&lt;code&gt;fio&lt;/code&gt;&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;NVMe&lt;/td&gt;
 &lt;td style="text-align: right"&gt;46 °C&lt;/td&gt;
 &lt;td style="text-align: right"&gt;50 °C&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CPU&lt;/td&gt;
 &lt;td style="text-align: right"&gt;61 °C&lt;/td&gt;
 &lt;td style="text-align: right"&gt;59.2 °C&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Before running the tests, I was slightly concerned that every SBC enclosed in a metal case would outperform the plastic Odroid by a wide margin.&lt;/p&gt;
&lt;p&gt;That did not happen.&lt;/p&gt;
&lt;p&gt;The CPU remained comfortably below troublesome temperatures, and the NVMe temperature was perfectly reasonable as well.&lt;/p&gt;
&lt;p&gt;So plastic does not always mean:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;this is going to get hot.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Sometimes it simply means:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;someone designed the airflow properly.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="to-be-continued"&gt;To Be Continued
&lt;/h2&gt;&lt;p&gt;Next time:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rock 5T&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;More metal.&lt;/p&gt;
&lt;p&gt;More NVMe.&lt;/p&gt;
&lt;p&gt;And more reasons to run &lt;code&gt;stress-ng&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Cold and Darkness</title><link>https://lvajxi03.github.io/posts/2026-09-08/</link><pubDate>Tue, 08 Sep 2026 11:10:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-09-08/</guid><description>&lt;p&gt;After several weeks of experimenting with various SBCs, the time finally came to provide them with two basic necessities: a proper case and proper cooling.&lt;/p&gt;
&lt;p&gt;Neither is always as straightforward as it sounds, so I decided to document the process.&lt;/p&gt;
&lt;h1 id="raspberry-pi-5-and-deskpi"&gt;Raspberry Pi 5 and DeskPi
&lt;/h1&gt;&lt;h2 id="assembly"&gt;Assembly
&lt;/h2&gt;&lt;p&gt;For the Raspberry Pi 5, one can buy a &lt;strong&gt;DeskPi&lt;/strong&gt; kit.&lt;/p&gt;
&lt;p&gt;Which is exactly what I did.&lt;/p&gt;
&lt;p&gt;The word &lt;em&gt;kit&lt;/em&gt; is particularly appropriate here, because the box contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the case itself,&lt;/li&gt;
&lt;li&gt;a fan,&lt;/li&gt;
&lt;li&gt;thermal pads,&lt;/li&gt;
&lt;li&gt;a multifunction adapter board.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last item is by far the most interesting part.&lt;/p&gt;
&lt;p&gt;It acts as an M.2 NVMe adapter, converts the Raspberry Pi&amp;rsquo;s micro-HDMI connectors to full-size HDMI, reroutes USB-C power, and adds a proper physical power button.&lt;/p&gt;
&lt;p&gt;In other words, it fixes several things one might reasonably wish had been there from the beginning.&lt;/p&gt;
&lt;p&gt;DeskPi&amp;rsquo;s own product description confirms support for M.2 NVMe drives, full-size HDMI through an adapter board, rerouted USB-C, and a power switch.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Assembly is not particularly difficult.&lt;/p&gt;

 &lt;/blockquote&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Except for the ribbon cable.&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;The first step is to throw the manual into the trash.&lt;/p&gt;
&lt;p&gt;Unless you own a paper shredder.&lt;/p&gt;
&lt;p&gt;In that case, use the shredder.&lt;/p&gt;
&lt;p&gt;I envy you.&lt;/p&gt;
&lt;p&gt;Trust me: the manual will not help much.&lt;/p&gt;
&lt;p&gt;I had already been using an NVMe drive with my Raspberry Pi 5, so I first had to dismantle the existing setup and remove the &lt;strong&gt;Pimoroni NVMe Base&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A very good product, incidentally.&lt;/p&gt;
&lt;p&gt;Just no longer useful in this particular configuration.&lt;/p&gt;
&lt;p&gt;The USB-C and HDMI extension pieces fit without much trouble, and most of the assembly is pleasantly uneventful.&lt;/p&gt;
&lt;p&gt;If you do not already have a fan installed, this is a good moment to add one.&lt;/p&gt;
&lt;p&gt;Remember the thermal pads.&lt;/p&gt;
&lt;p&gt;My Raspberry Pi 5 already had the official active cooler installed, and I decided to keep it because it works perfectly well.&lt;/p&gt;
&lt;p&gt;The one thing that deserves disproportionate attention is the PCIe ribbon cable.&lt;/p&gt;
&lt;p&gt;Yes, it can physically be inserted in more than one orientation.&lt;/p&gt;
&lt;p&gt;No, only one of them is useful.&lt;/p&gt;
&lt;p&gt;The final result should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="ribbon" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="576" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-08/ribbon.jpg" srcset="https://lvajxi03.github.io/posts/2026-09-08/ribbon_hu_4d1fde607d91a351.jpg 800w, https://lvajxi03.github.io/posts/2026-09-08/ribbon.jpg 1024w" width="1024"&gt;&lt;/p&gt;
&lt;p&gt;If it does not, you may spend several enjoyable hours wondering why the Raspberry Pi can no longer see the NVMe drive.&lt;/p&gt;
&lt;p&gt;I did.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;No, the manual does not clearly explain the ribbon orientation.&lt;/p&gt;

 &lt;/blockquote&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Do not retrieve it from the trash.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;If the Raspberry Pi already booted from NVMe before moving into the case, there is a good chance it will continue doing so without any additional changes.&lt;/p&gt;
&lt;p&gt;That was exactly what happened in my case.&lt;/p&gt;
&lt;p&gt;No new &lt;code&gt;config.txt&lt;/code&gt; entries.&lt;/p&gt;
&lt;p&gt;No bootloader changes.&lt;/p&gt;
&lt;p&gt;Nothing.&lt;/p&gt;
&lt;p&gt;Null.&lt;/p&gt;
&lt;p&gt;Nada.&lt;/p&gt;
&lt;p&gt;However, my bootloader had already been configured for NVMe booting beforehand, so a fresh setup may require additional preparation.&lt;/p&gt;
&lt;h2 id="testing"&gt;Testing
&lt;/h2&gt;&lt;p&gt;Once everything works and the case is properly assembled, it seems reasonable to perform a few basic stress tests.&lt;/p&gt;
&lt;p&gt;I used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stress-ng&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fio&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nvme-cli&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;First:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install nvme-cli fio stress-ng
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The NVMe drive temperature can be read with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo nvme smart-log /dev/nvme0 | grep temperature
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The CPU temperature on Raspberry Pi can be checked with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vcgencmd measure_temp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first test applies ten minutes of CPU and memory pressure:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;stress-ng &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --cpu &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm-bytes 60% &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --timeout 10m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --metrics-brief
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The second one produces sustained I/O activity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fio &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;nvme-test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --filename&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/tmp/fio.test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;4G &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --rw&lt;span style="color:#f92672"&gt;=&lt;/span&gt;readwrite &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --bs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1M &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --iodepth&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --direct&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --runtime&lt;span style="color:#f92672"&gt;=&lt;/span&gt;5m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --time_based &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --group_reporting
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A small warning here:&lt;/p&gt;
&lt;p&gt;the &lt;code&gt;fio&lt;/code&gt; test writes data to the selected file.&lt;/p&gt;
&lt;p&gt;In this example that file is &lt;code&gt;/tmp/fio.test&lt;/code&gt;, which is intentional.&lt;/p&gt;
&lt;p&gt;Do not casually replace it with a block device unless destroying data is part of the experiment.&lt;/p&gt;
&lt;h2 id="results"&gt;Results
&lt;/h2&gt;&lt;p&gt;The NVMe drive peaked at around &lt;strong&gt;50°C&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The CPU reached &lt;strong&gt;73.6°C&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For a Raspberry Pi 5 under sustained load inside a compact case, I consider that perfectly acceptable.&lt;/p&gt;
&lt;h1 id="orange-pi-5-plus-and-the-geekpi-case"&gt;Orange Pi 5 Plus and the GeekPi Case
&lt;/h1&gt;&lt;h2 id="assembly-1"&gt;Assembly
&lt;/h2&gt;&lt;p&gt;The GeekPi case is a black aluminum enclosure.&lt;/p&gt;
&lt;p&gt;It is appropriately ugly.&lt;/p&gt;
&lt;p&gt;It is also extremely well suited to the Orange Pi 5 Plus, which is much more important.&lt;/p&gt;
&lt;p&gt;The case accommodates an &lt;strong&gt;M.2 2280&lt;/strong&gt; drive and has a top-mounted fan that moves air across a larger portion of the board rather than cooling only the SoC.&lt;/p&gt;
&lt;p&gt;The kit also includes thermal pads and small heatsinks.&lt;/p&gt;
&lt;p&gt;Unlike the DeskPi manual, the product page does a reasonably good job of showing where everything belongs.&lt;/p&gt;
&lt;p&gt;Progress.&lt;/p&gt;
&lt;p&gt;There is, however, one complication:&lt;/p&gt;
&lt;p&gt;the fan.&lt;/p&gt;
&lt;p&gt;The Orange Pi 5 Plus provides a dedicated two-pin &lt;strong&gt;FAN&lt;/strong&gt; connector using a small JST-style plug, while the fan included with the case terminates in two separate DuPont connectors intended for GPIO pins.&lt;/p&gt;
&lt;p&gt;If one simply wants the fan to run continuously, it can be connected to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+5V&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GND&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This works.&lt;/p&gt;
&lt;p&gt;The disadvantages are equally obvious:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the fan runs all the time,&lt;/li&gt;
&lt;li&gt;there is no temperature control,&lt;/li&gt;
&lt;li&gt;two GPIO header pins are occupied,&lt;/li&gt;
&lt;li&gt;even a quiet fan becomes surprisingly noticeable after several hours.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wanted the board itself to control the fan.&lt;/p&gt;
&lt;h2 id="modifying-the-fan-cable"&gt;Modifying the Fan Cable
&lt;/h2&gt;&lt;p&gt;The dedicated Orange Pi fan connector is intended to cooperate with software-controlled fan handling.&lt;/p&gt;
&lt;p&gt;On current Linux setups this is typically exposed through the kernel&amp;rsquo;s &lt;code&gt;pwm-fan&lt;/code&gt; mechanism and device-tree configuration. Orange Pi documentation and Armbian discussions show temperature-dependent fan levels rather than a simple permanently powered connection.&lt;/p&gt;
&lt;p&gt;That sounded much better.&lt;/p&gt;
&lt;p&gt;I bought a handful of inexpensive 1.25 mm JST pigtails and prepared to modify the cable.&lt;/p&gt;
&lt;p&gt;&lt;img alt="fan" class="gallery-image" data-flex-basis="409px" data-flex-grow="170" height="271" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-09-08/fan.jpg" width="462"&gt;&lt;/p&gt;
&lt;p&gt;Before soldering anything, I checked the connector electrically with a multimeter.&lt;/p&gt;
&lt;p&gt;This is highly recommended.&lt;/p&gt;
&lt;p&gt;Do not trust wire colors alone.&lt;/p&gt;
&lt;p&gt;Do not trust random diagrams found in image search.&lt;/p&gt;
&lt;p&gt;Do not trust yourself after midnight.&lt;/p&gt;
&lt;p&gt;I connected a JST pigtail to the FAN header, powered on the Orange Pi, and observed the behavior while increasing CPU temperature with &lt;code&gt;stress-ng&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The test confirmed that the dedicated connector was being controlled according to temperature.&lt;/p&gt;
&lt;p&gt;With that established, the rest was simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cut off the DuPont connectors,&lt;/li&gt;
&lt;li&gt;strip roughly one centimeter of insulation from the fan wires,&lt;/li&gt;
&lt;li&gt;strip the JST pigtail wires,&lt;/li&gt;
&lt;li&gt;slide heat-shrink tubing onto the wires &lt;strong&gt;before soldering&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;solder the matching wires,&lt;/li&gt;
&lt;li&gt;move the heat-shrink tubing over the joints,&lt;/li&gt;
&lt;li&gt;gently heat it until it contracts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The third item deserves special emphasis.&lt;/p&gt;
&lt;p&gt;Most people remember heat-shrink tubing immediately after finishing the solder joint.&lt;/p&gt;
&lt;p&gt;This is one of the traditional stages of electronics education.&lt;/p&gt;
&lt;h2 id="testing-1"&gt;Testing
&lt;/h2&gt;&lt;p&gt;After carefully assembling the case again, it was time for another round of tests.&lt;/p&gt;
&lt;p&gt;First, the tools:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install nvme-cli fio stress-ng
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The NVMe temperature:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo nvme smart-log /dev/nvme0 | grep temperature
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the Orange Pi under Armbian, system temperature can conveniently be monitored with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;armbianmonitor -m
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The output refreshes every few seconds and includes the SoC temperature.&lt;/p&gt;
&lt;p&gt;The same stress tests can then be repeated:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;stress-ng &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --cpu &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vm-bytes 60% &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --timeout 10m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --metrics-brief
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fio &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;nvme-test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --filename&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/tmp/fio.test &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;4G &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --rw&lt;span style="color:#f92672"&gt;=&lt;/span&gt;readwrite &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --bs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1M &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --iodepth&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --direct&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --runtime&lt;span style="color:#f92672"&gt;=&lt;/span&gt;5m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --time_based &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --group_reporting
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="results-1"&gt;Results
&lt;/h2&gt;&lt;p&gt;The NVMe drive peaked at &lt;strong&gt;44°C&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The CPU briefly reached &lt;strong&gt;68.4°C&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;At that point, the fan came alive.&lt;/p&gt;
&lt;p&gt;Afterward, the temperature rarely exceeded roughly &lt;strong&gt;65°C&lt;/strong&gt; under the same workload.&lt;/p&gt;
&lt;p&gt;That is exactly the kind of behavior I wanted: silent when cooling is unnecessary, audible only when the board actually needs help.&lt;/p&gt;
&lt;p&gt;And, most importantly, controlled by the SBC rather than permanently wired to five volts like a tiny desk fan from 1997.&lt;/p&gt;
&lt;h1 id="to-be-continued"&gt;To Be Continued
&lt;/h1&gt;&lt;p&gt;Two boards down.&lt;/p&gt;
&lt;p&gt;Several still waiting.&lt;/p&gt;
&lt;p&gt;In the next part:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Odroid M2&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raspberry Pi 500+&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of them has a perfectly reasonable case.&lt;/p&gt;
&lt;p&gt;The other has opinions.&lt;/p&gt;</description></item><item><title>Cutting, Bending, Soldering</title><link>https://lvajxi03.github.io/posts/2026-08-25/</link><pubDate>Tue, 25 Aug 2026 23:00:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-08-25/</guid><description>&lt;h1 id="introduction"&gt;Introduction
&lt;/h1&gt;&lt;p&gt;Three years ago, I bought a computer with a preinstalled operating system commonly known as Windows.&lt;/p&gt;
&lt;p&gt;You may ask: why?&lt;/p&gt;
&lt;p&gt;Surely it would have been possible to find a machine with an empty drive and save myself the trouble.&lt;/p&gt;
&lt;p&gt;Well, I happened to find a computer with what was then the latest Core i5, 64 GB of RAM, a 1 TB NVMe drive, and an NVIDIA RTX 3050 at a price low enough that the correct order of operations was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;buy the computer,&lt;/li&gt;
&lt;li&gt;ask questions later.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And Windows?&lt;/p&gt;
&lt;p&gt;It came with the machine.&lt;/p&gt;
&lt;p&gt;Might as well keep it. You never know when it might become useful.&lt;/p&gt;
&lt;p&gt;After several months, however, &lt;em&gt;working&lt;/em&gt; with this otherwise magnificent operating system started to resemble an experiment designed to determine the limits of human patience.&lt;/p&gt;
&lt;p&gt;I wanted it gone.&lt;/p&gt;
&lt;p&gt;Or, at the very least, I wanted its influence over my life significantly reduced.&lt;/p&gt;
&lt;p&gt;Using Windows itself, I managed to shrink its partition and install &lt;strong&gt;Ubuntu&lt;/strong&gt;, which had long been my default Linux distribution both on PCs and on many ARM64 machines.&lt;/p&gt;
&lt;p&gt;Ubuntu received 244 GiB.&lt;/p&gt;
&lt;p&gt;In theory, that should be plenty. With a little maintenance, that amount of space can last for a very long time.&lt;/p&gt;
&lt;p&gt;The problem was that the rest of the drive was being wasted spectacularly.&lt;/p&gt;
&lt;p&gt;Windows later allowed me to reclaim another 115 GiB or so, but no more.&lt;/p&gt;
&lt;p&gt;Where was the administrator&amp;rsquo;s control over his own computer?&lt;/p&gt;
&lt;p&gt;Where was the sacred right to partition a disk according to one&amp;rsquo;s own unreasonable plans?&lt;/p&gt;
&lt;p&gt;This led to a bold decision:&lt;/p&gt;
&lt;p&gt;delete the main NTFS partition, reinstall the &lt;em&gt;hostile operating system&lt;/em&gt; into the smallest sensible amount of space, and use everything else to live dangerously.&lt;/p&gt;
&lt;p&gt;For Fedora, which I had never had time for.&lt;/p&gt;
&lt;p&gt;For FreeBSD, which I had never had room for.&lt;/p&gt;
&lt;p&gt;For Arch Linux, so that I could finally approach random strangers in the street and say:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;I use Arch, btw.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;For Slackware, so that I could watch its installer produce a &lt;code&gt;segmentation fault&lt;/code&gt; and reassure myself that reports of the distribution&amp;rsquo;s death had indeed been greatly exaggerated.&lt;/p&gt;
&lt;p&gt;And so on.&lt;/p&gt;
&lt;p&gt;That is exactly what I did.&lt;/p&gt;
&lt;h1 id="backup"&gt;Backup
&lt;/h1&gt;&lt;p&gt;First things first: backups.&lt;/p&gt;
&lt;p&gt;Important files from NTFS first, then the &lt;code&gt;ext4&lt;/code&gt; partition as well.&lt;/p&gt;
&lt;p&gt;Because you never know.&lt;/p&gt;
&lt;p&gt;A question for the brave:&lt;/p&gt;
&lt;p&gt;how often do you &lt;strong&gt;not&lt;/strong&gt; make backups?&lt;/p&gt;
&lt;p&gt;And what happens if the power disappears halfway through repartitioning the drive?&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m seriously asking.&lt;/p&gt;
&lt;p&gt;For a friend.&lt;/p&gt;
&lt;p&gt;Who also does not make backups.&lt;/p&gt;
&lt;p&gt;The obvious tool is &lt;code&gt;rsync&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -av --info&lt;span style="color:#f92672"&gt;=&lt;/span&gt;progress2 /source-directory/ /destination-directory/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The destination will probably be an external NTFS drive already containing other important backups.&lt;/p&gt;
&lt;p&gt;Converting it to &lt;code&gt;ext4&lt;/code&gt; right now would merely create another project that nobody asked for.&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;rsync&lt;/code&gt; as many times as necessary.&lt;/p&gt;
&lt;p&gt;Including the Windows user directory.&lt;/p&gt;
&lt;p&gt;You never know when something from there might turn out to be useful.&lt;/p&gt;
&lt;p&gt;At worst, six months later you discover several dozen gigabytes of Go, Firefox, &lt;code&gt;pip&lt;/code&gt;, and other caches proving that civilization once existed there.&lt;/p&gt;
&lt;h1 id="a-quick-trip-through-windows"&gt;A Quick Trip Through Windows
&lt;/h1&gt;&lt;p&gt;The world has moved on since Windows XP.&lt;/p&gt;
&lt;p&gt;You no longer need to keep an installation DVD hidden in a drawer somewhere.&lt;/p&gt;
&lt;p&gt;Windows installation media can be downloaded from the Internet, and on machines originally sold with Windows, activation will normally happen automatically using the OEM key stored in UEFI firmware.&lt;/p&gt;
&lt;p&gt;For peace of mind, however, you can still retrieve the key before doing anything destructive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(Get-CimInstance -ClassName SoftwareLicensingService).OA3xOriginalProductKey
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On Linux, the key stored in the ACPI MSDM table can often be displayed with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo strings /sys/firmware/acpi/tables/MSDM
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On my machine, that produced the correct activation key as well.&lt;/p&gt;
&lt;p&gt;To make reinstalling Windows slightly less painful, I prepared the USB stick using &lt;strong&gt;Rufus&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Rufus detects Windows installation images and offers several additional installation options.&lt;/p&gt;
&lt;p&gt;After pressing &lt;code&gt;Start&lt;/code&gt;, I was presented with the &lt;strong&gt;Windows User Experience&lt;/strong&gt; dialog.&lt;/p&gt;
&lt;p&gt;I selected, among other things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no requirement for a Microsoft account,&lt;/li&gt;
&lt;li&gt;creation of a local user with a name of my choosing,&lt;/li&gt;
&lt;li&gt;fewer telemetry and privacy questions,&lt;/li&gt;
&lt;li&gt;disabling automatic BitLocker device encryption.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With the installer prepared, the time finally came to delete some partitions.&lt;/p&gt;
&lt;p&gt;Using a proper operating system, naturally.&lt;/p&gt;
&lt;h1 id="whats-on-the-plate"&gt;What&amp;rsquo;s on the Plate?
&lt;/h1&gt;&lt;p&gt;&lt;code&gt;gparted&lt;/code&gt; revealed the following landscape:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Partition&lt;/th&gt;
 &lt;th style="text-align: right"&gt;Size&lt;/th&gt;
 &lt;th&gt;Purpose&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/dev/nvme0n1p1&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;300 MiB&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;EFI System Partition&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/dev/nvme0n1p2&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;128 MiB&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Microsoft Reserved Partition (MSR)&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/dev/nvme0n1p3&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;546.37 GiB&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Windows C:&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;unallocated&lt;/td&gt;
 &lt;td style="text-align: right"&gt;&lt;strong&gt;115.86 GiB&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;free space&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/dev/nvme0n1p4&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;1.12 GiB&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Windows Recovery / WinRE&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/dev/nvme0n1p5&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;244.14 GiB&lt;/td&gt;
 &lt;td&gt;existing Ubuntu&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;unallocated&lt;/td&gt;
 &lt;td style="text-align: right"&gt;1.05 GiB&lt;/td&gt;
 &lt;td&gt;a small gap&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/dev/nvme0n1p6&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;21.07 GiB&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Dell factory recovery image&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;/dev/nvme0n1p7&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;1.49 GiB&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Dell support / diagnostics&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;unallocated&lt;/td&gt;
 &lt;td style="text-align: right"&gt;1.71 MiB&lt;/td&gt;
 &lt;td&gt;alignment&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The plan:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Delete &lt;code&gt;nvme0n1p3&lt;/code&gt; and &lt;code&gt;nvme0n1p4&lt;/code&gt;. Windows Setup can recreate what it needs. Leave &lt;code&gt;p1&lt;/code&gt;, &lt;code&gt;p2&lt;/code&gt;, &lt;code&gt;p5&lt;/code&gt;, &lt;code&gt;p6&lt;/code&gt;, and &lt;code&gt;p7&lt;/code&gt; alone.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Snip.&lt;/p&gt;
&lt;p&gt;Gone.&lt;/p&gt;
&lt;h1 id="installing-windows"&gt;Installing Windows
&lt;/h1&gt;&lt;p&gt;On Dell laptops, pressing &lt;strong&gt;F12&lt;/strong&gt; during boot opens the temporary boot menu, from which the Windows installer USB stick can be selected.&lt;/p&gt;
&lt;p&gt;The installer started correctly.&lt;/p&gt;
&lt;p&gt;There was just one tiny problem:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;The only visible drive was the USB stick itself.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;The solution was simple.&lt;/p&gt;
&lt;p&gt;Back into the firmware setup using &lt;strong&gt;F2&lt;/strong&gt;, then change the storage-controller mode from &lt;strong&gt;RAID&lt;/strong&gt; to &lt;strong&gt;AHCI&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;After rebooting, the NVMe drive appeared in Windows Setup.&lt;/p&gt;
&lt;p&gt;This is where a little attention becomes important:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Do not simply keep clicking &lt;em&gt;Next&lt;/em&gt;.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Windows is perfectly happy to consume all available space for itself.&lt;/p&gt;
&lt;p&gt;I manually created a partition of roughly 150 GiB and installed Windows there.&lt;/p&gt;
&lt;p&gt;Partition created.&lt;/p&gt;
&lt;p&gt;Windows installed.&lt;/p&gt;
&lt;h1 id="initial-windows-configuration"&gt;Initial Windows Configuration
&lt;/h1&gt;&lt;p&gt;The objective here was simple: prevent Windows from quietly reserving unreasonable amounts of disk space for things I did not particularly care about.&lt;/p&gt;
&lt;p&gt;First, disable hibernation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;powercfg /h off
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This also removes &lt;code&gt;hiberfil.sys&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sysdm.cpl
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advanced → Performance → Settings → Advanced → Virtual Memory → Change&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Automatic pagefile management can be disabled and a fixed limit selected.&lt;/p&gt;
&lt;p&gt;In my case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Initial size: &lt;code&gt;1024 MB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Maximum size: &lt;code&gt;4096 MB&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not universal advice.&lt;/p&gt;
&lt;p&gt;My machine has 64 GB of RAM. On systems with significantly less memory, leaving pagefile management to Windows may be the better option.&lt;/p&gt;
&lt;p&gt;Next:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advanced → Startup and Recovery → Settings → Write debugging information → None&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If full crash dumps are not useful to you, there is little reason to reserve many gigabytes for them.&lt;/p&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;SystemPropertiesProtection
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For &lt;code&gt;C:&lt;/code&gt;, System Protection can either be disabled or given a small storage limit.&lt;/p&gt;
&lt;p&gt;I kept it enabled but limited.&lt;/p&gt;
&lt;p&gt;After major updates, the component store can also be cleaned:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DISM /Online /Cleanup-Image /AnalyzeComponentStore
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DISM /Online /Cleanup-Image /StartComponentCleanup
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And finally:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Settings → System → Storage → Temporary Files&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Phew.&lt;/p&gt;
&lt;p&gt;Windows had now been successfully confined to its 150-gigabyte room.&lt;/p&gt;
&lt;p&gt;It was time to return to the actual experiment.&lt;/p&gt;
&lt;h1 id="the-second-operating-system"&gt;The Second Operating System
&lt;/h1&gt;&lt;p&gt;For the second operating system — from the normal family — I chose &lt;strong&gt;Fedora KDE&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With every intention of eventually making it my main system.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;fvwm3&lt;/code&gt; configuration can wait for long autumn evenings, when ordinary life stops providing enough problems.&lt;/p&gt;
&lt;h2 id="installing-fedora"&gt;Installing Fedora
&lt;/h2&gt;&lt;p&gt;Fedora boots into a Live environment, which has several advantages.&lt;/p&gt;
&lt;p&gt;First, a complete system and disk tools are available before installation.&lt;/p&gt;
&lt;p&gt;Second, you can open a browser, play a movie, and enjoy a genuine &lt;em&gt;sit back and relax&lt;/em&gt; experience.&lt;/p&gt;
&lt;p&gt;Another company advertised that concept back in 1995.&lt;/p&gt;
&lt;p&gt;It did not go particularly well.&lt;/p&gt;
&lt;p&gt;Fedora&amp;rsquo;s installer was also quite enthusiastic about using all available disk space, so I politely declined and prepared the partitions myself.&lt;/p&gt;
&lt;p&gt;I allocated 2 GiB of &lt;code&gt;ext4&lt;/code&gt; for &lt;code&gt;/boot&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The root filesystem received roughly 215 GiB of &lt;code&gt;btrfs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It is worth keeping Fedora&amp;rsquo;s usual Btrfs subvolume layout:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;root&lt;/code&gt; → &lt;code&gt;/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;home&lt;/code&gt; → &lt;code&gt;/home&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is no need for a separate &lt;code&gt;/home&lt;/code&gt; partition.&lt;/p&gt;
&lt;p&gt;Both subvolumes share the same Btrfs storage pool, so free space is not artificially divided between the operating system and user files.&lt;/p&gt;
&lt;p&gt;The installer then needed three mount points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/boot&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/boot/efi&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last one comes with an important rule:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;do &lt;strong&gt;not&lt;/strong&gt; format the existing EFI System Partition.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Unless crying and gnashing of teeth happens to be part of the plan.&lt;/p&gt;
&lt;p&gt;At this point, tea can be prepared while the installer does its work.&lt;/p&gt;
&lt;h2 id="unsupportedpartitioningerror"&gt;&lt;code&gt;UnsupportedPartitioningError&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;There was, naturally, one surprise.&lt;/p&gt;
&lt;p&gt;The installer refused to use the Btrfs filesystem I had prepared for &lt;code&gt;/&lt;/code&gt; and stopped with &lt;code&gt;UnsupportedPartitioningError&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In my particular case, manually creating the expected subvolumes solved the problem:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mount /dev/nvme0n1p9 /mnt/btrfs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo btrfs subvolume create /mnt/btrfs/root
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo btrfs subvolume create /mnt/btrfs/home
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo btrfs subvolume list /mnt/btrfs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In other words:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mount the filesystem,&lt;/li&gt;
&lt;li&gt;create &lt;code&gt;root&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;create &lt;code&gt;home&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;verify that they actually exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The next installation attempt completed successfully.&lt;/p&gt;
&lt;p&gt;Fedora installed its own GRUB configuration afterward.&lt;/p&gt;
&lt;p&gt;Fortunately, it also discovered the other operating systems and added them to the boot menu.&lt;/p&gt;
&lt;h2 id="first-boot"&gt;First Boot
&lt;/h2&gt;&lt;p&gt;After the first successful boot, I created the normal administrative user.&lt;/p&gt;
&lt;p&gt;Its UID and GID were the usual:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;1000:1000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Exactly the same as my user account under Ubuntu.&lt;/p&gt;
&lt;p&gt;A small detail, but a very convenient one when files are shared between installations.&lt;/p&gt;
&lt;p&gt;The root password remained unset.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Use sudo, Luke.&lt;/em&gt;&lt;/p&gt;
&lt;h1 id="mounting-the-ntfs-partition"&gt;Mounting the NTFS Partition
&lt;/h1&gt;&lt;p&gt;Once the Windows partition was known, it could be added to &lt;code&gt;/etc/fstab&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My first version used the kernel &lt;code&gt;ntfs3&lt;/code&gt; driver:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-fstab" data-lang="fstab"&gt;UUID=NTFS-PART-UUID /mnt/c ntfs3 rw,nosuid,nodev,nofail,uid=1000,gid=1000,umask=022,windows_names 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Later, however, I found that &lt;code&gt;ntfs-3g&lt;/code&gt; behaved much better in my particular setup, so that is what I ultimately kept.&lt;/p&gt;
&lt;h1 id="fonts"&gt;Fonts
&lt;/h1&gt;&lt;p&gt;With the Windows partition mounted, its fonts can also be reused.&lt;/p&gt;
&lt;p&gt;After all, the files are already sitting on a machine whose Windows license came with the hardware.&lt;/p&gt;
&lt;p&gt;For manually installed system-wide fonts, a suitable location is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mkdir -p /usr/local/share/fonts/microsoft
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cp /mnt/c/Windows/Fonts/*.ttf /usr/local/share/fonts/microsoft/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo fc-cache -fv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And this is where another adventure began.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fc-cache&lt;/code&gt; reported no usable fonts.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fc-scan&lt;/code&gt; failed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fc-scan /usr/local/share/fonts/microsoft/arial.ttf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;while:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;file /usr/local/share/fonts/microsoft/arial.ttf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;identified the file merely as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Something was clearly wrong.&lt;/p&gt;
&lt;p&gt;After several experiments, the issue disappeared when I switched the Windows filesystem from the kernel &lt;code&gt;ntfs3&lt;/code&gt; driver to &lt;code&gt;ntfs-3g&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I remounted it, copied the font files again, rebuilt the Fontconfig cache, and this time everything was recognized correctly.&lt;/p&gt;
&lt;p&gt;I am &lt;strong&gt;not&lt;/strong&gt; claiming that &lt;code&gt;ntfs3&lt;/code&gt; inherently breaks font files.&lt;/p&gt;
&lt;p&gt;I am saying that this was the exact symptom on my machine, and changing the driver fixed it.&lt;/p&gt;
&lt;p&gt;That distinction becomes rather important once something is published on the Internet.&lt;/p&gt;
&lt;p&gt;I could, of course, have installed one of the packages that retrieves Microsoft&amp;rsquo;s traditional core fonts.&lt;/p&gt;
&lt;p&gt;But why add another mechanism for downloading data when the files I actually wanted were already sitting on the neighboring partition?&lt;/p&gt;
&lt;h1 id="standard-software"&gt;Standard Software
&lt;/h1&gt;&lt;p&gt;After this unexpected NTFS lesson, normal life could finally begin.&lt;/p&gt;
&lt;p&gt;First:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf upgrade --refresh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then the basic toolkit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fish rsync curl wget zip unzip ansible git &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; emacs vim firefox gimp inkscape jq yq mc htop tmux &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gcc gcc-g++ make cmake python3-pip python3-devel &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; podman buildah skopeo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At this point the computer finally begins to resemble a workstation.&lt;/p&gt;
&lt;h1 id="additional-software"&gt;Additional Software
&lt;/h1&gt;&lt;h2 id="keepassxc"&gt;KeePassXC
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;KeePassXC&lt;/strong&gt; is available directly from the Fedora repositories:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install keepassxc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Years ago, applications like this required considerably more effort.&lt;/p&gt;
&lt;p&gt;Civilization occasionally moves forward.&lt;/p&gt;
&lt;h2 id="heidisql"&gt;HeidiSQL
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;HeidiSQL&lt;/strong&gt; now offers a Linux build and an RPM package.&lt;/p&gt;
&lt;p&gt;Installing it is therefore considerably less exotic than it once would have been.&lt;/p&gt;
&lt;p&gt;The Linux version uses Qt rather than .NET.&lt;/p&gt;
&lt;h2 id="opera"&gt;Opera
&lt;/h2&gt;&lt;p&gt;Opera provides its own RPM repository.&lt;/p&gt;
&lt;p&gt;Import the signing key:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo rpm --import https://rpm.opera.com/rpmrepo.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add the repository:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo tee /etc/yum.repos.d/opera.repo &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;&amp;#39;RPMREPO&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;[opera]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;name=Opera packages
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;type=rpm-md
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;baseurl=https://rpm.opera.com/rpm
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;gpgcheck=1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;gpgkey=https://rpm.opera.com/rpmrepo.key
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;enabled=1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;RPMREPO&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install opera-stable
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Not &lt;code&gt;opera-developer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I have no idea why I briefly considered installing that one.&lt;/p&gt;
&lt;h2 id="visual-studio-code"&gt;Visual Studio Code
&lt;/h2&gt;&lt;p&gt;Following the recommendations of the company whose extremely enthusiastic CEO once ran around a stage shouting:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Developers! Developers! Developers!&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;first import &lt;em&gt;their&lt;/em&gt; signing key:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then add the repository:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;printf &lt;span style="color:#e6db74"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;[code]&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;name=Visual Studio Code&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;baseurl=https://packages.microsoft.com/yumrepos/vscode&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;enabled=1&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;autorefresh=1&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;type=rpm-md&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;gpgcheck=1&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;gpgkey=https://packages.microsoft.com/keys/microsoft.asc&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| sudo tee /etc/yum.repos.d/vscode.repo &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install code
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From this point on, a normal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf upgrade
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;will update VS Code along with everything else.&lt;/p&gt;
&lt;h2 id="vscodium"&gt;VSCodium
&lt;/h2&gt;&lt;p&gt;The process is similar, except that the packages no longer come from the repository belonging to the company whose enthusiastic CEO&amp;hellip;&lt;/p&gt;
&lt;p&gt;All right.&lt;/p&gt;
&lt;p&gt;Enough.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;printf &lt;span style="color:#e6db74"&gt;&amp;#39;%s\n&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;[gitlab.com_paulcarroty_vscodium_repo]&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;name=VSCodium&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;baseurl=https://paulcarroty.gitlab.io/vscodium-deb-rpm-repo/rpms/&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;enabled=1&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;gpgcheck=1&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;repo_gpgcheck=1&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;gpgkey=https://gitlab.com/paulcarroty/vscodium-deb-rpm-repo/raw/master/pub.gpg&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;metadata_expire=1h&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| sudo tee /etc/yum.repos.d/vscodium.repo &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install codium
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="brave"&gt;Brave
&lt;/h2&gt;&lt;p&gt;Right.&lt;/p&gt;
&lt;p&gt;Definitely enough about enthusiastic CEOs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install dnf-plugins-core
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf config-manager addrepo &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --from-repofile&lt;span style="color:#f92672"&gt;=&lt;/span&gt;https://brave-browser-rpm-release.s3.brave.com/brave-browser.repo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install brave-browser
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="spotify"&gt;Spotify
&lt;/h2&gt;&lt;p&gt;Spotify comes from Flatpak.&lt;/p&gt;
&lt;p&gt;Possibly because the company is Swedish and its CEO, in turn&amp;hellip;&lt;/p&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;Never mind.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;flatpak remote-add --if-not-exists flathub &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; https://dl.flathub.org/repo/flathub.flatpakrepo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;flatpak install flathub com.spotify.Client
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="virtualbox"&gt;VirtualBox
&lt;/h2&gt;&lt;p&gt;First enable RPM Fusion:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install VirtualBox akmod-VirtualBox kernel-devel
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To explicitly build the module for the currently running kernel:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo akmods --kernels &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;uname -r&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Future kernel updates should normally be handled automatically by &lt;code&gt;akmods&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="a-better-looking-grub-menu"&gt;A Better-Looking GRUB Menu
&lt;/h2&gt;&lt;p&gt;Once a computer contains several operating systems, it seems only reasonable to make the screen used to choose between them somewhat pleasant to look at.&lt;/p&gt;
&lt;p&gt;Fortunately, there is no need to spend an evening designing one.&lt;/p&gt;
&lt;p&gt;There is &lt;strong&gt;Tela&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git clone https://github.com/vinceliuice/grub2-themes.git
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd grub2-themes
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo ./install.sh -t tela -s 1080p
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And suddenly the boot menu looks as though somebody actually expected a human being to see it.&lt;/p&gt;
&lt;h1 id="enough-for-one-day"&gt;Enough for One Day
&lt;/h1&gt;&lt;p&gt;In a single day, I managed to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;confine Windows to an amount of disk space I can tolerate,&lt;/li&gt;
&lt;li&gt;preserve Ubuntu,&lt;/li&gt;
&lt;li&gt;add Fedora,&lt;/li&gt;
&lt;li&gt;learn something about Btrfs,&lt;/li&gt;
&lt;li&gt;discover an interesting difference between &lt;code&gt;ntfs3&lt;/code&gt; and &lt;code&gt;ntfs-3g&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;recover the fonts,&lt;/li&gt;
&lt;li&gt;install the basic workstation software,&lt;/li&gt;
&lt;li&gt;and make GRUB look respectable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Doctors do not recommend more excitement than this in a twenty-four-hour period.&lt;/p&gt;
&lt;p&gt;And there is still plenty of unallocated disk space left.&lt;/p&gt;
&lt;p&gt;FreeBSD is waiting.&lt;/p&gt;
&lt;p&gt;So is Arch.&lt;/p&gt;
&lt;p&gt;Slackware, one hopes, is still waiting too.&lt;/p&gt;
&lt;p&gt;So there will be more.&lt;/p&gt;
&lt;p&gt;Probably sooner rather than later.&lt;/p&gt;</description></item><item><title>The Most Popular Computer in the World</title><link>https://lvajxi03.github.io/posts/2026-08-02/</link><pubDate>Sun, 02 Aug 2026 19:20:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-08-02/</guid><description>&lt;h1 id="introduction"&gt;Introduction
&lt;/h1&gt;&lt;p&gt;Imagine that, several decades ago, one person made a few slightly different decisions — and today your professional life looked nothing like the one you know.&lt;/p&gt;
&lt;p&gt;Dennis Ritchie, Niklaus Wirth, Steve Jobs, Richard Stallman, Bill Gates — although in that last case it might have worked out rather well for all of us — Guido van Rossum&amp;hellip;&lt;/p&gt;
&lt;p&gt;For me, that person is the author of the phrase “computers for the masses, not the classes”: Jack Tramiel. Without him, the eight-bit computer would never have found its way into quite so many homes.&lt;/p&gt;
&lt;p&gt;It certainly would not have found its way into mine.&lt;/p&gt;
&lt;h1 id="a-little-history"&gt;A Little History
&lt;/h1&gt;&lt;p&gt;Jack Tramiel was born in Łódź in 1928 as Idek Trzmiel. He rarely spoke about the early years of his life, which is hardly surprising. During the war, he was imprisoned in the Łódź Ghetto, then deported to Auschwitz-Birkenau, and later sent to the Ahlem labor camp near Hanover. He was liberated by American troops in 1945 and emigrated to the United States two years later.&lt;/p&gt;
&lt;p&gt;He served in the US Army, where he learned to repair office equipment. He later worked for small typewriter companies until he eventually founded his own business, selling and repairing such machines under the &lt;code&gt;Commodore&lt;/code&gt; brand.&lt;/p&gt;
&lt;p&gt;The name was chosen deliberately. It was meant to evoke a military rank — preferably an American one — and sound suitably serious. &lt;code&gt;General&lt;/code&gt; and &lt;code&gt;Admiral&lt;/code&gt; were already taken, so he had to move a little farther down the chain of command.&lt;/p&gt;
&lt;h1 id="calculators-and-computers"&gt;Calculators and Computers
&lt;/h1&gt;&lt;p&gt;When typewriters could no longer compete effectively with Japanese products, the company changed direction. It moved into electronic calculators with LED displays, which at the time represented the height of modern technology.&lt;/p&gt;
&lt;p&gt;A price war with &lt;em&gt;Texas Instruments&lt;/em&gt; taught Tramiel one important lesson: it was better to own a semiconductor factory than to buy chips from your main competitor.&lt;/p&gt;
&lt;p&gt;Commodore therefore acquired &lt;strong&gt;MOS Technology&lt;/strong&gt;, gaining control over both the design and production of its most important components.&lt;/p&gt;
&lt;p&gt;Designer Chuck Peddle persuaded Tramiel to enter the computer market. He argued that the calculator business had little future left and that the company needed to take the next step.&lt;/p&gt;
&lt;p&gt;This led to machines including the &lt;strong&gt;Commodore PET&lt;/strong&gt; and the &lt;strong&gt;VIC-20&lt;/strong&gt;.&lt;/p&gt;
&lt;h1 id="the-most-popular-computer-in-the-world"&gt;The Most Popular Computer in the World
&lt;/h1&gt;&lt;p&gt;The &lt;strong&gt;Commodore 64&lt;/strong&gt; was unveiled in January 1982 as the successor to the &lt;strong&gt;VIC-20&lt;/strong&gt;. It reached the market — at least in the United States — in August of the same year.&lt;/p&gt;
&lt;p&gt;Production continued until 1994, when Commodore declared bankruptcy. The machine also entered the Guinness Book of World Records as the best-selling desktop computer model in history.&lt;/p&gt;
&lt;p&gt;Its success did not come from one exceptional component. Commodore combined excellent custom chips, its own manufacturing facilities, a generous amount of memory, and a merciless pricing strategy.&lt;/p&gt;
&lt;p&gt;Competitors could design computers that were equally good, or perhaps even better. Commodore, however, could build its machine quickly, produce it in enormous numbers, and place it on store shelves at a price that caused sudden emergency meetings in the boardrooms of other companies.&lt;/p&gt;
&lt;h2 id="specifications"&gt;Specifications
&lt;/h2&gt;&lt;p&gt;At the heart of the C64 was the &lt;strong&gt;MOS 6510&lt;/strong&gt;, a modified version of the MOS 6502 processor.&lt;/p&gt;
&lt;p&gt;Because &lt;strong&gt;MOS Technology&lt;/strong&gt; already belonged to Commodore, it could prepare chips tailored to the needs of its parent company — provided, of course, that they could be manufactured quickly, cheaply, and in enormous quantities, in accordance with management policy.&lt;/p&gt;
&lt;p&gt;Graphics were handled by the &lt;strong&gt;VIC-II&lt;/strong&gt;, while sound came from the legendary &lt;strong&gt;SID&lt;/strong&gt;. Both were also designed by MOS Technology and, at the time of release, ranked among the finest chips of their kind available in home computers.&lt;/p&gt;
&lt;p&gt;Depending on the target market, the computer used a VIC-II variant compatible with either &lt;strong&gt;PAL&lt;/strong&gt; or &lt;strong&gt;NTSC&lt;/strong&gt;. It offered a palette of 16 colors, a 40 × 25-character text mode, and graphics modes reaching a resolution of 320 × 200 pixels.&lt;/p&gt;
&lt;p&gt;The VIC-II could also display eight hardware sprites: independent graphical objects that could move across the screen without constantly redrawing the background.&lt;/p&gt;
&lt;p&gt;Today, that may not sound particularly revolutionary.&lt;/p&gt;
&lt;p&gt;At the time, it certainly did.&lt;/p&gt;
&lt;p&gt;The character set used by the computer was called &lt;strong&gt;PETSCII&lt;/strong&gt;. In addition to letters, digits, and punctuation marks, it included a substantial collection of graphical symbols, allowing users to create borders, simple illustrations, interface elements, and anything else a determined person could assemble from a grid of characters.&lt;/p&gt;
&lt;p&gt;Two &lt;strong&gt;DE-9&lt;/strong&gt; ports, already familiar from Atari computers and consoles, accepted joysticks. The keyboard, joystick ports, timers, and several other input-output functions were managed by two &lt;strong&gt;MOS 6526 CIA&lt;/strong&gt; chips.&lt;/p&gt;
&lt;p&gt;The original machine came in its distinctive rounded case, commonly known as the &lt;strong&gt;breadbin&lt;/strong&gt;, and occasionally as the &lt;strong&gt;soapbox&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Characters were printed both on the top and the front of the keys. Four physical function keys on the right-hand side, used either alone or together with &lt;code&gt;Shift&lt;/code&gt;, provided eight possible combinations.&lt;/p&gt;
&lt;p&gt;The joystick ports sat on the right side of the case next to the power switch. Along the back were, among other things, the cartridge port, video output, serial disk-drive connector, cassette-port connection, and user port.&lt;/p&gt;
&lt;p&gt;And then there was the most important feature: &lt;strong&gt;64 KiB of RAM&lt;/strong&gt;, from which the computer took its name, together with 20 KiB of ROM.&lt;/p&gt;
&lt;p&gt;In 1982, when memory was expensive, offering 64 KiB in a computer aimed at ordinary users was impressive.&lt;/p&gt;
&lt;p&gt;Furthermore, although part of the processor’s address space was occupied by ROM and input-output hardware, RAM still existed underneath. By switching memory banks, an experienced programmer could use almost all of the available memory.&lt;/p&gt;
&lt;p&gt;And an experienced C64 programmer used everything.&lt;/p&gt;
&lt;p&gt;Preferably several times.&lt;/p&gt;
&lt;h2 id="peripherals"&gt;Peripherals
&lt;/h2&gt;&lt;p&gt;The three basic devices purchased by the user — or, more accurately, by the user’s parents — were a cassette drive, a joystick, and a cartridge.&lt;/p&gt;
&lt;p&gt;The cassette drive was the cheapest and therefore the most popular mass-storage device. Programs were stored on ordinary audio cassettes. The waiting times, the peculiar noises coming from the television, and loading errors occurring just before the end soon achieved legendary status among users.&lt;/p&gt;
&lt;p&gt;The joystick was naturally used for &lt;em&gt;improving productivity&lt;/em&gt;, usually in the form of better grades at school.&lt;/p&gt;
&lt;p&gt;Without a decent utility cartridge equipped with at least a fast-loading system, working with either cassette or disk storage became significantly more irritating. Popular cartridges added faster loading routines, machine-code monitors, program-freezing features, convenient file-copying tools, and a reset button.&lt;/p&gt;
&lt;p&gt;In other words, everything that should have been built into the computer from the beginning, but could not then have been sold separately.&lt;/p&gt;
&lt;p&gt;Commodore also offered dedicated monitors, such as the later &lt;strong&gt;Commodore 1084S&lt;/strong&gt;. But who could afford a monitor when the computer could be connected to an ordinary household television without much trouble?&lt;/p&gt;
&lt;p&gt;That is exactly what most of us did.&lt;/p&gt;
&lt;p&gt;Another peripheral that remained beyond the reach of many users was the &lt;strong&gt;Commodore 1541&lt;/strong&gt; disk drive, followed later by the &lt;strong&gt;1541-II&lt;/strong&gt;, with its external power supply and a case designed to match the C64C.&lt;/p&gt;
&lt;p&gt;The drive was expensive because it was almost a separate computer in its own right. It had its own &lt;strong&gt;MOS 6502&lt;/strong&gt; processor, RAM, ROM, and an operating system responsible for handling disks.&lt;/p&gt;
&lt;p&gt;Some programmers even persuaded it to perform tasks extending far beyond simply loading files.&lt;/p&gt;
&lt;p&gt;Does that remind you of anything?&lt;/p&gt;
&lt;p&gt;Ah, if only cryptocurrencies or machine learning had existed back then&amp;hellip;&lt;/p&gt;
&lt;p&gt;A third device, known to many users only from urban legends and computer magazines, was the dot-matrix printer — for example, the &lt;strong&gt;MPS 801&lt;/strong&gt;, &lt;strong&gt;MPS 802&lt;/strong&gt;, or &lt;strong&gt;MPS 803&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Most of us probably learned about plotters, mice, modems, and memory expansions only twenty years later, after venturing much deeper into retrocomputing and the demoscene.&lt;/p&gt;
&lt;h2 id="reception"&gt;Reception
&lt;/h2&gt;&lt;p&gt;From the beginning, Jack Tramiel imposed an aggressive pricing policy. The competition was always supposed to remain two steps &lt;em&gt;behind&lt;/em&gt;, never one step &lt;em&gt;ahead&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Commodore did not restrict itself to specialist computer stores. The C64 appeared in ordinary shops and department stores, and repeated price cuts brought it within reach of customers who had never previously considered owning a computer.&lt;/p&gt;
&lt;p&gt;Eventually it also crossed the Iron Curtain — and certainly reached one particular country on the Vistula River.&lt;/p&gt;
&lt;p&gt;The gaming community and the demoscene welcomed the new machine with open arms, especially as its price continued to fall. The number of games, demos, utilities, and productivity programs created for the C64 probably surpassed everything produced for competing platforms.&lt;/p&gt;
&lt;p&gt;The computer worked at home, at school, and in small offices. It could be used for gaming, programming, word processing, household accounting, music, drawing, modem communication, or spending several hours adjusting the cassette-head alignment.&lt;/p&gt;
&lt;p&gt;It suited its era perfectly.&lt;/p&gt;
&lt;p&gt;Nobody knows exactly how many units were sold. Serious estimates begin at around 12.5 million, official Commodore figures claimed 17 million, and Jack Tramiel himself mentioned numbers exceeding 20 million.&lt;/p&gt;
&lt;p&gt;Commodore was more interested in counting the money it earned than in maintaining accurate sales statistics.&lt;/p&gt;
&lt;p&gt;This is unfortunate, because later inventions such as the &lt;strong&gt;C116&lt;/strong&gt;, &lt;strong&gt;C16&lt;/strong&gt;, and &lt;strong&gt;Plus/4&lt;/strong&gt; did not share the fate of their older and far more famous sibling.&lt;/p&gt;
&lt;p&gt;Keeping precise records of their sales would probably have been considerably easier.&lt;/p&gt;
&lt;h2 id="shortcomings"&gt;Shortcomings
&lt;/h2&gt;&lt;p&gt;The computer’s greatest limitation was &lt;strong&gt;Commodore BASIC V2&lt;/strong&gt;, whose origins went back to the Microsoft BASIC interpreter licensed by Commodore in the 1970s.&lt;/p&gt;
&lt;p&gt;The language provided virtually no convenient commands for controlling graphics, sound, or sprites. To use the VIC-II or SID, users had to write values directly into hardware registers with the &lt;code&gt;POKE&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;As a result, the first program written by many young users was not:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-basic" data-lang="basic"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;PRINT&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;HELLO&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;but something more like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-basic" data-lang="basic"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;POKE&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;53280&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;POKE&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;53281&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You did not yet know what a hardware register or an address space was, but at least you had a black border and a black background.&lt;/p&gt;
&lt;p&gt;Mass-storage support also fell far short of what some competing systems offered. BASIC V2 did not even include a convenient command for listing the contents of a disk.&lt;/p&gt;
&lt;p&gt;The directory had to be loaded as though it were a program, potentially destroying the program already stored in memory.&lt;/p&gt;
&lt;p&gt;The absence of a built-in reset button was another serious inconvenience. The user had to turn the machine off and back on again, or rely on a button provided by an external cartridge.&lt;/p&gt;
&lt;p&gt;Frequently switching the computer off and immediately back on was not a particularly good idea. The original C64 power supply was hardly the pinnacle of engineering. It was, however, unquestionably the pinnacle of cost-cutting, and when it failed, it could take the computer’s chips with it.&lt;/p&gt;
&lt;p&gt;The consequences of cost-cutting could also be seen on the keyboard. Reducing the cursor keys to two because the other two directions could be produced using &lt;code&gt;Shift&lt;/code&gt; was worse than a crime.&lt;/p&gt;
&lt;p&gt;It was a mistake.&lt;/p&gt;
&lt;p&gt;Still, the accounts balanced, and fortunately neither &lt;em&gt;VisiCalc&lt;/em&gt; nor Excel had arrived yet.&lt;/p&gt;
&lt;h2 id="variants"&gt;Variants
&lt;/h2&gt;&lt;p&gt;The best-known later variant was probably the &lt;strong&gt;C64C&lt;/strong&gt;, housed in a slimmer, more angular, and considerably more modern case styled after the &lt;strong&gt;Commodore 128&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In terms of capabilities, the machine remained compatible with the original. During production, however, the motherboard was redesigned to reduce the number of chips and lower manufacturing costs.&lt;/p&gt;
&lt;p&gt;In later units, the main components included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;MOS 8500&lt;/strong&gt; processor,&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;MOS 8580 SID&lt;/strong&gt; sound chip,&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;MOS 8565 VIC-II&lt;/strong&gt; graphics chip in PAL machines, or the &lt;strong&gt;MOS 8562&lt;/strong&gt; in NTSC models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The newer chips used less power and produced less heat. The SID 8580, however, sounded somewhat different from the older 6581, particularly in the behavior of its filters.&lt;/p&gt;
&lt;p&gt;To this day, the subject can provoke a debate every bit as calm and rational as a discussion about the best text editor — Emacs, obviously.&lt;/p&gt;
&lt;p&gt;As another cost-saving measure, Commodore stopped printing graphical symbols on the front faces of the keys. All markings were now placed on the top surface.&lt;/p&gt;
&lt;p&gt;Another variant was the &lt;strong&gt;C64G&lt;/strong&gt;. It combined the light-colored keyboard of the C64C with the classic breadbin case, also produced in light-colored plastic. Inside, it usually contained one of the newer simplified motherboards.&lt;/p&gt;
&lt;p&gt;The result looked as though two generations of the C64 had met by accident in a parts warehouse and decided to build a computer from whatever happened to be lying around.&lt;/p&gt;
&lt;p&gt;Entirely in keeping with Commodore tradition.&lt;/p&gt;
&lt;h1 id="the-company-goes-bankrupt"&gt;The Company Goes Bankrupt
&lt;/h1&gt;&lt;p&gt;Commodore’s management style was truly original: from one major argument to another, from the conflict between Jack Tramiel and Irving Gould to yet another loan carrying an interest rate high enough to make even a banker glance around nervously.&lt;/p&gt;
&lt;p&gt;After one particularly serious dispute, Tramiel left the company. Not long afterward, he acquired the consumer division of the collapsing Atari and began work on the &lt;strong&gt;Atari ST&lt;/strong&gt; family.&lt;/p&gt;
&lt;p&gt;Commodore, meanwhile, possessed the Amiga technology — probably the finest multimedia platform of the second half of the 1980s — but failed to exploit its potential as effectively as it had once exploited the C64.&lt;/p&gt;
&lt;p&gt;The company went bankrupt in 1994. This was the culmination of many problems: poor management, internal conflicts, unsuccessful products, bad marketing decisions, and a complete lack of long-term vision.&lt;/p&gt;
&lt;p&gt;More forward-looking machines such as the &lt;strong&gt;Amiga 1200&lt;/strong&gt; and &lt;strong&gt;Amiga 4000&lt;/strong&gt; later found a new owner in the form of Escom.&lt;/p&gt;
&lt;p&gt;That did not lead to a happy ending either.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Commodore curse&lt;/strong&gt; is a name that could be given to the fate of successive companies acquiring rights to the brand or technology of the former corporation. Sooner or later, the new owner runs into trouble, goes bankrupt, or sells the rights to the next brave contender.&lt;/p&gt;
&lt;p&gt;The name survives.&lt;/p&gt;
&lt;p&gt;The owners change.&lt;/p&gt;
&lt;h1 id="afterward"&gt;Afterward
&lt;/h1&gt;&lt;p&gt;The community surrounding Commodore products seems not to have noticed that the company went bankrupt.&lt;/p&gt;
&lt;p&gt;Admittedly, for obvious reasons, millions of new machines no longer appear in stores, and the global economy is no longer based on C64 sales.&lt;/p&gt;
&lt;p&gt;The computer itself, however, remains very much alive.&lt;/p&gt;
&lt;p&gt;Original units can still be purchased secondhand. Replacements are being developed for increasingly rare chips, together with new motherboards, FPGA-based systems, HDMI video adapters, SD-card readers, modern joysticks, and universal tools such as the &lt;strong&gt;Ultimate-II+&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;From time to time, the market also receives a device such as &lt;strong&gt;THEC64&lt;/strong&gt;, available in a full-size version with a functional keyboard, or the &lt;strong&gt;MEGA65&lt;/strong&gt; — a development of the concept behind the almost mythical and never properly released &lt;strong&gt;Commodore 65&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Nor do the original machine’s limitations appear to represent a particularly low ceiling for modern creators.&lt;/p&gt;
&lt;p&gt;The number and quality of new demos, hobbyist programs, and commercial games can be astonishing. Every year, someone crosses another boundary that was supposedly impossible to cross. Someone discovers a new way to exploit the graphics chip, another memory trick, or a method of extracting a sound from the SID that the chip should theoretically be unable to produce.&lt;/p&gt;
&lt;p&gt;The ceiling is still nowhere in sight.&lt;/p&gt;
&lt;p&gt;So, should you have a little free time, join me in returning to this unforgettable world — to discover the computer that changed an entire generation, to enjoy its limitations, or simply to remember the years of our youth.&lt;/p&gt;
&lt;p&gt;In future articles, we will look inside the C64, run it on modern hardware, connect a few devices, see what it can still be useful for, and try to answer the most important question of all:&lt;/p&gt;
&lt;p&gt;can a computer from 1982 still improve our productivity?&lt;/p&gt;
&lt;p&gt;Welcome.&lt;/p&gt;</description></item><item><title>Seek &amp; Destroy</title><link>https://lvajxi03.github.io/posts/2026-07-25/</link><pubDate>Sat, 25 Jul 2026 00:20:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-07-25/</guid><description>&lt;p&gt;Every respectable text editor supports searching and replacing text, whether interactively or automatically. Editors from the upper shelves can also work with regular expressions.&lt;/p&gt;
&lt;p&gt;And then there is the editor from the highest shelf — Emacs — which handles all of this brilliantly, in several different ways.&lt;/p&gt;
&lt;h1 id="searching"&gt;Searching
&lt;/h1&gt;&lt;p&gt;In an average text editor, searching for text requires pressing a keyboard shortcut. Alternatively, you can try to remember beneath which pile of debris on your desk the mouse is buried, move it around until the pointer appears, and click whichever button opens the appropriate dialog box.&lt;/p&gt;
&lt;p&gt;Then you type the search term, confirm it, and the text is found. Thank you very much, and we hope to see you again during your next search.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;But not in Emacs.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;By default, the King of Editors provides &lt;strong&gt;incremental search&lt;/strong&gt;. Pressing &lt;code&gt;C-s&lt;/code&gt; — or invoking &lt;code&gt;M-x isearch-forward&lt;/code&gt; — starts the search and waits for input in the minibuffer.&lt;/p&gt;
&lt;p&gt;With every character you type, Emacs attempts to find the entire string entered so far and moves the cursor to the matching location in the text. The current match is highlighted by default. Any other occurrences visible in the buffer are highlighted as well, using a different combination of foreground and background colours.&lt;/p&gt;
&lt;p&gt;The search string is remembered between searches. Pressing &lt;code&gt;C-s&lt;/code&gt; again moves the cursor to the next occurrence of the current search string.&lt;/p&gt;
&lt;p&gt;When Emacs reaches the end of the buffer, it displays an appropriate message in the minibuffer. Pressing &lt;code&gt;C-s&lt;/code&gt; once more wraps the search around and continues from the beginning of the buffer.&lt;/p&gt;
&lt;h2 id="searching-backwards"&gt;Searching backwards
&lt;/h2&gt;&lt;p&gt;Incremental search also works &lt;strong&gt;backwards&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;C-r&lt;/code&gt; shortcut — or &lt;code&gt;M-x isearch-backward&lt;/code&gt; — behaves just like forward incremental search, except that it moves in the opposite direction.&lt;/p&gt;
&lt;p&gt;After reaching the &lt;em&gt;beginning&lt;/em&gt; of the buffer, Emacs displays an appropriate message in the minibuffer. Pressing &lt;code&gt;C-r&lt;/code&gt; again wraps the search around and continues from the &lt;em&gt;end of the buffer&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="search-results"&gt;Search results
&lt;/h2&gt;&lt;p&gt;Pressing &lt;code&gt;M-s o&lt;/code&gt; during an incremental search — which invokes &lt;code&gt;M-x isearch-occur&lt;/code&gt; — opens another &lt;em&gt;window&lt;/em&gt; containing the search results: every line in which the requested phrase occurs.&lt;/p&gt;
&lt;p&gt;The matching phrase is, naturally, highlighted so that it stands out. At the top of the window, Emacs displays a summary showing how many matches were found, on how many lines, and in which buffer.&lt;/p&gt;
&lt;p&gt;More importantly, the results are navigable. Move to any entry and press &lt;code&gt;RET&lt;/code&gt;, and Emacs takes you directly to the corresponding line in the original buffer.&lt;/p&gt;
&lt;h2 id="non-incremental-search"&gt;Non-incremental search
&lt;/h2&gt;&lt;p&gt;What if someone simply wants to enter a complete search string first and then jump to its first occurrence?&lt;/p&gt;
&lt;p&gt;A traditional, non-incremental search can be performed as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C-s RET &amp;lt;search text&amp;gt; RET&lt;/code&gt; — or &lt;code&gt;M-x search-forward&lt;/code&gt; — searches forwards,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-r RET &amp;lt;search text&amp;gt; RET&lt;/code&gt; — or &lt;code&gt;M-x search-backward&lt;/code&gt; — searches backwards.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, it works much like incremental search, except that you press &lt;code&gt;RET&lt;/code&gt; before entering the phrase.&lt;/p&gt;
&lt;h2 id="case-sensitivity"&gt;Case sensitivity
&lt;/h2&gt;&lt;p&gt;By default, Emacs uses a convenient mechanism known as &lt;strong&gt;case folding&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As long as the search string contains only lowercase letters, Emacs ignores case and attempts to match as many occurrences as possible. As soon as you enter an uppercase letter, the search becomes case-sensitive.&lt;/p&gt;
&lt;p&gt;During an incremental search, &lt;code&gt;M-c&lt;/code&gt; toggles case sensitivity explicitly, so there is no need to introduce an uppercase letter merely to change the matching behaviour.&lt;/p&gt;
&lt;h2 id="searching-with-regular-expressions"&gt;Searching with regular expressions
&lt;/h2&gt;&lt;p&gt;Emacs also provides both incremental and non-incremental searches based on regular expressions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;M-x re-search-forward&lt;/code&gt; searches forwards for the next piece of text matching the given regular expression. When a match is found, the cursor is moved to its end. If no match exists, Emacs displays an appropriate message in the minibuffer.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;M-x re-search-backward&lt;/code&gt; performs the same operation in the opposite direction.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-M-s&lt;/code&gt; — &lt;code&gt;M-x isearch-forward-regexp&lt;/code&gt; — starts an incremental forward search using a regular expression.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-M-r&lt;/code&gt; — &lt;code&gt;M-x isearch-backward-regexp&lt;/code&gt; — starts an incremental backward search using a regular expression.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="searching-within-a-region"&gt;Searching within a region
&lt;/h2&gt;&lt;p&gt;By default, Emacs searches through the buffer. To limit a search to a particular region, one can use a small but extremely useful trick: &lt;strong&gt;narrowing&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Press &lt;code&gt;C-x n n&lt;/code&gt; — or invoke &lt;code&gt;M-x narrow-to-region&lt;/code&gt; — to narrow the buffer to the currently selected region.&lt;/p&gt;
&lt;p&gt;This is interesting because the rest of the text temporarily disappears. All subsequent operations, including searching, are restricted to the visible part of the buffer.&lt;/p&gt;
&lt;p&gt;Once the work is complete, press &lt;code&gt;C-x n w&lt;/code&gt; — or invoke &lt;code&gt;M-x widen&lt;/code&gt; — to restore the hidden text to its rightful place.&lt;/p&gt;
&lt;h2 id="search-history"&gt;Search history
&lt;/h2&gt;&lt;p&gt;Search history is, of course, preserved.&lt;/p&gt;
&lt;p&gt;While searching, you can use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;M-p&lt;/code&gt; to recall the previous search string,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;M-n&lt;/code&gt; to move to the next search string in the history.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="other-useful-commands"&gt;Other useful commands
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C-y&lt;/code&gt; — &lt;code&gt;M-x yank&lt;/code&gt; — inserts the most recently killed text into the search string, because why would it not?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-w&lt;/code&gt; adds the remainder of the word at point to the search string.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="replacing"&gt;Replacing
&lt;/h1&gt;&lt;p&gt;Once again, the King of Editors refuses to blend in with the crowd and offers something better than the competition.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;M-%&lt;/code&gt; — &lt;code&gt;M-x query-replace&lt;/code&gt; — interactively replaces occurrences of one string with another, starting from the position at which the command was invoked.&lt;/p&gt;
&lt;p&gt;At every match, Emacs asks the user what should happen next:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; replaces the current match and moves to the next one,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; skips the current match and moves to the next one,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q&lt;/code&gt; ends the replacement operation,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!&lt;/code&gt; replaces the current match and every remaining match without asking any more unnecessary questions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Other available actions can be found, for example, by invoking:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M-x describe-function RET query-replace RET
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;C-M-%&lt;/code&gt; — &lt;code&gt;M-x query-replace-regexp&lt;/code&gt; — works in the same way, but uses regular expressions.&lt;/p&gt;
&lt;p&gt;Emacs also tries to preserve the &lt;strong&gt;capitalisation style&lt;/strong&gt; of the replaced text.&lt;/p&gt;
&lt;p&gt;Suppose that every occurrence of &lt;code&gt;windows&lt;/code&gt; must be replaced with &lt;code&gt;linux&lt;/code&gt;. Emacs may perform the following replacements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;windows&lt;/code&gt; becomes &lt;code&gt;linux&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Windows&lt;/code&gt; becomes &lt;code&gt;Linux&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WINDOWS&lt;/code&gt; becomes &lt;code&gt;LINUX&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;The author believes that Windows should be replaced with Linux not only in this example, but also in the lives of us all.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="non-interactive-replacement"&gt;Non-interactive replacement
&lt;/h2&gt;&lt;p&gt;Emacs also provides commands that replace all matching occurrences without asking for confirmation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;M-x replace-string&lt;/code&gt; replaces every occurrence of one string with another, starting from the position at which the command was invoked.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;M-x replace-regexp&lt;/code&gt; replaces every piece of text matching the given regular expression with the specified replacement string, again starting from the current position.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="replacing-within-a-region"&gt;Replacing within a region
&lt;/h2&gt;&lt;p&gt;If a region is active, Emacs performs replacement commands within that region by default.&lt;/p&gt;
&lt;p&gt;There is no need to narrow or widen the buffer manually — although, naturally, Emacs will not prevent anyone from doing so.&lt;/p&gt;</description></item><item><title>Jenkins, Part Two: Disposable Agents and Builder Images</title><link>https://lvajxi03.github.io/posts/2026-07-12/</link><pubDate>Sun, 12 Jul 2026 17:20:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-07-12/</guid><description>&lt;h1 id="once-again-why-all-these-containers"&gt;Once again: why all these containers?
&lt;/h1&gt;&lt;p&gt;The Kubernetes approach — short-lived, disposable things — works beautifully for Jenkins and similar CI/CD systems.&lt;/p&gt;
&lt;p&gt;Here is a list of tasks to execute in a place described by a specification, not by a physical location. Whether that place existed five minutes ago or will disappear five minutes from now does not matter all that much.&lt;/p&gt;
&lt;p&gt;Look at how the approach to running CI/CD jobs has evolved over the last twenty years:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run the job in a blocking shell script,&lt;/li&gt;
&lt;li&gt;run the job directly on the Jenkins master,&lt;/li&gt;
&lt;li&gt;run the job on a dedicated Jenkins node,&lt;/li&gt;
&lt;li&gt;run the job on one of several dedicated Jenkins nodes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And now?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run the job inside an ephemeral Pod, created just before the work starts and removed when the work is done.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hard not to admire that, is it?&lt;/p&gt;
&lt;p&gt;And there is more to admire. Each container can provide a different set of tools. Instead of installing everything manually on physical machines, you can point Jenkins at a ready-made image, built by someone else — and if you do not like that one, you can use another.&lt;/p&gt;
&lt;p&gt;All right, that is enough admiration for now.&lt;/p&gt;
&lt;h1 id="what-do-we-already-have"&gt;What do we already have?
&lt;/h1&gt;&lt;p&gt;Quite a lot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Jenkins&lt;/strong&gt; running under Kubernetes,&lt;/li&gt;
&lt;li&gt;a working &lt;strong&gt;container registry&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;PVC&lt;/strong&gt; for Jenkins data,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;Kubernetes cloud&lt;/strong&gt; definition in Jenkins,&lt;/li&gt;
&lt;li&gt;two already built &lt;strong&gt;images&lt;/strong&gt; that we are about to use.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="what-else-do-we-need"&gt;What else do we need?
&lt;/h1&gt;&lt;p&gt;Eventually, Jenkins will run jobs such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;building Python applications,&lt;/li&gt;
&lt;li&gt;building Go applications,&lt;/li&gt;
&lt;li&gt;building Hugo sites.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we need three images.&lt;/p&gt;
&lt;h1 id="python-builder"&gt;Python builder
&lt;/h1&gt;&lt;p&gt;The Python image can be built from the following &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; &lt;span style="color:#e6db74"&gt;registry.lab.local/base-builder:0.1&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;USER&lt;/span&gt; &lt;span style="color:#e6db74"&gt;root&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; apt-get update &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get install -y --no-install-recommends &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; python3 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; python3-pip &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; python3-venv &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; python3-build &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rm -rf /var/lib/apt/lists/*&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; python3 -m pip install --break-system-packages &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; build &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; twine &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wheel &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setuptools &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pytest &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ruff&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;USER&lt;/span&gt; &lt;span style="color:#e6db74"&gt;builder&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WORKDIR&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/workspace&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CMD&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;sleep&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;infinity&amp;#34;&lt;/span&gt;]&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wait a moment. Do not reach for &lt;code&gt;docker build&lt;/code&gt; yet.&lt;/p&gt;
&lt;p&gt;Actually, do not reach for it at all.&lt;/p&gt;
&lt;p&gt;Put this &lt;code&gt;Dockerfile&lt;/code&gt; into a source repository, preferably in a dedicated subdirectory. Later it will be much easier to tell what kind of creatures are being produced from it.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;We are about to enable a tiny bit of magic. If you are not ready, perhaps take a short walk.&lt;/p&gt;
&lt;p&gt;Jenkins can wait. That is, after all, usually its main job.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Create a &lt;code&gt;Jenkinsfile&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-groovy" data-lang="groovy"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pipeline &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; agent &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; kubernetes &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; yaml &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;apiVersion: v1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;kind: Pod
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;spec:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; hostAliases:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - ip: &amp;#34;10.10.10.24&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; hostnames:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - &amp;#34;registry.lab.local&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; containers:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: base
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; image: registry.lab.local/base-builder:0.1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; command: [&amp;#34;sleep&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; args: [&amp;#34;infinity&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: kaniko
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; image: registry.lab.local/kaniko-builder:latest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; command: [&amp;#34;sleep&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; args: [&amp;#34;infinity&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; volumeMounts:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: docker-config
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mountPath: /kaniko/.docker
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: docker-config
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; configMap:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; name: kaniko-docker-config
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; environment &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IMAGE_NAME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;registry.lab.local/python-builder&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IMAGE_TAG &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;3.11.1&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stages &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stage&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;Prepare&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; steps &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;base&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; script &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; env&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;GIT_SHA&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; sh&lt;span style="color:#f92672"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; script: &lt;span style="color:#e6db74"&gt;&amp;#39;git rev-parse --short HEAD&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; returnStdout: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;).&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;trim&lt;/span&gt;&lt;span style="color:#f92672"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentBuild&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;displayName&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#${env.BUILD_NUMBER} python-3&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentBuild&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;description&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;python-builder:3, commit ${env.GIT_SHA}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sh &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#34;IMAGE=${IMAGE_NAME}:${IMAGE_TAG}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ls -la
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stage&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;Build and push image&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; steps &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;kaniko&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sh &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; /kaniko/executor \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --context &amp;#34;${WORKSPACE}&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --dockerfile &amp;#34;${WORKSPACE}/python-builder/Dockerfile&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --destination &amp;#34;${IMAGE_NAME}:${IMAGE_TAG}&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --insecure \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --skip-tls-verify \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --insecure-registry registry.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; post &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; success &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Built and pushed ${IMAGE_NAME}:${IMAGE_TAG}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Commit it to the same repository, in the same subdirectory as the &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The cluster needs one additional &lt;strong&gt;ConfigMap&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ConfigMap&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;kaniko-docker-config&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;data&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;config.json&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;auths&amp;#34;: {}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;Jenkinsfile&lt;/code&gt; refers to this ConfigMap in the &lt;code&gt;agent&lt;/code&gt; section.&lt;/p&gt;
&lt;p&gt;Now create a new &lt;strong&gt;Multibranch Pipeline&lt;/strong&gt; job. Give it a sensible name, for example &lt;code&gt;python-builder&lt;/code&gt;, and configure it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;enter the repository URL,&lt;/li&gt;
&lt;li&gt;if credentials are required, select them, or add them first,&lt;/li&gt;
&lt;li&gt;under &lt;strong&gt;Build configuration&lt;/strong&gt; → &lt;strong&gt;Mode&lt;/strong&gt;, make sure &lt;strong&gt;by Jenkinsfile&lt;/strong&gt; is selected,&lt;/li&gt;
&lt;li&gt;in &lt;strong&gt;Script Path&lt;/strong&gt;, enter the path to the file, for example &lt;code&gt;python-builder/Jenkinsfile&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save the job and wait a moment.&lt;/p&gt;
&lt;p&gt;Jenkins will start scanning branches in your repository, looking for the specified &lt;code&gt;Jenkinsfile&lt;/code&gt; in each of them. If it finds one, it will try to run the job for that branch.&lt;/p&gt;
&lt;p&gt;Once the &lt;strong&gt;Stage View&lt;/strong&gt; starts rendering, you can observe what is happening in the &lt;code&gt;jenkins&lt;/code&gt; namespace:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get pods -n jenkins -w
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A new Pod will appear, do its job, and leave, as in the example below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME READY STATUS RESTARTS AGE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jenkins-0 1/1 Running 12 (57m ago) 19d
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python-builder-main-1-30xvv-l1cp7-vcw9m 0/3 Pending 0 0s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python-builder-main-1-30xvv-l1cp7-vcw9m 0/3 ContainerCreating 0 0s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python-builder-main-1-30xvv-l1cp7-vcw9m 3/3 Running 0 1s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python-builder-main-1-30xvv-l1cp7-vcw9m 3/3 Terminating 0 63s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python-builder-main-1-30xvv-l1cp7-vcw9m 0/3 Error 0 93s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The job completed successfully and the &lt;code&gt;python-builder&lt;/code&gt; image landed in the registry.&lt;/p&gt;
&lt;p&gt;You can verify that either through &lt;code&gt;registry-ui&lt;/code&gt; or directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl http://registry.lab.local/v2/python-builder/tags/list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The response should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;python-builder&amp;#34;&lt;/span&gt;,&lt;span style="color:#f92672"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;:[&lt;span style="color:#e6db74"&gt;&amp;#34;3.11.1&amp;#34;&lt;/span&gt;]}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The image is now ready to use.&lt;/p&gt;
&lt;h1 id="what-just-happened"&gt;What just happened?
&lt;/h1&gt;&lt;p&gt;The contents of the &lt;code&gt;Jenkinsfile&lt;/code&gt; definitely deserve an explanation, so let us start from the beginning.&lt;/p&gt;
&lt;h2 id="agent"&gt;&lt;code&gt;agent&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;agent&lt;/code&gt; section is without a doubt the most interesting part of this file.&lt;/p&gt;
&lt;p&gt;Besides the type name, &lt;code&gt;kubernetes&lt;/code&gt;, it contains a full Kubernetes Pod definition. In the specification we can see that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hostAliases&lt;/code&gt; are used, because we do not have proper DNS here and still need to smuggle the registry address into the Pod,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;all important containers are listed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;base&lt;/code&gt;, which provides Git and basic shell tools,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kaniko&lt;/code&gt;, which provides&amp;hellip; well, kaniko.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keeping the names consistent is important.&lt;/p&gt;
&lt;p&gt;If you name the containers something like &lt;code&gt;the_one_with_git&lt;/code&gt; and &lt;code&gt;the_one_with_kaniko&lt;/code&gt;, you will have to use exactly those names later in the actual pipeline steps:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-groovy" data-lang="groovy"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;stages &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stage&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;Prepare&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; steps &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;the_one_with_git&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In short: Jenkins creates an agent described in the &lt;code&gt;agent&lt;/code&gt; section, and pipeline steps are executed inside specific containers from that Pod — the ones that provide the required tools.&lt;/p&gt;
&lt;p&gt;This is the important part.&lt;/p&gt;
&lt;p&gt;We are not installing build tools inside the main Jenkins server. The server only orchestrates work. The tools live in disposable containers, created only for the job that needs them.&lt;/p&gt;
&lt;p&gt;That keeps the Jenkins controller cleaner, makes builds easier to reproduce, and avoids the traditional CI server problem where one machine slowly turns into a museum of forgotten compilers, package managers, SDKs, symlinks and emotional damage.&lt;/p&gt;
&lt;h2 id="kaniko"&gt;&lt;code&gt;kaniko&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;Remember that &lt;code&gt;kaniko&lt;/code&gt; is not Docker.&lt;/p&gt;
&lt;p&gt;Its job is simply to build a container image and push it to a registry.&lt;/p&gt;
&lt;p&gt;The important parameters are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--context&lt;/code&gt; — the working directory, usually the job workspace,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--dockerfile&lt;/code&gt; — the location of the &lt;code&gt;Dockerfile&lt;/code&gt;, in this case inside a subdirectory,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--destination&lt;/code&gt; — the registry location, image name and tag to push after the build.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Simple? Simple.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;But wait!&amp;rdquo; you might shout. &amp;ldquo;What about that mysterious &lt;code&gt;kaniko-docker-config&lt;/code&gt; ConfigMap?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Here it is.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kaniko&lt;/code&gt; likes to look inside &lt;code&gt;/kaniko/.docker&lt;/code&gt;, mainly for a &lt;code&gt;config.json&lt;/code&gt; file containing credentials, if they are required.&lt;/p&gt;
&lt;p&gt;Our registry does not require authentication, but we still mount a ConfigMap there and provide an empty &lt;code&gt;auths&lt;/code&gt; object:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;auths&amp;#34;&lt;/span&gt;: {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This allows kaniko to try pushing the image anonymously right away.&lt;/p&gt;
&lt;h1 id="go-builder"&gt;Go builder
&lt;/h1&gt;&lt;p&gt;Not enough examples? Fine. Let us create a &lt;code&gt;go-builder&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt; could look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; &lt;span style="color:#e6db74"&gt;golang:1.26-bookworm&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; apt-get update &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get install -y --no-install-recommends &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; git &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; openssh-client &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ca-certificates &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; curl &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wget &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; jq &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; make &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rsync &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rm -rf /var/lib/apt/lists/*&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; update-ca-certificates&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;USER&lt;/span&gt; &lt;span style="color:#e6db74"&gt;root&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WORKDIR&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/workspace&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; go version&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CMD&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;sleep&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;infinity&amp;#34;&lt;/span&gt;]&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the &lt;code&gt;Jenkinsfile&lt;/code&gt; could look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-groovy" data-lang="groovy"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pipeline &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; agent &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; kubernetes &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; yaml &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;apiVersion: v1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;kind: Pod
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;spec:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; hostAliases:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - ip: &amp;#34;10.10.10.24&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; hostnames:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - &amp;#34;registry.lab.local&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; containers:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: base
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; image: registry.lab.local/base-builder:0.1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; command: [&amp;#34;sleep&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; args: [&amp;#34;infinity&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: kaniko
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; image: registry.lab.local/kaniko-builder:latest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; command: [&amp;#34;sleep&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; args: [&amp;#34;infinity&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; volumeMounts:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: docker-config
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mountPath: /kaniko/.docker
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: docker-config
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; configMap:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; name: kaniko-docker-config
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; environment &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IMAGE_NAME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;registry.lab.local/go-builder&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IMAGE_TAG &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;1.26.1&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stages &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stage&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;Prepare&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; steps &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;base&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; script &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; env&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;GIT_SHA&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; sh&lt;span style="color:#f92672"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; script: &lt;span style="color:#e6db74"&gt;&amp;#39;git rev-parse --short HEAD&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; returnStdout: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;).&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;trim&lt;/span&gt;&lt;span style="color:#f92672"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentBuild&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;displayName&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#${env.BUILD_NUMBER} go-${env.IMAGE_TAG}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentBuild&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;description&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;go-builder:${env.IMAGE_TAG}, commit ${env.GIT_SHA}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sh &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#34;GO_VERSION=${IMAGE_TAG}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#34;IMAGE=${IMAGE_NAME}:${IMAGE_TAG}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ls -la
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stage&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;Build and push image&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; steps &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;kaniko&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sh &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; /kaniko/executor \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --context &amp;#34;${WORKSPACE}&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --dockerfile &amp;#34;${WORKSPACE}/go-builder/Dockerfile&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --destination &amp;#34;${IMAGE_NAME}:${IMAGE_TAG}&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --insecure \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --skip-tls-verify \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --insecure-registry registry.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; post &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; success &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;Built and pushed ${IMAGE_NAME}:${IMAGE_TAG}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Yes, you do not need to rub your eyes in disbelief.&lt;/p&gt;
&lt;p&gt;This &lt;code&gt;Jenkinsfile&lt;/code&gt; looks almost identical to the previous one. The agent definition is the same, the &lt;code&gt;kaniko&lt;/code&gt; invocation is the same, and the base image usage is the same. In practice, only names, version numbers and file paths change.&lt;/p&gt;
&lt;p&gt;Simple? Simple.&lt;/p&gt;
&lt;h1 id="example-use-case"&gt;Example use case
&lt;/h1&gt;&lt;p&gt;If you have a Go application lying around, you can use a &lt;code&gt;Jenkinsfile&lt;/code&gt; like this one.&lt;/p&gt;
&lt;p&gt;In this example, the test application is called &lt;code&gt;rme&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-groovy" data-lang="groovy"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pipeline &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; agent &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; kubernetes &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; yaml &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;apiVersion: v1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;kind: Pod
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;spec:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; hostAliases:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - ip: &amp;#34;10.10.10.24&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; hostnames:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - &amp;#34;registry.lab.local&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; containers:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: rme-go-builder
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; image: registry.lab.local/go-builder:1.26.1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; command: [&amp;#34;sleep&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; args: [&amp;#34;infinity&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: rme-kaniko
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; image: registry.lab.local/kaniko-builder:latest
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; command: [&amp;#34;sleep&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; args: [&amp;#34;infinity&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; volumeMounts:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: docker-config
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mountPath: /kaniko/.docker
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; - name: docker-config
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; configMap:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; name: kaniko-docker-config
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; environment &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IMAGE_NAME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;registry.lab.local/rme&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stages &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stage&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;Prepare&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; steps &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;rme-go-builder&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; script &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; env&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;VERSION&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; env&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;GIT_COMMIT&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;take&lt;/span&gt;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentBuild&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;displayName&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#${env.BUILD_NUMBER} ${env.VERSION}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentBuild&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;description&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;${env.IMAGE_NAME}:${env.VERSION}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stage&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;Build binary&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; steps &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;rme-go-builder&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sh &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; git config --global --add safe.directory &amp;#34;$WORKSPACE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; CGO_ENABLED=0 GOOS=linux go build -o rme .
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stage&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;Build and push image&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; steps &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;rme-kaniko&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sh &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; /kaniko/executor \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --context &amp;#34;${WORKSPACE}&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --dockerfile &amp;#34;${WORKSPACE}/Dockerfile&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --destination &amp;#34;${IMAGE_NAME}:${VERSION}&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --destination &amp;#34;${IMAGE_NAME}:latest&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --insecure \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --skip-tls-verify \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --insecure-registry registry.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The application &lt;code&gt;Dockerfile&lt;/code&gt; can be very small:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; &lt;span style="color:#e6db74"&gt;scratch&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;COPY&lt;/span&gt; rme /rme&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;EXPOSE&lt;/span&gt; &lt;span style="color:#e6db74"&gt;9101&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ENTRYPOINT&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;/rme&amp;#34;&lt;/span&gt;]&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is a more realistic example.&lt;/p&gt;
&lt;p&gt;It uses a dedicated builder image, &lt;code&gt;go-builder&lt;/code&gt;, to compile the Go application, and &lt;code&gt;kaniko&lt;/code&gt; only for building and pushing the final container image.&lt;/p&gt;
&lt;p&gt;A few details are worth noticing, because the devil usually lives exactly there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;container names matter, because they are later used in &lt;code&gt;container(...)&lt;/code&gt; blocks,&lt;/li&gt;
&lt;li&gt;the second &lt;code&gt;--destination&lt;/code&gt; in the &lt;code&gt;kaniko&lt;/code&gt; invocation:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;--destination &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;IMAGE_NAME&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;:latest&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;is intentional.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;latest&lt;/code&gt; tag will not magically appear in our registry by itself. If we want the most recently pushed image to also be available as &lt;code&gt;latest&lt;/code&gt;, we need to tag and push it that way.&lt;/p&gt;
&lt;h1 id="summary-and-notes"&gt;Summary and notes
&lt;/h1&gt;&lt;p&gt;As you can see, there is less and less that we strictly &lt;strong&gt;must&lt;/strong&gt; do inside the cluster, and more and more that we &lt;strong&gt;can&lt;/strong&gt; do in a controlled and repeatable way.&lt;/p&gt;
&lt;p&gt;Versioned configuration, job history, artifact history, example applications, builds running inside containers — quite a lot is happening here.&lt;/p&gt;
&lt;p&gt;Why use a &lt;strong&gt;Multibranch Pipeline&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;Good question, but the answers are fairly obvious:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we do not have to clone the repository manually or hard-code credentials in the &lt;code&gt;Jenkinsfile&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;test branches do not require separate Jenkins jobs,&lt;/li&gt;
&lt;li&gt;Jenkins detects new branches automatically,&lt;/li&gt;
&lt;li&gt;deleted branches can disappear from Jenkins automatically as well,&lt;/li&gt;
&lt;li&gt;each branch can have its own version of the &lt;code&gt;Jenkinsfile&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;pull requests are handled in a much cleaner way.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;And now a suspicious thought appears.&lt;/p&gt;
&lt;p&gt;What if we put all those YAML files under version control and let some clever mechanism take care of them automatically, instead of calling &lt;code&gt;kubectl&lt;/code&gt; by hand every few minutes?&lt;/p&gt;
&lt;p&gt;Could such a thing exist?&lt;/p&gt;
&lt;p&gt;Who knows.&lt;/p&gt;</description></item><item><title>Jenkins in Kubernetes: Bring a Bucket</title><link>https://lvajxi03.github.io/posts/2026-07-11/</link><pubDate>Sat, 11 Jul 2026 11:10:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-07-11/</guid><description>
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; working with Jenkins requires a lot of determination. It is also wise to prepare a bucket in advance — for blood, sweat, tears, and all the failed pipelines.&lt;/p&gt;
&lt;p&gt;There. I warned you.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h1 id="why-jenkins"&gt;Why Jenkins?
&lt;/h1&gt;&lt;p&gt;Jenkins has accumulated enough myths and urban legends to deserve several dedicated episodes of a good television show about mysterious phenomena and conspiracy theories.&lt;/p&gt;
&lt;p&gt;For now, let us focus on a few practical facts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jenkins is a true Swiss Army knife in the CI/CD world.&lt;/li&gt;
&lt;li&gt;Jenkins is worth knowing, because it may be lurking just around the corner in the organization you are about to join.&lt;/li&gt;
&lt;li&gt;Jenkins is easy to run both locally and centrally.&lt;/li&gt;
&lt;li&gt;Thanks to its many plugins, Jenkins can probably be used for almost anything.&lt;/li&gt;
&lt;li&gt;You do not have to spend your life clicking around the UI. Jenkins Pipelines do their job.&lt;/li&gt;
&lt;li&gt;It is worth getting familiar with Jenkins if only to form your own opinion about what you actually need from a CI/CD ecosystem.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="a-pinch-of-history"&gt;A pinch of history
&lt;/h1&gt;&lt;p&gt;Jenkins originally started as a tool for building Java code, wrapped in a convenient web service available to everyone.&lt;/p&gt;
&lt;p&gt;At first it was called &lt;strong&gt;Hudson&lt;/strong&gt;, and perhaps it would still be called that today, but Sun Microsystems — where Hudson was created — was acquired by the large and not particularly cuddly company known for large and not particularly agile databases.&lt;/p&gt;
&lt;p&gt;Then came a dispute over the name and project ownership. A fork appeared, someone proposed a new name, and off it went.&lt;/p&gt;
&lt;p&gt;If this reminds you of MariaDB, congratulations: your pattern recognition module is working correctly.&lt;/p&gt;
&lt;h1 id="jenkins-in-a-kubernetes-cluster"&gt;Jenkins in a Kubernetes cluster
&lt;/h1&gt;&lt;p&gt;We will use Jenkins not only to build containers, but also to build regular applications and run ordinary tasks using purpose-built containers.&lt;/p&gt;
&lt;h2 id="requirements"&gt;Requirements
&lt;/h2&gt;&lt;p&gt;Jenkins stores data on disk, so we will need a PersistentVolumeClaim.&lt;/p&gt;
&lt;p&gt;From the cluster&amp;rsquo;s point of view, the main Jenkins server will run as a StatefulSet. An Ingress controller will provide HTTP(S) access through the address &lt;code&gt;jenkins.lab.local&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As usual in this lab, add the following line to &lt;code&gt;/etc/hosts&lt;/code&gt; on every machine that needs to access Jenkins, including your workstation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.24 jenkins.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-image"&gt;The image
&lt;/h2&gt;&lt;p&gt;You can use an existing Jenkins image, for example the latest LTS image.&lt;/p&gt;
&lt;p&gt;I do not recommend doing that here.&lt;/p&gt;
&lt;p&gt;The bucket for blood, sweat and tears would need to be emptied far too often, with very little benefit. It is better to commit a custom image yourself — or in pairs, because at some point the basement nerds need someone to hold the light bulb.&lt;/p&gt;
&lt;h2 id="dockerfile"&gt;Dockerfile
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; &lt;span style="color:#e6db74"&gt;jenkins/jenkins:lts-jdk21&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; &lt;span style="color:#e6db74"&gt;jenkins-tools&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; &lt;span style="color:#e6db74"&gt;eclipse-temurin:21-jre-jammy&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ARG&lt;/span&gt; JENKINS_VERSION&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;.555.3&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;USER&lt;/span&gt; &lt;span style="color:#e6db74"&gt;root&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; apt-get update &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get install -y --no-install-recommends &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; curl &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; git &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; openssh-client &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ca-certificates &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tini &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fontconfig &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; jq &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; unzip &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; zip &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rm -rf /var/lib/apt/lists/*&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; useradd -m -d /var/jenkins_home -u &lt;span style="color:#ae81ff"&gt;1000&lt;/span&gt; jenkins&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; mkdir -p /usr/share/jenkins /var/jenkins_home /usr/share/jenkins/ref/plugins&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; curl -fsSL &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -o /usr/share/jenkins/jenkins.war &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;https://get.jenkins.io/war-stable/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;JENKINS_VERSION&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/jenkins.war&amp;#34;&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;COPY&lt;/span&gt; --from&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jenkins-tools /usr/bin/jenkins-plugin-cli /usr/bin/jenkins-plugin-cli&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;COPY&lt;/span&gt; --from&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jenkins-tools /opt/jenkins-plugin-manager.jar /opt/jenkins-plugin-manager.jar&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;COPY&lt;/span&gt; plugins.txt /tmp/plugins.txt&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; JENKINS_WAR&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/usr/share/jenkins/jenkins.war &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; JENKINS_HOME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/usr/share/jenkins/ref &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; jenkins-plugin-cli &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --war /usr/share/jenkins/jenkins.war &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --plugin-file /tmp/plugins.txt&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; chown -R jenkins:jenkins /usr/share/jenkins /var/jenkins_home&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;USER&lt;/span&gt; &lt;span style="color:#e6db74"&gt;jenkins&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ENV&lt;/span&gt; JENKINS_HOME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/var/jenkins_home&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;VOLUME&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/var/jenkins_home&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;EXPOSE&lt;/span&gt; &lt;span style="color:#e6db74"&gt;8080&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;50000&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ENTRYPOINT&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;/usr/bin/tini&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;--&amp;#34;&lt;/span&gt;]&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CMD&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;java&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;-Djenkins.install.runSetupWizard=false&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/usr/share/jenkins/jenkins.war&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;--httpPort=8080&amp;#34;&lt;/span&gt;]&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can put plugins in &lt;code&gt;plugins.txt&lt;/code&gt; and install them directly into the image.&lt;/p&gt;
&lt;p&gt;You can also install them later, because Jenkins will have persistent storage. Whether this is a convenience or the beginning of a future archaeology project depends on your level of optimism.&lt;/p&gt;
&lt;h1 id="building-the-image"&gt;Building the image
&lt;/h1&gt;&lt;p&gt;The image is built for the &lt;code&gt;arm64&lt;/code&gt; architecture:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker buildx build &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --platform linux/arm64 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -t registry.lab.local/jenkins-lab:0.0.1 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --push .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The most likely result at this point is something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt; [stage-1 2/11] RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends curl git openssh-client ca-certificates tini fontconfig jq unzip zip &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;0.126 exec /bin/sh: exec format error
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which means we need to help Docker understand how to run &lt;code&gt;arm64&lt;/code&gt; binaries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --privileged --rm tonistiigi/binfmt --install arm64
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A successful installation should produce something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Installing: arm64 OK
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;supported&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;linux/amd64&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;linux/amd64/v2&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;linux/amd64/v3&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;linux/arm64&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;linux/386&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;emulators&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;python3.14&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;qemu-aarch64&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we can build the image again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker buildx build &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --platform linux/arm64 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -t registry.lab.local/jenkins-lab:0.0.1 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --push .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="cluster-objects"&gt;Cluster objects
&lt;/h1&gt;&lt;p&gt;For Jenkins to work properly, we need a few Kubernetes objects.&lt;/p&gt;
&lt;h2 id="namespace"&gt;Namespace
&lt;/h2&gt;&lt;p&gt;The simplest way to create the namespace is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl create namespace jenkins
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you keep your configuration under version control — and you should, because future you has enough problems already — use a manifest instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Namespace&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="persistentvolumeclaim"&gt;PersistentVolumeClaim
&lt;/h2&gt;&lt;p&gt;The PVC definition looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins-home&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;accessModes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;ReadWriteOnce&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;resources&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;requests&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;storage&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;20Gi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;storageClassName&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;local-path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="serviceaccount-and-role"&gt;ServiceAccount and Role
&lt;/h2&gt;&lt;p&gt;Jenkins needs permission to create and manage agent Pods in its namespace.&lt;/p&gt;
&lt;p&gt;The following definitions are enough for this lab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ServiceAccount&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Role&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins-agent-manager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;rules&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;apiGroups&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;resources&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;pods&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;pods/exec&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;pods/log&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;persistentvolumeclaims&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;verbs&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;get&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;list&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;watch&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;create&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;delete&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;patch&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;update&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;apiGroups&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;resources&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;secrets&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;configmaps&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;verbs&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;get&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;list&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;watch&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;apiGroups&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;resources&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;events&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;verbs&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;get&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;list&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;watch&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;RoleBinding&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins-agent-manager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;subjects&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ServiceAccount&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;roleRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Role&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins-agent-manager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;apiGroup&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;rbac.authorization.k8s.io&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="service-and-ingress"&gt;Service and Ingress
&lt;/h2&gt;&lt;p&gt;Now we need a Service and an Ingress:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;selector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;app&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;http&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;port&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;targetPort&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;http&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;agent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;port&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;50000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;targetPort&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;agent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Ingress&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ingressClassName&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;nginx&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rules&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins.lab.local&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;http&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;paths&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;pathType&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Prefix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;backend&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;service&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;port&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;http&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="statefulset"&gt;StatefulSet
&lt;/h2&gt;&lt;p&gt;Finally, the StatefulSet:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;apps/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;StatefulSet&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;serviceName&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;replicas&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;selector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;matchLabels&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;app&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;template&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;labels&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;app&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;serviceAccountName&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;nodeSelector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;przeznaczenie&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ci-cd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins-home&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;persistentVolumeClaim&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;claimName&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins-home&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;containers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;registry.lab.local/jenkins-lab:0.0.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;imagePullPolicy&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Always&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;http&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;containerPort&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;agent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;containerPort&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;50000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;env&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;JENKINS_HOME&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/var/jenkins_home&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;JAVA_OPTS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;-Djenkins.install.runSetupWizard=false&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumeMounts&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jenkins-home&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;mountPath&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/var/jenkins_home&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;startupProbe&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;httpGet&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/login&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;port&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;http&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;periodSeconds&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;failureThreshold&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;readinessProbe&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;httpGet&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/login&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;port&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;http&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;periodSeconds&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;failureThreshold&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;livenessProbe&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;httpGet&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/login&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;port&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;http&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;periodSeconds&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;failureThreshold&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;blockquote&gt;
 &lt;p&gt;Save all of the above definitions as YAML files and apply them with &lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h1 id="first-start"&gt;First start
&lt;/h1&gt;&lt;p&gt;Jenkins should start and greet us by asking for the initial token.&lt;/p&gt;
&lt;p&gt;That token can be found in the depths of its home directory on the PVC. After that, it is the usual Jenkins ritual: administrator account, password, pending plugins, and all the other traditional rites of passage.&lt;/p&gt;
&lt;p&gt;A freshly started Jenkins is a strange creature.&lt;/p&gt;
&lt;p&gt;On one hand, it can be armed to the teeth with plugins. On the other hand, it is as naked as a jaybird. It does not have any build tools installed — but that is fine. We do not really need them.&lt;/p&gt;
&lt;p&gt;The actual work will be done by purpose-built containers, created only when needed.&lt;/p&gt;
&lt;h1 id="configuring-the-kubernetes-cloud"&gt;Configuring the Kubernetes cloud
&lt;/h1&gt;&lt;p&gt;This is where the Jenkins &lt;strong&gt;Cloud&lt;/strong&gt; configuration comes in.&lt;/p&gt;
&lt;p&gt;Configure it like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Manage Jenkins&lt;/strong&gt; → &lt;strong&gt;Clouds&lt;/strong&gt; → &lt;strong&gt;Add a new cloud&lt;/strong&gt; → &lt;strong&gt;Kubernetes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;kubernetes&lt;/code&gt;, or any other name you prefer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes URL:&lt;/strong&gt; &lt;code&gt;https://kubernetes.default.svc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes namespace:&lt;/strong&gt; &lt;code&gt;jenkins&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;WebSocket&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jenkins URL:&lt;/strong&gt; &lt;code&gt;http://jenkins.jenkins.svc.cluster.local:8080&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save the configuration.&lt;/p&gt;
&lt;p&gt;Now all that remains is to create a few useful images, wire them into Jenkins pipelines, and then use them, use them, and use them some more.&lt;/p&gt;
&lt;h1 id="base-builder"&gt;Base builder
&lt;/h1&gt;&lt;p&gt;The first image will be&amp;hellip; a base image for building other, better base images.&lt;/p&gt;
&lt;p&gt;What does that mean?&lt;/p&gt;
&lt;p&gt;The plan is for Jenkins to build many different applications, and also run jobs that may not build anything at all, but still need containers with specific tools inside.&lt;/p&gt;
&lt;p&gt;There are two ways to approach this problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create one large image with every possible tool inside,&lt;/li&gt;
&lt;li&gt;create many small, specialized images and use each one only when needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The second approach is easier to maintain.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt; for the &lt;strong&gt;base builder&lt;/strong&gt; looks roughly like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; &lt;span style="color:#e6db74"&gt;debian:bookworm-slim&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; apt-get update &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get install -y --no-install-recommends &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ca-certificates &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; curl &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wget &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; git &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; openssh-client &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bash &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; make &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; jq &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; yq &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tar &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gzip &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; xz-utils &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; unzip &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; zip &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rsync &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; file &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; procps &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iputils-ping &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dnsutils &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rm -rf /var/lib/apt/lists/*&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; useradd -m -u &lt;span style="color:#ae81ff"&gt;1000&lt;/span&gt; builder&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;USER&lt;/span&gt; &lt;span style="color:#e6db74"&gt;builder&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WORKDIR&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/workspace&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CMD&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;sleep&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;infinity&amp;#34;&lt;/span&gt;]&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Build it like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker buildx build &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --platform linux/arm64 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -t registry.lab.local/base-builder:0.1 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --push .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="kaniko-builder"&gt;Kaniko builder
&lt;/h1&gt;&lt;p&gt;The second image will contain &lt;strong&gt;kaniko&lt;/strong&gt;, mainly for building container images.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; this is a lab setup. Kaniko works well enough for our purposes here, but before using it in production, check the current state of the project and consider alternatives such as Buildah or Podman. The CI/CD ecosystem moves quickly, usually while pretending it is doing so in a controlled manner.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Yes, kaniko is simpler to use than Docker in this setup. It does not require root privileges and it does not need a Docker daemon.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt; can look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; &lt;span style="color:#e6db74"&gt;gcr.io/kaniko-project/executor:latest&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; &lt;span style="color:#e6db74"&gt;kaniko&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; &lt;span style="color:#e6db74"&gt;alpine:3.21&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; apk add --no-cache &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ca-certificates &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bash &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; coreutils &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; curl &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; git&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;COPY&lt;/span&gt; --from&lt;span style="color:#f92672"&gt;=&lt;/span&gt;kaniko /kaniko /kaniko&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ENV&lt;/span&gt; PATH&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/kaniko:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;PATH&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WORKDIR&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/workspace&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CMD&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;sleep&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;infinity&amp;#34;&lt;/span&gt;]&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Build it like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker buildx build &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --platform linux/arm64 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -t registry.lab.local/kaniko-builder:0.1 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --push .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These two images need to be built manually.&lt;/p&gt;
&lt;p&gt;All the other ones will be created later by Jenkins itself.&lt;/p&gt;
&lt;p&gt;If you managed to get here in one piece, that is already quite good. In the next episode, we will learn how to make actual use of our newly assembled toys.&lt;/p&gt;</description></item><item><title>2026-06-30</title><link>https://lvajxi03.github.io/posts/2026-06-30/</link><pubDate>Tue, 30 Jun 2026 19:10:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-30/</guid><description>&lt;h1 id="suit-up-and-march-on"&gt;Suit Up and March On!
&lt;/h1&gt;&lt;h2 id="what-is-helm"&gt;What is Helm?
&lt;/h2&gt;&lt;p&gt;Kubernetes is popular enough that it was only a matter of time before someone came up with a way to install applications using one or two commands instead of manually assembling yet another small mountain of YAML files.&lt;/p&gt;
&lt;p&gt;That tool is called &lt;strong&gt;Helm&lt;/strong&gt;. It helps define, install, upgrade and manage applications running inside a Kubernetes cluster.&lt;/p&gt;
&lt;p&gt;Think of it as the Kubernetes equivalent of &lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;dnf&lt;/code&gt; or &lt;code&gt;brew&lt;/code&gt; — except instead of installing packages on a machine, it installs applications into your cluster.&lt;/p&gt;
&lt;h2 id="how-does-it-work"&gt;How does it work?
&lt;/h2&gt;&lt;p&gt;Helm introduces three basic concepts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chart&lt;/strong&gt; — a package containing templates for all Kubernetes manifests required by an application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Values&lt;/strong&gt; — parameters used to customize those templates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Release&lt;/strong&gt; — a deployed instance of a chart inside a cluster. The first installation creates the initial revision, and every upgrade creates another one, making rollbacks straightforward.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, you take a chart, adjust its values, install it into the cluster and get a named release that Helm can later upgrade, list, inspect or remove.&lt;/p&gt;
&lt;h2 id="what-do-we-need"&gt;What do we need?
&lt;/h2&gt;&lt;p&gt;Not much:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a Kubernetes cluster,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;helm&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our first application will be &lt;strong&gt;Headlamp&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Kubernetes Dashboard still exists, but these days Headlamp is often the more pleasant choice, so that is what we will use here.&lt;/p&gt;
&lt;p&gt;Headlamp is one of those rare applications that you install once and it simply works. Such things deserve appreciation.&lt;/p&gt;
&lt;h2 id="installing-applications-with-helm"&gt;Installing applications with Helm
&lt;/h2&gt;&lt;p&gt;Most applications are distributed through Helm repositories.&lt;/p&gt;
&lt;p&gt;Adding one is simple:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm repo add repository-name https://some.repo.url
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm install my-application repository-name/chart
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;repository-name&lt;/code&gt; is your local alias for the repository.&lt;/p&gt;
&lt;p&gt;Likewise, &lt;code&gt;my-application&lt;/code&gt; is the name of your release. It may look redundant at first, but imagine testing three different MariaDB installations from three different sources. Suddenly meaningful release names start looking less like ceremony and more like self-defense.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;helm repo update&lt;/code&gt; works much like &lt;code&gt;apt update&lt;/code&gt;: it refreshes the local cache with information about available chart versions.&lt;/p&gt;
&lt;p&gt;You can also search through charts available in your configured repositories:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm search repo headlamp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm search repo bitnami
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="where-do-the-values-come-from"&gt;Where do the values come from?
&lt;/h2&gt;&lt;p&gt;The easiest way is to ask Helm for the default configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm show values repository-name/chart &amp;gt; values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then edit the generated file and adjust it to your environment.&lt;/p&gt;
&lt;p&gt;For Headlamp, the following &lt;code&gt;values.yaml&lt;/code&gt; is enough for this lab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;config&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;inCluster&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;ingress&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ingressClassName&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;nginx&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hosts&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;headlamp.lab.local&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;paths&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;type&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Prefix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;nodeSelector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;przeznaczenie&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;observability&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We need to define the Ingress class, the hostname under which the application will be available, and optionally a &lt;code&gt;nodeSelector&lt;/code&gt; if we want to control where the Pod runs.&lt;/p&gt;
&lt;p&gt;As usual in this lab setup, we also need to add the hostname to &lt;code&gt;/etc/hosts&lt;/code&gt; on every machine that will access the service, including the workstation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.24 headlamp.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="installing-headlamp"&gt;Installing Headlamp
&lt;/h2&gt;&lt;p&gt;Now comes the easy part:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm install headlamp headlamp/headlamp &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --namespace headlamp &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --create-namespace &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -f values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After opening the URL in a browser, we get a friendly login screen:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Login screen" class="gallery-image" data-flex-basis="310px" data-flex-grow="129" height="376" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-30/headlamp-1.png" width="487"&gt;&lt;/p&gt;
&lt;p&gt;Headlamp asks for a Kubernetes authentication token.&lt;/p&gt;
&lt;p&gt;Generating one is straightforward:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl -n headlamp create token headlamp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The application itself is clean, tasteful and not overloaded with options. Most things are exactly where you expect them to be.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Headlamp UI" class="gallery-image" data-flex-basis="451px" data-flex-grow="188" height="916" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-30/headlamp-2.png" srcset="https://lvajxi03.github.io/posts/2026-06-30/headlamp-2_hu_3c665b46754731f4.png 800w, https://lvajxi03.github.io/posts/2026-06-30/headlamp-2_hu_251e384dec7fcfd6.png 1600w, https://lvajxi03.github.io/posts/2026-06-30/headlamp-2.png 1725w" width="1725"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Headlamp UI" class="gallery-image" data-flex-basis="451px" data-flex-grow="188" height="916" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-30/headlamp-3.png" srcset="https://lvajxi03.github.io/posts/2026-06-30/headlamp-3_hu_2be15f917b29e3f8.png 800w, https://lvajxi03.github.io/posts/2026-06-30/headlamp-3_hu_163c51f0590c4ee4.png 1600w, https://lvajxi03.github.io/posts/2026-06-30/headlamp-3.png 1725w" width="1725"&gt;&lt;/p&gt;
&lt;p&gt;You can browse resources, inspect logs and click through various settings without leaving the browser.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Headlamp UI" class="gallery-image" data-flex-basis="451px" data-flex-grow="188" height="912" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-30/headlamp-4.png" srcset="https://lvajxi03.github.io/posts/2026-06-30/headlamp-4_hu_fac915c1e8f7d707.png 800w, https://lvajxi03.github.io/posts/2026-06-30/headlamp-4_hu_f7d985a2f3d157cb.png 1600w, https://lvajxi03.github.io/posts/2026-06-30/headlamp-4.png 1716w" width="1716"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Headlamp UI" class="gallery-image" data-flex-basis="451px" data-flex-grow="188" height="915" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-30/headlamp-5.png" srcset="https://lvajxi03.github.io/posts/2026-06-30/headlamp-5_hu_c12026e6a6c5acb6.png 800w, https://lvajxi03.github.io/posts/2026-06-30/headlamp-5_hu_614b5cdb15bee19d.png 1600w, https://lvajxi03.github.io/posts/2026-06-30/headlamp-5.png 1722w" width="1722"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Another excellent Kubernetes client is &lt;strong&gt;k9s&lt;/strong&gt;. It runs entirely in the terminal, so you do not get fancy graphical topology views, but everything else is there — and it is fast.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="k9s" class="gallery-image" data-flex-basis="451px" data-flex-grow="188" height="949" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-30/k9s-1.png" srcset="https://lvajxi03.github.io/posts/2026-06-30/k9s-1_hu_7f4b4c77f089e7c2.png 800w, https://lvajxi03.github.io/posts/2026-06-30/k9s-1_hu_a6d23966b1a797a2.png 1600w, https://lvajxi03.github.io/posts/2026-06-30/k9s-1.png 1786w" width="1786"&gt;&lt;/p&gt;
&lt;p&gt;As you can see, basic Helm usage is pleasantly simple.&lt;/p&gt;
&lt;h2 id="installing-databases"&gt;Installing databases
&lt;/h2&gt;&lt;p&gt;There is not much point in overexplaining databases here. We are going to install a few of them from a popular public repository and see how little work is actually required.&lt;/p&gt;
&lt;p&gt;We will use Bitnami&amp;rsquo;s repository:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm repo add bitnami https://charts.bitnami.com/bitnami
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl create namespace databases
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The separate namespace is not strictly magical, but it keeps things tidy.&lt;/p&gt;
&lt;h2 id="postgresql"&gt;PostgreSQL
&lt;/h2&gt;&lt;p&gt;Example &lt;code&gt;values.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;global&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;storageClass&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;local-path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;architecture&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;standalone&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;auth&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;postgresPassword&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;some-password&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;username&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;marcin&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;some-other-password&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;database&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;utensils&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;primary&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;nodeSelector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;przeznaczenie&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;heavy-worker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;persistence&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;size&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;20Gi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;service&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;type&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;NodePort&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;postgresql&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;5432&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;nodePorts&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;postgresql&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;30432&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The database will be exposed through NodePort &lt;strong&gt;30432&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Since this is not a web application, we need to remember which node actually hosts the Pod. Thanks to the &lt;code&gt;nodeSelector&lt;/code&gt;, that should be &lt;code&gt;10.10.10.25&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Installation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm install postgresql bitnami/postgresql &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --namespace databases &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -f values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Afterwards Helm prints a few useful notes, including commands for retrieving generated credentials:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;To get the password for &amp;#34;postgres&amp;#34; run:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;export POSTGRES_ADMIN_PASSWORD=$(kubectl get secret --namespace databases postgresql -o jsonpath=&amp;#34;{.data.postgres-password}&amp;#34; | base64 -d)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;To get the password for &amp;#34;marcin&amp;#34; run:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;export POSTGRES_PASSWORD=$(kubectl get secret --namespace databases postgresql -o jsonpath=&amp;#34;{.data.password}&amp;#34; | base64 -d)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="mariadb"&gt;MariaDB
&lt;/h2&gt;&lt;p&gt;Example configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;global&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;storageClass&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;local-path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;architecture&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;standalone&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;auth&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rootPassword&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;some-password&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;database&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;utensils&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;username&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;marcin&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;some-other-password&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;primary&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;nodeSelector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;przeznaczenie&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;heavy-worker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;persistence&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;size&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;20Gi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;service&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;type&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;NodePort&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;mysql&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;3306&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;nodePorts&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;mysql&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;30306&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This database will listen on NodePort &lt;strong&gt;30306&lt;/strong&gt;, on the same node as PostgreSQL.&lt;/p&gt;
&lt;p&gt;Installation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm install mariadb bitnami/mariadb &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --namespace databases &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -f values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Administrator credentials can be retrieved from the generated Secret:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Username: root
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Password: $(kubectl get secret --namespace databases mariadb -o jsonpath=&amp;#34;{.data.mariadb-root-password}&amp;#34; | base64 -d)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="redis"&gt;Redis
&lt;/h2&gt;&lt;p&gt;Example configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;architecture&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;standalone&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;auth&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;master&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;nodeSelector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;przeznaczenie&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;heavy-worker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;persistence&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;storageClass&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;local-path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;size&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;5Gi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;resources&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;requests&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cpu&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;50m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;memory&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;128Mi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;limits&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;memory&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;512Mi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metrics&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;serviceMonitor&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;databases&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Installation is exactly what you would expect:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm install redis bitnami/redis &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --namespace databases &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -f values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At this point the cluster is starting to look like an actual environment instead of an empty playground.&lt;/p&gt;
&lt;p&gt;As an exercise, I recommend installing a few well-known applications and checking how they are packaged, configured and exposed. There is no need to rediscover America when so many useful things are already available in public Helm repositories.&lt;/p&gt;
&lt;h2 id="updating-an-application"&gt;Updating an application
&lt;/h2&gt;&lt;p&gt;Sooner or later, you will change something in &lt;code&gt;values.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Instead of uninstalling and installing the application again, upgrade the existing release:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm upgrade headlamp headlamp/headlamp &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --namespace headlamp &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -f values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Helm applies the new configuration while preserving release history. If something goes sideways, that history becomes useful very quickly.&lt;/p&gt;
&lt;h2 id="removing-applications"&gt;Removing applications
&lt;/h2&gt;&lt;p&gt;When an application outlives its usefulness — and why is it always Headlamp? — removing it is just as easy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm uninstall my-application
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If it was installed in a specific namespace, add it explicitly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm uninstall my-application &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --namespace namespace-name
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="listing-installed-releases"&gt;Listing installed releases
&lt;/h2&gt;&lt;p&gt;This command shows releases from the current namespace:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which is technically correct and practically disappointing most of the time.&lt;/p&gt;
&lt;p&gt;Usually, this is what you want:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm list -A
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It lists releases across all namespaces.&lt;/p&gt;
&lt;p&gt;And that is the nice thing about Helm: the basic workflow stays small.&lt;/p&gt;
&lt;p&gt;Add a repository, inspect the values, install a release, upgrade it when needed, list what is running, remove what is no longer useful. Underneath, Kubernetes still does Kubernetes things, but at least you do not have to hand-write every single manifest like it is some YAML-based monastic ritual.&lt;/p&gt;</description></item><item><title>2026-06-29</title><link>https://lvajxi03.github.io/posts/2026-06-29/</link><pubDate>Mon, 29 Jun 2026 19:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-29/</guid><description>&lt;h1 id="buffer-overflow"&gt;Buffer Overflow
&lt;/h1&gt;&lt;p&gt;Today we begin in a slightly unusual place.&lt;/p&gt;
&lt;h2 id="interrupting-the-current-operation"&gt;Interrupting the Current Operation
&lt;/h2&gt;&lt;p&gt;If there is one key binding worth remembering before you even open your first file in Emacs, it is this one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;C-g
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;While Emacs is executing some more complicated Lisp code, it may occasionally stop responding to user commands and give the impression that it has frozen.&lt;/p&gt;
&lt;p&gt;Nothing could be further from the truth.&lt;/p&gt;
&lt;p&gt;Emacs is not dead. It is merely busy executing that unfortunate piece of Lisp.&lt;/p&gt;
&lt;p&gt;There are two possible approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;wait patiently until the Lisp interpreter finishes its work,&lt;/li&gt;
&lt;li&gt;or interrupt it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The latter is usually more practical.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C-g&lt;/code&gt; interrupts the current operation. It works when Emacs is running a long command, waiting for minibuffer input, or otherwise pretending not to hear you.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;M-x keyboard-quit&lt;/code&gt; is the function bound to &lt;code&gt;C-g&lt;/code&gt; by default. Calling it manually is usually pointless: when Emacs is busy, you cannot invoke it, and when Emacs is not busy, there is nothing to interrupt. Still, the name is worth knowing if you ever decide to remap keys.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you forget everything else from this section, remember &lt;code&gt;C-g&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When something goes wrong, Emacs will usually listen.&lt;/p&gt;
&lt;p&gt;Lisp programmers are less predictable.&lt;/p&gt;
&lt;h1 id="what-does-this-key-do"&gt;What Does This Key Do?
&lt;/h1&gt;&lt;p&gt;Let us continue.&lt;/p&gt;
&lt;p&gt;Emacs contains an excellent built-in help system. One of its most useful features is the ability to ask:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;What command is this key binding actually running?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;To find out, use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;M-x describe-key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;or, more conveniently, &lt;code&gt;C-h k&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Emacs will then ask you to press a key sequence, mouse button, or menu item.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;C-h k C-p
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;opens a help buffer describing the command bound to &lt;code&gt;C-p&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="describe key" class="gallery-image" data-flex-basis="3381px" data-flex-grow="1408" height="45" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-29/emacs-1.png" width="634"&gt;&lt;/p&gt;
&lt;p&gt;A shortened version looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;C-p runs the command previous-line.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;It is bound to C-p and &amp;lt;up&amp;gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Move cursor vertically up ARG lines.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;This function is for interactive use only;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;in Lisp code use `forward-line&amp;#39; with negative argument instead.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Even this small fragment tells us quite a lot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which function is bound to the key,&lt;/li&gt;
&lt;li&gt;where the function comes from,&lt;/li&gt;
&lt;li&gt;which other keys invoke the same command,&lt;/li&gt;
&lt;li&gt;what the command does,&lt;/li&gt;
&lt;li&gt;whether it is meant for interactive use,&lt;/li&gt;
&lt;li&gt;and sometimes which related commands or variables are worth knowing about.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is one of Emacs&amp;rsquo; great strengths.&lt;/p&gt;
&lt;p&gt;The program is not merely configurable. It is also unusually good at explaining itself.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Bookmark:&lt;/strong&gt; if you have reached this point and started yawning, leave this article open. Tomorrow you will know exactly where to resume reading.&lt;/p&gt;
&lt;p&gt;You do not have to thank me.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h1 id="what-does-this-function-do"&gt;What Does This Function Do?
&lt;/h1&gt;&lt;p&gt;The same idea works in the other direction.&lt;/p&gt;
&lt;p&gt;If you know the name of a function and want to inspect it, use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;M-x describe-function&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;or simply &lt;code&gt;C-h f&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;C-h f find-file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;opens a help buffer for the function used to visit files.&lt;/p&gt;
&lt;p&gt;&lt;img alt="describe function" class="gallery-image" data-flex-basis="1882px" data-flex-grow="784" height="32" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-29/emacs-2.png" width="251"&gt;&lt;/p&gt;
&lt;p&gt;A shortened fragment looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;find-file is an interactive function in `files.el`.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;It is bound to &amp;lt;open&amp;gt; and C-x C-f.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Edit file FILENAME.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Switch to a buffer visiting file FILENAME, creating one if none already exists.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You can visit files on remote machines by specifying something like:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/ssh:SOME_REMOTE_MACHINE:FILE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You can also visit local files as a different user by specifying:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/sudo::FILE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;To visit a file without conversion and without automatically choosing
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;a major mode, use M-x find-file-literally.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is why reading Emacs help buffers is often surprisingly useful.&lt;/p&gt;
&lt;p&gt;Even a command as ordinary as &lt;code&gt;find-file&lt;/code&gt; turns out to do more than merely open files.&lt;/p&gt;
&lt;p&gt;Several useful details appear immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wildcards&lt;/strong&gt; — file name patterns can be expanded, allowing multiple files to be opened at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote files&lt;/strong&gt; — paths such as &lt;code&gt;/ssh:some-host:some-file&lt;/code&gt; open files over SSH.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Files as another user&lt;/strong&gt; — paths such as &lt;code&gt;/sudo::some-file&lt;/code&gt; allow editing files with elevated privileges.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last option is useful for editing configuration files that require administrative access.&lt;/p&gt;
&lt;p&gt;Other possible uses are left to the reader&amp;rsquo;s imagination and the local security department.&lt;/p&gt;
&lt;p&gt;At this point, Dear Reader, I assume you have already typed a single &lt;code&gt;*&lt;/code&gt; in some directory full of random files and discovered that Emacs can open rather more things than expected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;images,&lt;/li&gt;
&lt;li&gt;PDFs,&lt;/li&gt;
&lt;li&gt;OpenOffice and LibreOffice documents,&lt;/li&gt;
&lt;li&gt;documents produced by that office suite which now apparently wishes to be known as a co-pilot,&lt;/li&gt;
&lt;li&gt;EPUB and MOBI files,&lt;/li&gt;
&lt;li&gt;archives,&lt;/li&gt;
&lt;li&gt;and many other formats.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It probably will not play your movies, but it can certainly inspect archives.&lt;/p&gt;
&lt;p&gt;What to do with all those joyfully opened files will be discussed a little later.&lt;/p&gt;
&lt;h1 id="buffers-and-frames"&gt;Buffers and Frames
&lt;/h1&gt;&lt;p&gt;Dividing the working area of a frame into two or more windows was invented mainly so that several buffers could be displayed side by side.&lt;/p&gt;
&lt;p&gt;Anyone who writes code probably understands the value of that immediately.&lt;/p&gt;
&lt;p&gt;The King of Editors can split a frame both horizontally and vertically. The resulting windows can be split again, although not forever. Once a window becomes too small, Emacs will politely refuse and display an appropriate message.&lt;/p&gt;
&lt;p&gt;Useful key bindings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C-x 2&lt;/code&gt; — split the current window horizontally&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-x 3&lt;/code&gt; — split the current window vertically&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-x 1&lt;/code&gt; — delete all other windows and return to a single-window layout&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-x 0&lt;/code&gt; — delete the current window&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-x 5 2&lt;/code&gt; — create a new frame&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-x 5 1&lt;/code&gt; — delete other frames&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-x 5 0&lt;/code&gt; — delete the current frame&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-x o&lt;/code&gt; — move focus to the next window&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-x S-o&lt;/code&gt; — move focus to the previous window&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A very important detail:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;C-x 1&lt;/code&gt; removes window splits, but it does not kill any buffers. The buffers remain open; only one of them is currently visible.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Arbitrary frame splitting" class="gallery-image" data-flex-basis="222px" data-flex-grow="92" height="870" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-29/emacs-3.png" srcset="https://lvajxi03.github.io/posts/2026-06-29/emacs-3_hu_a4277411b90ad2d4.png 800w, https://lvajxi03.github.io/posts/2026-06-29/emacs-3.png 806w" width="806"&gt;&lt;/p&gt;
&lt;h1 id="working-with-multiple-buffers"&gt;Working with Multiple Buffers
&lt;/h1&gt;&lt;p&gt;Emacs can open as many buffers as available memory allows.&lt;/p&gt;
&lt;p&gt;Because of that, there is usually no point in starting another Emacs instance every time you want to open another file. When you need another file, open it in the same Emacs session.&lt;/p&gt;
&lt;p&gt;Emacs was designed for working with many buffers at once. Running several separate instances is possible, but in everyday use it is rarely necessary.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Yes, this is the &amp;ldquo;a little later&amp;rdquo; mentioned above.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;If a file is no longer needed, kill its buffer with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;C-x k
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By default, Emacs asks what to do with buffers that contain unsaved changes.&lt;/p&gt;
&lt;p&gt;You can move to the previous or next buffer with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C-x Left&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-x Right&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also switch directly to a named buffer using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;C-x b
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the buffer visits a file, its name is usually the same as the file name.&lt;/p&gt;
&lt;p&gt;If you do not remember the exact name, press &lt;code&gt;TAB&lt;/code&gt;. Emacs will display possible completions in a temporary window. You can also start typing and let completion narrow the list.&lt;/p&gt;
&lt;p&gt;Completion, selection, and minibuffer behavior are, of course, topics for another volume of the Emacs saga.&lt;/p&gt;
&lt;p&gt;To list all buffers, use:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;C-x C-b
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This opens a buffer containing the buffer list. You can move into that buffer and choose one of the listed entries.&lt;/p&gt;
&lt;p&gt;The buffer list does not automatically refresh every time a new buffer is created. This makes sense once you realize that the buffer list is itself just another buffer.&lt;/p&gt;
&lt;p&gt;Press:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;g
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;to refresh it.&lt;/p&gt;
&lt;p&gt;What else can be done inside the buffer list?&lt;/p&gt;
&lt;p&gt;As homework, press:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and read the help page.&lt;/p&gt;
&lt;h1 id="scrolling-another-window"&gt;Scrolling Another Window
&lt;/h1&gt;&lt;p&gt;One of the more useful Emacs features is the ability to scroll the contents of another window while continuing to work in the current one.&lt;/p&gt;
&lt;p&gt;No switching back and forth.&lt;/p&gt;
&lt;p&gt;No mouse.&lt;/p&gt;
&lt;p&gt;No unnecessary ceremony.&lt;/p&gt;
&lt;p&gt;If you are writing in one window and need to inspect something above or below in another window, Emacs has you covered.&lt;/p&gt;
&lt;p&gt;This is especially useful when both windows display different parts of the same file and you want to check what regrettable code you wrote earlier.&lt;/p&gt;
&lt;p&gt;Useful key bindings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C-M-v&lt;/code&gt; — scroll the other window down&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C-M-S-v&lt;/code&gt; — scroll the other window up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alternative bindings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;M-PageDown&lt;/code&gt; — scroll the other window down&lt;/li&gt;
&lt;li&gt;&lt;code&gt;M-PageUp&lt;/code&gt; — scroll the other window up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is one of those features that sounds minor until you start using it. After a few days, working without it feels unnecessarily clumsy.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;A note that will age badly and is not really on topic:&lt;/strong&gt; at the moment, southern Poland is experiencing temperatures that feel entirely incompatible with human life. Some people of questionable judgment still insist that climate change does not exist, and that trains in the seventeenth century were also stuffy.&lt;/p&gt;
&lt;p&gt;Drink enough water.&lt;/p&gt;
&lt;p&gt;This is not a joke.&lt;/p&gt;
&lt;p&gt;Otherwise, the seventeenth-century train will arrive for you personally.&lt;/p&gt;

 &lt;/blockquote&gt;</description></item><item><title>2026-06-28</title><link>https://lvajxi03.github.io/posts/2026-06-28/</link><pubDate>Sun, 28 Jun 2026 16:08:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-28/</guid><description>&lt;h1 id="listbox-anyone"&gt;Listbox, anyone?
&lt;/h1&gt;&lt;p&gt;Many popular UI toolkits have their listbox control implemented to be as simple in use as possible. The most common operations are: creating the control, adding the data rows, deleting and editing data, handling the selection. Almost all toolkits provide single functions or suitable methods to realize above operations.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Gtk+&lt;/code&gt; is quite different: it requires more abstract way of thinking and additional objects to be created, but the lists and trees are more flexible and elegant.&lt;/p&gt;
&lt;h1 id="storing-the-data"&gt;Storing the data
&lt;/h1&gt;&lt;p&gt;Before we can display a single string, let&amp;rsquo;s first earn a PhD in data models.&lt;/p&gt;
&lt;p&gt;Unlike the the other toolkits, the gtk+ list allows to display a few values per a row. Gtk+ list also distinguishes between the types of data, while other lists treat its row data as a single string.&lt;/p&gt;
&lt;p&gt;In fact, the &lt;code&gt;TreeView&lt;/code&gt; widget will be used to display elements. The model lets the display widget know about data types to be used in the control, it also determines the type of the control (if it should be a list or a tree). Besides, the model is used to store all the data (it is common to find a &amp;ldquo;store&amp;rdquo; term instead of &amp;ldquo;model&amp;rdquo; in various papers or in someone&amp;rsquo;s code)&lt;/p&gt;
&lt;p&gt;A store/model can be created by calling a proper constructor with at least one data type (to be used in the list) as argument:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;store &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ListStore(type1, &lt;span style="color:#f92672"&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;store &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ListStore(int, str, str)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Created store will contain one integer value and two strings per row. Although pygtk reference recommends to use build-in GObject types (for instance, gobject.TYPE_INT and gobject.TYPE_STRING instead of int and str respectively), python built-in types are also allowed (GObject names really matter in C or C++)&lt;/p&gt;
&lt;h1 id="adding-the-data-rows"&gt;Adding the data rows
&lt;/h1&gt;&lt;p&gt;The method append from Gtk.ListStore class allows to append one row at the end of the store. Sequence with all values should be passed as a parameter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;store&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([val1, &lt;span style="color:#f92672"&gt;...&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;store&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;str1&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;str2&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="columns-and-renderers"&gt;Columns and renderers
&lt;/h1&gt;&lt;p&gt;Control widget should know about columns it should display, their order and how to present data from the store. Gtk+ provides the column widget (Gtk.TreeViewColumn) to manage displaying the cells of data and renderer widgets (Gtk.CellRenderer and its derivatives) to realize rendering of particular cells.&lt;/p&gt;
&lt;p&gt;Column widget is usually created by calling a constructor of &lt;code&gt;Gtk.TreeViewColumn&lt;/code&gt; with a header title passed as the argument:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(title)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(&lt;span style="color:#e6db74"&gt;&amp;#34;Column header&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Column header is visible by default — passing an empty string to the constructor will not get rid of it.&lt;/p&gt;
&lt;p&gt;Cell renderer should be one of &lt;code&gt;Gtk.CellRendererAccel&lt;/code&gt;, &lt;code&gt;Gtk.CellRendererCombo&lt;/code&gt;, &lt;code&gt;Gtk.CellRendererPixbuf&lt;/code&gt;, &lt;code&gt;Gtk.CellRendererProgress&lt;/code&gt;, &lt;code&gt;Gtk.CellRendererSpin&lt;/code&gt;, &lt;code&gt;Gtk.CellRendererText&lt;/code&gt;, &lt;code&gt;Gtk.CellRendererToggle&lt;/code&gt;. The renderer object is usually created by calling the constructor with no parameters&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;renderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Renderers should be added to the column widgets: pack_start and pack_end methods are used to do the packing.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(renderer)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_end(renderer)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column1 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(&lt;span style="color:#e6db74"&gt;&amp;#34;Column1&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;renderer1 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;renderer2 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;renderer3 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(renderer1)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(renderer2)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_end(renderer3)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The column widget must know which data column from the model should be connected to any of these renderers. This could be done by calling set_attributes method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(renderer, &lt;span style="color:#f92672"&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All optional parameters are &amp;ldquo;key = value&amp;rdquo; pairs, where key is the data type and value is number of column in the store&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(renderer1, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(renderer2, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;column1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(renderer3, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="treeview-control"&gt;TreeView control
&lt;/h1&gt;&lt;p&gt;The last step is to create a control and add all columns. Constructor of the control takes the store as first parameter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;listControl &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeView(model&lt;span style="color:#f92672"&gt;=&lt;/span&gt;store)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The method append_column adds given column to the tree/list control:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;listControl&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append_column(column)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Summary: at least, four objects should be created to have the tree/list control working: a model which determines types of data and stores all the rows, a renderer, which takes care of displaying the cells, a column, which glues together model columns and suitable renderers and a treeview control itself.&lt;/p&gt;
&lt;h1 id="working-examples"&gt;Working examples
&lt;/h1&gt;&lt;p&gt;Following examples illustrate the creation and displaying of the lists.&lt;/p&gt;
&lt;h2 id="very-simple-list-control"&gt;Very simple list control
&lt;/h2&gt;&lt;p&gt;The simplest list control contains only one column and displays only one value per row&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; gi
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gi&lt;span style="color:#f92672"&gt;.&lt;/span&gt;require_version(&lt;span style="color:#e6db74"&gt;&amp;#34;Gtk&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;3.0&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; gi.repository &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Gtk
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ListBoxViewer1&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;destroyCallback&lt;/span&gt;(self, widget, data&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;main_quit()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;__init__&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ListStore(str)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 1&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 2&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 3&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 4&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 5&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(&lt;span style="color:#e6db74"&gt;&amp;#34;Sample column&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mainWindow &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Window(type&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;WindowType&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TOPLEVEL)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mainWindow&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_title(&lt;span style="color:#e6db74"&gt;&amp;#39;ListBoxViewer1&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listBox &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeView(model&lt;span style="color:#f92672"&gt;=&lt;/span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listBox&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append_column(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mainWindow&lt;span style="color:#f92672"&gt;.&lt;/span&gt;add(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listBox)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mainWindow&lt;span style="color:#f92672"&gt;.&lt;/span&gt;connect(&lt;span style="color:#e6db74"&gt;&amp;#39;delete_event&amp;#39;&lt;/span&gt;, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;destroyCallback)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mainWindow&lt;span style="color:#f92672"&gt;.&lt;/span&gt;show_all()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; __name__ &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lbv1 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ListBoxViewer1()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;main()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This example covers the theory of creating listboxes with additional routines necessary to run minimal gtk+ program. Additional method is added to provide termination of this example when its window has been closed.&lt;/p&gt;
&lt;p&gt;All list-related routines are explained below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ListStore(str)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 1&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 2&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 3&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 4&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 5&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The model is created: it can hold one string value per a row. Then it is populated by 5 rows.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Text renderer is created.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(&lt;span style="color:#e6db74"&gt;&amp;#34;Sample column&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A column is created. Then the renderer is packed into the column, and the column is informed which column and what type of data from the store will be used.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listBox &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeView(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listBox&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append_column(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Main widget is created. The column is appended to the list.&lt;/p&gt;
&lt;p&gt;It looks like the following:&lt;/p&gt;
&lt;p&gt;&lt;img alt="ListboxViewer 1" class="gallery-image" data-flex-basis="224px" data-flex-grow="93" height="333" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-28/listbox-1.png" width="312"&gt;&lt;/p&gt;
&lt;p&gt;Explanation of the next examples contains only the routines related to list control; other routines did not change if not mentioned.&lt;/p&gt;
&lt;h2 id="two-values-per-row"&gt;Two values per row
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ListStore(str, int)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 1&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;18000&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 2&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1500&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 3&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;900&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 4&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 5&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the store holds a string an integer pair per a row.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(&lt;span style="color:#e6db74"&gt;&amp;#34;Sample column&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_property(&lt;span style="color:#e6db74"&gt;&amp;#39;xalign&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A column and two cell renderers are created. The set_property routine with xalign property to be set tells the renderer to align right the values in the row. Value of 0.0 means left alignment, 0.5 centers the text and 1.0 means right alignment.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer, &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The two renderers are packed into the column. The string value renderer will expand the cell if possible. Proper renderers are assigned to proper data columns from the store.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listBox &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeView(store&lt;span style="color:#f92672"&gt;=&lt;/span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listBox&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append_column(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The control is created and the column is appended to the control.&lt;/p&gt;
&lt;p&gt;The following picture shows the result of this example:&lt;/p&gt;
&lt;p&gt;&lt;img alt="ListboxViewer 2" class="gallery-image" data-flex-basis="265px" data-flex-grow="110" height="240" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-28/listbox-2.png" width="265"&gt;&lt;/p&gt;
&lt;h2 id="two-columns-in-the-control"&gt;Two columns in the control
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(&lt;span style="color:#e6db74"&gt;&amp;#34;Ascii text&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(&lt;span style="color:#e6db74"&gt;&amp;#34;Numeric value&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_property(&lt;span style="color:#e6db74"&gt;&amp;#39;xalign&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer, &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every column packs one of the renderes. It looks like the following:&lt;/p&gt;
&lt;p&gt;&lt;img alt="ListboxViewer 3" class="gallery-image" data-flex-basis="222px" data-flex-grow="92" height="219" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-28/listbox-3.png" width="203"&gt;&lt;/p&gt;
&lt;h2 id="adding-pixmaps-to-the-list"&gt;Adding pixmaps to the list
&lt;/h2&gt;&lt;p&gt;Now, creating the store is like below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sheetIcon &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;gdk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pixbuf_new_from_file(&lt;span style="color:#e6db74"&gt;&amp;#34;sheet.xpm&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ListStore(str, int, Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;gdk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Pixbuf)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 1&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;18000&lt;/span&gt;, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sheetIcon])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 2&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1500&lt;/span&gt;, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sheetIcon])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 3&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;900&lt;/span&gt;, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sheetIcon])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 4&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sheetIcon])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listStore&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append([&lt;span style="color:#e6db74"&gt;&amp;#34;String 5&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sheetIcon])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The pixbuf is added in every row. Of course, it may be different in every row. This is only the example.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iconRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererPixbuf()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The icon requires a special kind of cell renderer.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iconRenderer, &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iconRenderer, pixbuf &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pixbuf cell renderer is now packed into the first column together with the text renderer.&lt;/p&gt;
&lt;p&gt;It looks like the following:&lt;/p&gt;
&lt;p&gt;&lt;img alt="ListboxViewer 4" class="gallery-image" data-flex-basis="125px" data-flex-grow="52" height="465" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-28/listbox-4.png" width="244"&gt;&lt;/p&gt;
&lt;h2 id="display-the-same-values-twice-or-more-times"&gt;Display the same values twice or more times
&lt;/h2&gt;&lt;p&gt;Displaying more values per row is achieved by multiplicating the renderes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(&lt;span style="color:#e6db74"&gt;&amp;#34;Ascii text&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeViewColumn(&lt;span style="color:#e6db74"&gt;&amp;#34;Numeric value&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer2 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer2 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer3 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iconRenderer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererPixbuf()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iconRenderer2 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Gtk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CellRendererPixbuf()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_property(&lt;span style="color:#e6db74"&gt;&amp;#39;xalign&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_property(&lt;span style="color:#e6db74"&gt;&amp;#39;xalign&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_property(&lt;span style="color:#e6db74"&gt;&amp;#39;xalign&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0.5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_property(&lt;span style="color:#e6db74"&gt;&amp;#39;xalign&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iconRenderer, &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer, &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer2, &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iconRenderer2, &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer3, &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pack_start(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer2, &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iconRenderer, pixbuf &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn1&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer2, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;numRenderer3, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;textRenderer2, text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;listColumn2&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_attributes(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iconRenderer2, pixbuf &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Also, the left, right and center alignment of text is shown.&lt;/p&gt;
&lt;p&gt;It looks like the following:&lt;/p&gt;
&lt;p&gt;&lt;img alt="ListboxViewer 5" class="gallery-image" data-flex-basis="209px" data-flex-grow="87" height="511" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-28/listbox-5.png" width="447"&gt;&lt;/p&gt;</description></item><item><title>2026-06-26</title><link>https://lvajxi03.github.io/posts/2026-06-26/</link><pubDate>Fri, 26 Jun 2026 11:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-26/</guid><description>&lt;h1 id="why-do-we-need-a-registry"&gt;Why Do We Need a Registry?
&lt;/h1&gt;&lt;p&gt;Sooner or later, every Kubernetes cluster starts running applications that were built locally.&lt;/p&gt;
&lt;p&gt;At first, using Docker Hub seems perfectly reasonable.&lt;/p&gt;
&lt;p&gt;Then the first custom application appears.&lt;/p&gt;
&lt;p&gt;Then another.&lt;/p&gt;
&lt;p&gt;Eventually every deployment references images that exist only on your workstation.&lt;/p&gt;
&lt;p&gt;Clearly, this is not a sustainable solution.&lt;/p&gt;
&lt;p&gt;A private container registry solves this problem by providing a permanent home for your own images. Since the home lab already has plenty of storage available, hosting the registry locally is both the simplest and the fastest option.&lt;/p&gt;
&lt;h1 id="persistentvolume-and-persistentvolumeclaim"&gt;PersistentVolume and PersistentVolumeClaim
&lt;/h1&gt;&lt;p&gt;One of the original design goals of Kubernetes was that Pods should be ephemeral.&lt;/p&gt;
&lt;p&gt;Applications come and go.&lt;/p&gt;
&lt;p&gt;Containers are recreated.&lt;/p&gt;
&lt;p&gt;Nodes may disappear.&lt;/p&gt;
&lt;p&gt;Unfortunately, databases tend to have rather strong opinions about losing their contents.&lt;/p&gt;
&lt;p&gt;To bridge the gap between ephemeral workloads and persistent storage, Kubernetes introduced two API objects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PersistentVolume (PV)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PersistentVolumeClaim (PVC)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="persistentvolume"&gt;PersistentVolume
&lt;/h2&gt;&lt;p&gt;A &lt;strong&gt;PersistentVolume&lt;/strong&gt; represents an actual storage resource available inside the cluster.&lt;/p&gt;
&lt;p&gt;Unlike Pods, a PersistentVolume exists independently of any application using it. Deleting a Pod does not automatically remove the underlying storage.&lt;/p&gt;
&lt;p&gt;Think of it as the physical disk—or, more precisely, a piece of storage that Kubernetes knows how to manage.&lt;/p&gt;
&lt;h2 id="persistentvolumeclaim"&gt;PersistentVolumeClaim
&lt;/h2&gt;&lt;p&gt;A &lt;strong&gt;PersistentVolumeClaim&lt;/strong&gt; is exactly what its name suggests: a request for storage.&lt;/p&gt;
&lt;p&gt;Rather than asking for a specific disk, an application simply requests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a certain amount of space,&lt;/li&gt;
&lt;li&gt;an access mode,&lt;/li&gt;
&lt;li&gt;and, optionally, a storage class.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Pod never mounts a PersistentVolume directly. Instead, it references a PVC and lets Kubernetes handle the rest.&lt;/p&gt;
&lt;h2 id="binding"&gt;Binding
&lt;/h2&gt;&lt;p&gt;Whenever a PersistentVolumeClaim is created, Kubernetes searches for a suitable PersistentVolume that satisfies the requested parameters.&lt;/p&gt;
&lt;p&gt;If one is found, the claim becomes permanently bound to that volume.&lt;/p&gt;
&lt;p&gt;From the application&amp;rsquo;s point of view, none of this complexity exists.&lt;/p&gt;
&lt;p&gt;The Pod simply mounts the PVC as a directory inside the container.&lt;/p&gt;
&lt;h2 id="access-modes"&gt;Access Modes
&lt;/h2&gt;&lt;p&gt;Access modes describe &lt;strong&gt;how Kubernetes nodes are allowed to mount a volume&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Four access modes currently exist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ReadWriteOnce (RWO)&lt;/strong&gt; — the volume may be mounted for reading and writing by only &lt;strong&gt;one node&lt;/strong&gt; at a time. Multiple Pods may still use it, provided they all run on that node.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ReadWriteMany (RWX)&lt;/strong&gt; — the volume may be mounted simultaneously by multiple nodes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ReadOnlyMany (ROX)&lt;/strong&gt; — multiple nodes may mount the volume, but only for reading.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ReadWriteOncePod (RWOP)&lt;/strong&gt; — introduced more recently, this mode guarantees that exactly one Pod in the entire cluster has read-write access to the volume.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; the underlying storage backend must actually support the requested access mode. Kubernetes cannot magically turn a local SSD into a distributed filesystem.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h1 id="where-should-the-data-live"&gt;Where Should the Data Live?
&lt;/h1&gt;&lt;p&gt;By default, &lt;strong&gt;k3s&lt;/strong&gt; ships with the &lt;strong&gt;Local Path Provisioner&lt;/strong&gt;, which stores dynamically created PersistentVolumes under:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/var/lib/rancher/k3s/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Personally, I prefer keeping persistent application data somewhere more obvious, such as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/srv/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To do that, create the directory on every node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mkdir -p /srv/storage
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo chmod &lt;span style="color:#ae81ff"&gt;777&lt;/span&gt; /srv/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The permissions are intentionally generous. This is a home lab, not Fort Knox.&lt;/p&gt;
&lt;p&gt;Next, edit the &lt;code&gt;local-path-config&lt;/code&gt; ConfigMap:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl -n kube-system edit configmap local-path-config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The interesting part looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;data&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;config.json&lt;/span&gt;: |-&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;nodePathMap&amp;#34;:[
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;node&amp;#34;:&amp;#34;DEFAULT_PATH_FOR_NON_LISTED_NODES&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;paths&amp;#34;:[&amp;#34;/var/lib/rancher/k3s/storage&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Simply replace:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;paths&amp;#34;&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;:&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;/var/lib/rancher/k3s/storage&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;paths&amp;#34;&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;:&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;/srv/storage&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Newly created PersistentVolumes will now live there instead.&lt;/p&gt;
&lt;h1 id="deploying-the-registry"&gt;Deploying the Registry
&lt;/h1&gt;&lt;p&gt;The registry will be available as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;registry.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Since there is no local DNS server yet, every machine—including your workstation—should receive the following &lt;code&gt;/etc/hosts&lt;/code&gt; entry:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.24 registry.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The registry should preferably run on the most capable machine in the cluster.&lt;/p&gt;
&lt;p&gt;In my case, that is &lt;code&gt;iridium&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Assign it an appropriate label:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl label node iridium registry&lt;span style="color:#f92672"&gt;=&lt;/span&gt;true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The Deployment manifest below uses that label via a &lt;code&gt;nodeSelector&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At first glance, a registry seems like a simple application.&lt;/p&gt;
&lt;p&gt;Then you remember it needs persistent storage.&lt;/p&gt;
&lt;p&gt;And a Service.&lt;/p&gt;
&lt;p&gt;And an Ingress.&lt;/p&gt;
&lt;p&gt;And perhaps its own namespace.&lt;/p&gt;
&lt;p&gt;And a ConfigMap.&lt;/p&gt;
&lt;p&gt;So, in the end, it turns out to require exactly six Kubernetes objects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Namespace&lt;/li&gt;
&lt;li&gt;PersistentVolumeClaim&lt;/li&gt;
&lt;li&gt;ConfigMap&lt;/li&gt;
&lt;li&gt;Deployment&lt;/li&gt;
&lt;li&gt;Service&lt;/li&gt;
&lt;li&gt;Ingress&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Their complete definitions are shown below.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(YAML manifest unchanged.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Save everything as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;registry.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and deploy it using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl apply -f registry.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="interesting-bits"&gt;Interesting Bits
&lt;/h2&gt;&lt;p&gt;The PVC requests persistent storage:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;accessModes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;ReadWriteOnce&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The registry is pinned to the node labelled earlier:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;nodeSelector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;registry&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The ingress disables request size limits:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;nginx.ingress.kubernetes.io/proxy-body-size&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Without this annotation, pushing sufficiently large images eventually results in sadness, disappointment, and extensive arm waving.&lt;/p&gt;
&lt;p&gt;The registry configuration itself is stored in a ConfigMap rather than a long list of environment variables.&lt;/p&gt;
&lt;p&gt;Since the registry already expects a YAML configuration file, using a ConfigMap keeps the deployment considerably cleaner.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;This setup stores all registry data on the local filesystem of a single node. That is perfectly adequate for a home lab. Production environments usually rely on distributed storage instead.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h1 id="running-without-tls"&gt;Running Without TLS
&lt;/h1&gt;&lt;p&gt;If the registry temporarily runs over plain HTTP, Docker must be told to trust it.&lt;/p&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/etc/docker/daemon.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;containing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;insecure-registries&amp;#34;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;registry.lab.local&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Likewise, every k3s node should receive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/etc/rancher/k3s/registries.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;containing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;mirrors&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;registry.lab.local&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;endpoint&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;http://registry.lab.local&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, restart k3s:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo systemctl restart k3s &lt;span style="color:#f92672"&gt;||&lt;/span&gt; sudo systemctl restart k3s-agent
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Don&amp;rsquo;t forget to update the ConfigMap as well if you&amp;rsquo;re switching from HTTPS to HTTP.&lt;/p&gt;
&lt;h1 id="verification"&gt;Verification
&lt;/h1&gt;&lt;p&gt;The usual suspects come first:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get pods -n registry
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get pvc -n registry
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get ingress -n registry
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get svc -n registry
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl describe ingress registry -n registry
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get configmap -n registry
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the PVC reports:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;STATUS: Bound
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;everything is proceeding nicely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the PersistentVolume exists,&lt;/li&gt;
&lt;li&gt;the claim has been bound,&lt;/li&gt;
&lt;li&gt;the Pod is successfully using it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="smoke-test"&gt;Smoke Test
&lt;/h1&gt;&lt;p&gt;Let&amp;rsquo;s put the registry to work.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker pull php:8.4.22-zts-trixie
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker tag php:8.4.22-zts-trixie &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; registry.lab.local/php:8.4.22
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push registry.lab.local/php:8.4.22
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To make sure the image really came from the registry rather than your local cache, remove it first:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker image rm registry.lab.local/php:8.4.22
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker pull registry.lab.local/php:8.4.22
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, query the registry directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl https://registry.lab.local/v2/php/tags/list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which should return:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;php&amp;#34;&lt;/span&gt;,&lt;span style="color:#f92672"&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;:[&lt;span style="color:#e6db74"&gt;&amp;#34;8.4.22&amp;#34;&lt;/span&gt;]}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or list every repository:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -s https://registry.lab.local/v2/_catalog | jq
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Congratulations.&lt;/p&gt;
&lt;p&gt;Your registry is officially alive.&lt;/p&gt;
&lt;h1 id="registry-ui"&gt;Registry UI
&lt;/h1&gt;&lt;p&gt;A registry works perfectly well without a graphical interface.&lt;/p&gt;
&lt;p&gt;Humans, however, tend to appreciate one.&lt;/p&gt;
&lt;p&gt;The excellent &lt;strong&gt;Docker Registry UI&lt;/strong&gt; provides browsing, searching, and deleting images from a web browser.&lt;/p&gt;
&lt;p&gt;As before, add:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.24 registry-ui.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;to every &lt;code&gt;/etc/hosts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Deploy the accompanying Deployment, Service, and Ingress manifests (shown below), save them as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;registry-ui.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and apply them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl apply -f registry-ui.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The interface is intentionally simple.&lt;/p&gt;
&lt;p&gt;It won&amp;rsquo;t win any design awards, but it makes exploring your registry significantly more pleasant than remembering API endpoints.&lt;/p&gt;
&lt;h1 id="wrapping-up"&gt;Wrapping Up
&lt;/h1&gt;&lt;p&gt;At this point, the cluster has become largely self-sufficient.&lt;/p&gt;
&lt;p&gt;Applications can now be built locally, pushed into the private registry, and deployed without relying on Docker Hub.&lt;/p&gt;
&lt;p&gt;It may not look particularly impressive yet, but from now on every new service added to the cluster can remain entirely within the home lab.&lt;/p&gt;
&lt;p&gt;Which, after all, was the idea from the very beginning.&lt;/p&gt;</description></item><item><title>2026-06-25</title><link>https://lvajxi03.github.io/posts/2026-06-25/</link><pubDate>Thu, 25 Jun 2026 11:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-25/</guid><description>&lt;h1 id="installation"&gt;Installation
&lt;/h1&gt;&lt;p&gt;There isn&amp;rsquo;t much to say about installing Emacs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every respectable Linux distribution includes Emacs in its package repositories. Simply use your distribution&amp;rsquo;s package manager.&lt;/li&gt;
&lt;li&gt;The same is true for the BSD family.&lt;/li&gt;
&lt;li&gt;On macOS, you can install it using &lt;code&gt;Homebrew&lt;/code&gt;, &lt;code&gt;MacPorts&lt;/code&gt;, or download a native build from &lt;a class="link" href="https://emacsformacosx.com/" target="_blank" rel="noopener"
 &gt;https://emacsformacosx.com/&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;On Windows, simply download the official installer (&lt;code&gt;.exe&lt;/code&gt;) or the ZIP archive and extract it somewhere sensible.&lt;/li&gt;
&lt;li&gt;On Android, install the official &lt;code&gt;.apk&lt;/code&gt; package and allow applications from unknown sources in the system settings.&lt;/li&gt;
&lt;/ul&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;A small note:&lt;/strong&gt; if you have no interest in running the graphical version, most Unix-like systems also provide an &lt;code&gt;emacs-nox&lt;/code&gt; package.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Of course, you can always build Emacs from source.&lt;/p&gt;
&lt;p&gt;But why would you, when you could spend that hour starting another flame war on Twitter (formerly &amp;ldquo;X&amp;rdquo;), Reddit, or simply improving your thumb&amp;rsquo;s scrolling endurance on social media?&lt;/p&gt;
&lt;p&gt;Personally, I&amp;rsquo;d rather spend that hour weeding the vegetable garden.&lt;/p&gt;
&lt;p&gt;Usenet has long since passed into history, and no equally entertaining place for operating-system flame wars has ever truly replaced it.&lt;/p&gt;
&lt;h1 id="getting-started"&gt;Getting Started
&lt;/h1&gt;&lt;h2 id="launching-emacs"&gt;Launching Emacs
&lt;/h2&gt;&lt;p&gt;Launching the Finest Editor Known to Humankind is surprisingly simple.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On graphical desktops, Emacs should appear in your applications menu like any other program.&lt;/li&gt;
&lt;li&gt;On Windows, if you extracted the ZIP archive instead of running the installer, start &lt;code&gt;bin\runemacs.exe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you prefer a purely terminal-based experience, simply run:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;emacs -nw
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is where the Emacs journey truly begins.&lt;/p&gt;
&lt;p&gt;The first launch of an unmodified (&lt;em&gt;vanilla&lt;/em&gt;) Emacs under X11, Windows, or macOS opens a window featuring the GNU antelope, a welcome screen, and a few introductory notes.&lt;/p&gt;
&lt;p&gt;Curiously, what most desktop environments call a &lt;em&gt;window&lt;/em&gt;, Emacs calls a &lt;strong&gt;frame&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The reason is historical.&lt;/p&gt;
&lt;p&gt;Long before graphical user interfaces became commonplace, Emacs already knew how to divide a terminal screen into multiple independent editing areas called &lt;strong&gt;windows&lt;/strong&gt;. When graphical support arrived, the terminology remained unchanged: the top-level GUI object became a &lt;em&gt;frame&lt;/em&gt;, while the individual editing regions inside it continued to be called &lt;em&gt;windows&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Some traditions are simply too old to change.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Emacs main window" class="gallery-image" data-flex-basis="230px" data-flex-grow="96" data-title-escaped="Emacs main window" height="908" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-25/emacs-1.png" srcset="https://lvajxi03.github.io/posts/2026-06-25/emacs-1_hu_1d06649b4d62bc31.png 800w, https://lvajxi03.github.io/posts/2026-06-25/emacs-1.png 872w" title="Emacs main window" width="872"&gt;&lt;/p&gt;
&lt;h1 id="meet-the-interface"&gt;Meet the Interface
&lt;/h1&gt;&lt;p&gt;A default Emacs frame consists of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a menu bar,&lt;/li&gt;
&lt;li&gt;a toolbar (whose appearance ranges from &amp;ldquo;functional&amp;rdquo; to &amp;ldquo;deeply unfortunate&amp;rdquo;, although fortunately it can be hidden),&lt;/li&gt;
&lt;li&gt;one or more editing buffers,&lt;/li&gt;
&lt;li&gt;and the command line, better known as the &lt;strong&gt;minibuffer&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="buffers"&gt;Buffers
&lt;/h2&gt;&lt;p&gt;A &lt;strong&gt;buffer&lt;/strong&gt; is the place where text is edited.&lt;/p&gt;
&lt;p&gt;Unlike most editors, however, an Emacs buffer is not necessarily associated with a file.&lt;/p&gt;
&lt;p&gt;Buffers may contain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;source code,&lt;/li&gt;
&lt;li&gt;documentation,&lt;/li&gt;
&lt;li&gt;shell sessions,&lt;/li&gt;
&lt;li&gt;email,&lt;/li&gt;
&lt;li&gt;directory listings,&lt;/li&gt;
&lt;li&gt;Git status,&lt;/li&gt;
&lt;li&gt;help pages,&lt;/li&gt;
&lt;li&gt;or virtually anything else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This abstraction is one of the core ideas behind Emacs.&lt;/p&gt;
&lt;p&gt;Every buffer contains an editing area together with a &lt;strong&gt;mode line&lt;/strong&gt;, which displays useful information such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the buffer name,&lt;/li&gt;
&lt;li&gt;whether the buffer has unsaved changes,&lt;/li&gt;
&lt;li&gt;the current cursor position,&lt;/li&gt;
&lt;li&gt;and the active editing mode.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="major-modes"&gt;Major Modes
&lt;/h2&gt;&lt;p&gt;Most editing behavior in Emacs is controlled by &lt;strong&gt;major modes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A major mode typically corresponds to the type of document being edited.&lt;/p&gt;
&lt;p&gt;Whether you&amp;rsquo;re writing C code, XML, Markdown, Lisp, YAML, or something entirely your own, the selected mode determines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;syntax highlighting,&lt;/li&gt;
&lt;li&gt;indentation rules,&lt;/li&gt;
&lt;li&gt;available commands,&lt;/li&gt;
&lt;li&gt;keyboard shortcuts,&lt;/li&gt;
&lt;li&gt;and countless editor behaviors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Creating your own modes is also entirely possible, should inspiration strike.&lt;/p&gt;
&lt;h2 id="the-minibuffer"&gt;The Minibuffer
&lt;/h2&gt;&lt;p&gt;The &lt;strong&gt;minibuffer&lt;/strong&gt; serves as the communication channel between Emacs and its user.&lt;/p&gt;
&lt;p&gt;Emacs displays prompts and status messages there, while the user enters commands, filenames, search patterns, and countless other parameters.&lt;/p&gt;
&lt;p&gt;The minibuffer supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;command history,&lt;/li&gt;
&lt;li&gt;TAB completion,&lt;/li&gt;
&lt;li&gt;and context-sensitive suggestions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It may look like a single line of text, but it is one of the most heavily used parts of Emacs.&lt;/p&gt;
&lt;h2 id="windows-and-frames"&gt;Windows and Frames
&lt;/h2&gt;&lt;p&gt;A frame may be divided into multiple &lt;strong&gt;windows&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Each window displays a buffer.&lt;/p&gt;
&lt;p&gt;Interestingly, multiple windows may display the very same buffer simultaneously, each with its own cursor position and viewport.&lt;/p&gt;
&lt;p&gt;Only the minibuffer is shared across the entire frame.&lt;/p&gt;
&lt;h1 id="keys-and-commands"&gt;Keys and Commands
&lt;/h1&gt;&lt;p&gt;Emacs documentation uses a notation that initially looks mysterious but quickly becomes second nature.&lt;/p&gt;
&lt;p&gt;The most common forms are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C-x&lt;/strong&gt; — hold the &lt;strong&gt;Control&lt;/strong&gt; key while pressing &lt;code&gt;x&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x C-f&lt;/strong&gt; — execute the first key sequence, release the regular keys (you may keep holding Control if you like), then execute the second.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M-x&lt;/strong&gt; — hold the &lt;strong&gt;Meta&lt;/strong&gt; key. On most PC keyboards this is simply the left &lt;strong&gt;Alt&lt;/strong&gt; key. I strongly recommend getting used to the left Alt immediately, since the right one is often reserved for national characters or other platform-specific input methods.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-M-x&lt;/strong&gt; — press both Control and Meta together.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S-x&lt;/strong&gt; — press Shift together with the specified key.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, one notation deserves its own paragraph.&lt;/p&gt;
&lt;h2 id="m-x"&gt;M-x
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;M-x&lt;/code&gt; executes an Emacs command.&lt;/p&gt;
&lt;p&gt;Press &lt;code&gt;M-x&lt;/code&gt;, release the keys, type the command name into the minibuffer, and press Enter.&lt;/p&gt;
&lt;p&gt;Almost everything Emacs can do—apart from simply inserting text—is ultimately implemented as a Lisp function.&lt;/p&gt;
&lt;p&gt;Some of those functions appear in menus.&lt;/p&gt;
&lt;p&gt;Some are bound to keyboard shortcuts.&lt;/p&gt;
&lt;p&gt;All of them can be executed directly via &lt;code&gt;M-x&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;choosing &lt;strong&gt;File → Open&amp;hellip;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;pressing &lt;code&gt;C-x C-f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;executing &lt;code&gt;M-x find-file&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;all invoke exactly the same underlying function.&lt;/p&gt;
&lt;p&gt;&lt;img alt="M-x find-file" class="gallery-image" data-flex-basis="335px" data-flex-grow="139" data-title-escaped="M-x find-file" height="630" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-25/emacs-2.png" srcset="https://lvajxi03.github.io/posts/2026-06-25/emacs-2_hu_9996e68af439c3f.png 800w, https://lvajxi03.github.io/posts/2026-06-25/emacs-2.png 881w" title="M-x find-file" width="881"&gt;&lt;/p&gt;
&lt;h1 id="useful-keyboard-shortcuts"&gt;Useful Keyboard Shortcuts
&lt;/h1&gt;&lt;p&gt;The following commands quickly become second nature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C-x C-c&lt;/strong&gt; — exit Emacs. Generally considered a questionable life decision, although shutting down the computer is usually accepted as a valid excuse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x C-f&lt;/strong&gt; — open an existing file or create a new one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x C-s&lt;/strong&gt; — save the current buffer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x C-w&lt;/strong&gt; — save the current buffer under a different name.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x k&lt;/strong&gt; — kill (close) the current buffer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x Left&lt;/strong&gt; — previous buffer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x Right&lt;/strong&gt; — next buffer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x C-b&lt;/strong&gt; — list all buffers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x h&lt;/strong&gt; — select the entire buffer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="editing-text"&gt;Editing Text
&lt;/h1&gt;&lt;p&gt;Unlike some other editors, Emacs does not have separate &amp;ldquo;insert&amp;rdquo; and &amp;ldquo;command&amp;rdquo; modes.&lt;/p&gt;
&lt;p&gt;Open a file.&lt;/p&gt;
&lt;p&gt;Start typing.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all there is to it.&lt;/p&gt;
&lt;p&gt;Modern keyboards provide cursor keys, Home, End, Page Up, and Page Down, and Emacs fully supports them.&lt;/p&gt;
&lt;p&gt;For historical reasons—and because terminals were not always so well equipped—traditional Emacs key bindings are also available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C-a&lt;/strong&gt; — beginning of line&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-e&lt;/strong&gt; — end of line&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-f&lt;/strong&gt; — forward one character&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-b&lt;/strong&gt; — backward one character&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-p&lt;/strong&gt; — previous line&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-n&lt;/strong&gt; — next line&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M-v&lt;/strong&gt; — previous page&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-v&lt;/strong&gt; — next page&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M-w&lt;/strong&gt; — copy selected text&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-w&lt;/strong&gt; — cut selected text&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-y&lt;/strong&gt; — paste&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M-y&lt;/strong&gt; — immediately after &lt;code&gt;C-y&lt;/code&gt;, cycle through previous clipboard entries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-k&lt;/strong&gt; — kill text from the cursor to the end of the line. Pressing it twice at the beginning of a line removes the entire line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-x h&lt;/strong&gt; — select the whole buffer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C-_&lt;/strong&gt; (or &lt;code&gt;C-x u&lt;/code&gt;) — undo, with unlimited history.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Graphical Emacs naturally supports selecting text with the mouse.&lt;/p&gt;
&lt;p&gt;Keyboard users, however, can set the beginning of a selection using &lt;code&gt;C-SPC&lt;/code&gt;, move the cursor to the desired position, and then perform any operation on the selected region.&lt;/p&gt;
&lt;p&gt;It is one of those habits that soon becomes completely automatic.&lt;/p&gt;
&lt;p&gt;One unexpected benefit of learning Emacs is that the keyboard shortcuts tend to follow you.&lt;/p&gt;
&lt;p&gt;Bash, the Python REPL, GDB, and many other command-line programs borrow their line-editing behavior from GNU Readline, whose default key bindings are deliberately Emacs-like.&lt;/p&gt;
&lt;p&gt;Even if you eventually decide that Emacs is not your editor of choice, the time spent learning &lt;code&gt;C-a&lt;/code&gt;, &lt;code&gt;C-e&lt;/code&gt;, &lt;code&gt;C-k&lt;/code&gt;, or &lt;code&gt;M-f&lt;/code&gt; will continue to pay dividends every time you open a terminal.&lt;/p&gt;
&lt;p&gt;In fact, many people use Emacs key bindings every day without realizing where they originally came from.&lt;/p&gt;</description></item><item><title>2026-06-24</title><link>https://lvajxi03.github.io/posts/2026-06-24/</link><pubDate>Wed, 24 Jun 2026 11:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-24/</guid><description>&lt;h1 id="k3s-ingress"&gt;k3s: Ingress
&lt;/h1&gt;&lt;p&gt;What exactly is an Ingress?&lt;/p&gt;
&lt;p&gt;In the Kubernetes ecosystem, an &lt;strong&gt;Ingress&lt;/strong&gt; is an API object that manages external access to applications running inside a cluster. It acts as an intelligent HTTP and HTTPS router, eliminating the need to create a separate load balancer for every service.&lt;/p&gt;
&lt;h2 id="how-does-ingress-work"&gt;How Does Ingress Work?
&lt;/h2&gt;&lt;p&gt;Traditional service exposure methods, such as &lt;code&gt;NodePort&lt;/code&gt; or &lt;code&gt;LoadBalancer&lt;/code&gt;, typically require assigning an external IP address to each service. This quickly becomes tedious and difficult to manage as the cluster grows.&lt;/p&gt;
&lt;p&gt;Ingress centralizes external access and operates at Layer 7 (the application layer) of the OSI model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single entry point&lt;/strong&gt; — only one public IP address needs to be exposed to the outside world.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule-based routing&lt;/strong&gt; — traffic is forwarded to the appropriate service based on the requested URL path (for example, &lt;code&gt;my.domain/something&lt;/code&gt;) or hostname (for example, &lt;code&gt;something.my.domain&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSL/TLS termination&lt;/strong&gt; — HTTPS certificates can be handled centrally, removing that responsibility from individual applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="ingress-vs-ingress-controller"&gt;Ingress vs. Ingress Controller
&lt;/h2&gt;&lt;p&gt;This distinction is important.&lt;/p&gt;
&lt;p&gt;An Ingress object itself is merely a collection of routing rules.&lt;/p&gt;
&lt;p&gt;For those rules to have any effect, the cluster must run an &lt;strong&gt;Ingress Controller&lt;/strong&gt; — a reverse proxy responsible for processing incoming traffic and applying the rules defined in Ingress manifests.&lt;/p&gt;
&lt;p&gt;Popular examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NGINX Ingress Controller&lt;/li&gt;
&lt;li&gt;Traefik&lt;/li&gt;
&lt;li&gt;HAProxy Ingress&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This guide uses the &lt;strong&gt;NGINX Ingress Controller&lt;/strong&gt;.&lt;/p&gt;
&lt;h1 id="domain-names"&gt;Domain Names
&lt;/h1&gt;&lt;p&gt;Since the cluster will eventually host various web applications, it makes sense to identify services by DNS names rather than IP addresses.&lt;/p&gt;
&lt;p&gt;The examples below use:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;as the local domain.&lt;/p&gt;
&lt;p&gt;This allows services to be accessed using names such as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;grafana.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;argocd.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;whoami.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which is considerably more pleasant than remembering port numbers.&lt;/p&gt;
&lt;h1 id="certificates"&gt;Certificates
&lt;/h1&gt;&lt;p&gt;Sooner or later, running everything without TLS becomes annoying.&lt;/p&gt;
&lt;p&gt;For a home lab environment, generating your own Certificate Authority and issuing certificates locally is perfectly acceptable.&lt;/p&gt;
&lt;h2 id="root-certificate-authority"&gt;Root Certificate Authority
&lt;/h2&gt;&lt;p&gt;Create a dedicated CA:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir -p certs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd certs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;openssl genrsa -out lab-ca.key &lt;span style="color:#ae81ff"&gt;4096&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;openssl req -x509 -new -nodes &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -key lab-ca.key &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -sha256 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -days &lt;span style="color:#ae81ff"&gt;3650&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -out lab-ca.crt &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -subj &lt;span style="color:#e6db74"&gt;&amp;#34;/CN=Lab Local CA&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="wildcard-certificate"&gt;Wildcard Certificate
&lt;/h2&gt;&lt;p&gt;A wildcard certificate can secure every host under:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create the configuration file &lt;code&gt;lab-local.cnf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[req]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;default_bits = 2048
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;prompt = no
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;default_md = sha256
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;distinguished_name = dn
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;req_extensions = req_ext
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[dn]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CN = *.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[req_ext]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;subjectAltName = @alt_names
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[alt_names]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DNS.1 = *.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DNS.2 = lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Generate the key, request, and certificate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;openssl genrsa -out lab-local.key &lt;span style="color:#ae81ff"&gt;2048&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;openssl req -new &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -key lab-local.key &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -out lab-local.csr &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -config lab-local.cnf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;openssl x509 -req &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -in lab-local.csr &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -CA lab-ca.crt &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -CAkey lab-ca.key &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -CAcreateserial &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -out lab-local.crt &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -days &lt;span style="color:#ae81ff"&gt;825&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -sha256 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -extensions req_ext &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -extfile lab-local.cnf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="namespace-for-the-ingress-controller"&gt;Namespace for the Ingress Controller
&lt;/h1&gt;&lt;p&gt;The ingress controller will live in its own namespace.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;ingress-namespace.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Namespace&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ingress-nginx&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Apply it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl apply -f ingress-namespace.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="tls-secret"&gt;TLS Secret
&lt;/h1&gt;&lt;p&gt;The certificate must be stored inside Kubernetes as a TLS secret:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl -n ingress-nginx create secret tls lab-local-tls &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --cert&lt;span style="color:#f92672"&gt;=&lt;/span&gt;lab-local.crt &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --key&lt;span style="color:#f92672"&gt;=&lt;/span&gt;lab-local.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The secret is named:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;lab-local-tls
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and can be referenced as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ingress-nginx/lab-local-tls
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;throughout the cluster configuration.&lt;/p&gt;
&lt;h1 id="installing-the-ca-certificate-on-your-workstation"&gt;Installing the CA Certificate on Your Workstation
&lt;/h1&gt;&lt;p&gt;The root CA must also be trusted by your workstation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cp lab-ca.crt /usr/local/share/ca-certificates/lab-ca.crt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo update-ca-certificates
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Firefox users should additionally import the CA certificate manually through the browser settings.&lt;/p&gt;
&lt;h1 id="deploying-the-ingress-controller"&gt;Deploying the Ingress Controller
&lt;/h1&gt;&lt;p&gt;A complete ingress controller deployment requires several Kubernetes resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ServiceAccount&lt;/li&gt;
&lt;li&gt;IngressClass&lt;/li&gt;
&lt;li&gt;ConfigMap&lt;/li&gt;
&lt;li&gt;ClusterRole&lt;/li&gt;
&lt;li&gt;ClusterRoleBinding&lt;/li&gt;
&lt;li&gt;Deployment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The container image used in this setup is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;registry.k8s.io/ingress-nginx/controller:v1.14.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which provides the familiar NGINX reverse proxy and load balancer functionality.&lt;/p&gt;
&lt;p&gt;The deployment manifest is shown below.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ingress.yaml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# (manifest unchanged from the original article)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="noteworthy-configuration"&gt;Noteworthy Configuration
&lt;/h2&gt;&lt;p&gt;The ingress controller is pinned to the control-plane node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;nodeSelector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;kubernetes.io/hostname&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;iridium&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is perfectly acceptable for a small home lab.&lt;/p&gt;
&lt;p&gt;The controller also uses the wildcard certificate by default:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;--&lt;span style="color:#ae81ff"&gt;default-ssl-certificate=ingress-nginx/lab-local-tls&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which means every ingress can immediately benefit from TLS.&lt;/p&gt;
&lt;h1 id="verifying-the-installation"&gt;Verifying the Installation
&lt;/h1&gt;&lt;p&gt;Start with the usual checks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get pods -n ingress-nginx -o wide
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get rs -n ingress-nginx -o wide
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, add a test host entry:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.24 test.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;to your workstation&amp;rsquo;s &lt;code&gt;/etc/hosts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl http://test.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Expected result:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;404 Not Found&amp;lt;/&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;center&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;404 Not Found&amp;lt;/&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;center&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;hr&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;center&lt;/span&gt;&amp;gt;nginx&amp;lt;/&lt;span style="color:#f92672"&gt;center&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Surprisingly, this is good news.&lt;/p&gt;
&lt;p&gt;The DNS lookup works.&lt;/p&gt;
&lt;p&gt;The ingress controller works.&lt;/p&gt;
&lt;p&gt;NGINX works.&lt;/p&gt;
&lt;p&gt;There simply isn&amp;rsquo;t any application behind the hostname yet.&lt;/p&gt;
&lt;p&gt;The infrastructure is doing exactly what it should.&lt;/p&gt;
&lt;h2 id="tls-verification"&gt;TLS Verification
&lt;/h2&gt;&lt;p&gt;Next, test HTTPS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -v https://test.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The interesting part is not the final HTTP response, but the TLS negotiation itself.&lt;/p&gt;
&lt;p&gt;Look for output similar to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;SSL connection using TLSv1.3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;subject: CN=*.lab.local
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;issuer: CN=Lab Local CA
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;subjectAltName: &amp;#34;test.lab.local&amp;#34; matches cert&amp;#39;s &amp;#34;*.lab.local&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;SSL certificate verified via OpenSSL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If these messages appear, the certificate chain is functioning correctly and the wildcard certificate is being served as expected.&lt;/p&gt;
&lt;p&gt;Finally, open the URL in Firefox and confirm that the browser displays the expected lock icon and certificate information.&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion
&lt;/h1&gt;&lt;p&gt;At this point:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DNS works,&lt;/li&gt;
&lt;li&gt;the ingress controller is running,&lt;/li&gt;
&lt;li&gt;TLS works,&lt;/li&gt;
&lt;li&gt;wildcard certificates work,&lt;/li&gt;
&lt;li&gt;external traffic reaches the cluster.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No actual applications are exposed yet, but the foundation is now in place.&lt;/p&gt;
&lt;p&gt;The cluster is finally ready to host something more interesting than infrastructure components.&lt;/p&gt;
&lt;p&gt;Which, as every Kubernetes administrator eventually discovers, is usually the easy part.&lt;/p&gt;</description></item><item><title>2026-06-23</title><link>https://lvajxi03.github.io/posts/2026-06-23/</link><pubDate>Tue, 23 Jun 2026 11:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-23/</guid><description>&lt;h1 id="installing-k3s"&gt;Installing k3s
&lt;/h1&gt;
 &lt;blockquote&gt;
 &lt;p&gt;Warning:&lt;/p&gt;
&lt;p&gt;This article describes the preparation, installation, and initial configuration of a Kubernetes cluster.&lt;/p&gt;
&lt;p&gt;You are reading at your own risk. There is a non-zero chance that your life will never be quite the same afterward.&lt;/p&gt;
&lt;p&gt;All procedures described here have been tested.&lt;/p&gt;
&lt;p&gt;Also, no animals were harmed during the writing of this article. Unless we count two flies, six mosquitoes, and one unfortunate snail removed from a home cucumber plantation.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h1 id="hardware"&gt;Hardware
&lt;/h1&gt;&lt;p&gt;The cluster consists of the following machines:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Hostname&lt;/th&gt;
 &lt;th&gt;IP Address&lt;/th&gt;
 &lt;th&gt;SBC&lt;/th&gt;
 &lt;th&gt;Architecture&lt;/th&gt;
 &lt;th&gt;RAM&lt;/th&gt;
 &lt;th&gt;Storage&lt;/th&gt;
 &lt;th&gt;Operating System&lt;/th&gt;
 &lt;th&gt;Intended Role&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;palladium&lt;/td&gt;
 &lt;td&gt;10.10.10.20&lt;/td&gt;
 &lt;td&gt;Odroid N2&lt;/td&gt;
 &lt;td&gt;arm64&lt;/td&gt;
 &lt;td&gt;4 GB&lt;/td&gt;
 &lt;td&gt;128 GB&lt;/td&gt;
 &lt;td&gt;Armbian&lt;/td&gt;
 &lt;td&gt;tiny-int&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;vanadium&lt;/td&gt;
 &lt;td&gt;10.10.10.21&lt;/td&gt;
 &lt;td&gt;Raspberry Pi 5&lt;/td&gt;
 &lt;td&gt;arm64&lt;/td&gt;
 &lt;td&gt;8 GB&lt;/td&gt;
 &lt;td&gt;512 GB&lt;/td&gt;
 &lt;td&gt;Ubuntu Server&lt;/td&gt;
 &lt;td&gt;ci-cd&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;iridium&lt;/td&gt;
 &lt;td&gt;10.10.10.24&lt;/td&gt;
 &lt;td&gt;Odroid M2&lt;/td&gt;
 &lt;td&gt;arm64&lt;/td&gt;
 &lt;td&gt;16 GB&lt;/td&gt;
 &lt;td&gt;512 GB&lt;/td&gt;
 &lt;td&gt;Armbian&lt;/td&gt;
 &lt;td&gt;control-plane&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ruthenium&lt;/td&gt;
 &lt;td&gt;10.10.10.25&lt;/td&gt;
 &lt;td&gt;Orange Pi 5 Plus&lt;/td&gt;
 &lt;td&gt;arm64&lt;/td&gt;
 &lt;td&gt;16 GB&lt;/td&gt;
 &lt;td&gt;512 GB&lt;/td&gt;
 &lt;td&gt;Armbian&lt;/td&gt;
 &lt;td&gt;heavy-worker&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;tantalum&lt;/td&gt;
 &lt;td&gt;10.10.10.26&lt;/td&gt;
 &lt;td&gt;Raspberry Pi 500&lt;/td&gt;
 &lt;td&gt;arm64&lt;/td&gt;
 &lt;td&gt;16 GB&lt;/td&gt;
 &lt;td&gt;256 GB&lt;/td&gt;
 &lt;td&gt;Ubuntu Server&lt;/td&gt;
 &lt;td&gt;observability&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All IP addresses are assigned by the home router using static DHCP reservations based on MAC addresses.&lt;/p&gt;
&lt;h1 id="operating-system-and-required-software"&gt;Operating System and Required Software
&lt;/h1&gt;&lt;p&gt;All nodes run either Ubuntu Server or Armbian based on Ubuntu Noble.&lt;/p&gt;
&lt;p&gt;Versions may differ slightly, but package management remains identical across the cluster.&lt;/p&gt;
&lt;p&gt;The first step is ensuring that every machine can resolve the names of every other machine.&lt;/p&gt;
&lt;p&gt;At the moment there is no local DNS server, so each host requires an &lt;code&gt;/etc/hosts&lt;/code&gt; entry similar to the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.20 palladium
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.21 vanadium
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.24 iridium
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.25 ruthenium
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.26 tantalum
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It is also worth adding the same entries to the workstation or laptop used to manage the cluster.&lt;/p&gt;
&lt;p&gt;Windows users may perform the traditional rite of passage and figure out where Microsoft decided to hide the hosts file this decade.&lt;/p&gt;
&lt;h2 id="useful-tools"&gt;Useful Tools
&lt;/h2&gt;&lt;p&gt;It is highly recommended to install &lt;code&gt;kubectl&lt;/code&gt; on your workstation.&lt;/p&gt;
&lt;p&gt;The official installation instructions can be found here:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/" target="_blank" rel="noopener"
 &gt;https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;On every cluster node, install a basic set of utilities:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt update &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt install -y &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; curl &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wget &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vim &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; git &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; htop &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; net-tools &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iproute2 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iptables &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ca-certificates &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gnupg &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lsb-release &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; jq &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; socat &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; conntrack &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; open-iscsi &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nfs-common &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tmux &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dnsutils &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tcpdump &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lsof &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you prefer the Emacs lifestyle but do not enjoy &amp;ldquo;the Sixth Editor&amp;rdquo;, replace &lt;code&gt;vim&lt;/code&gt; with &lt;code&gt;mg&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You may also want:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install mc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;if navigating directories through a dual-panel interface reminds you of happier times.&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;iridium&lt;/code&gt;, I additionally install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install -y sqlite3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Swap was already disabled during the preparation of the machines, but it is worth double-checking before proceeding.&lt;/p&gt;
&lt;h2 id="kernel-modules"&gt;Kernel Modules
&lt;/h2&gt;&lt;p&gt;Enable the required kernel modules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cat &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;EOF | sudo tee /etc/modules-load.d/k3s.conf
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;overlay
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;br_netfilter
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo modprobe overlay
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo modprobe br_netfilter
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then configure the required kernel parameters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cat &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;EOF | sudo tee /etc/sysctl.d/k3s.conf
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;net.bridge.bridge-nf-call-iptables=1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;net.bridge.bridge-nf-call-ip6tables=1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;net.ipv4.ip_forward=1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo sysctl --system
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="cgroups"&gt;cgroups
&lt;/h2&gt;&lt;p&gt;It is also worth checking whether cgroups are properly enabled:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cat /proc/cgroups
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Some systems may require additional kernel parameters in &lt;code&gt;/boot/cmdline.txt&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="kubernetes"&gt;Kubernetes
&lt;/h1&gt;&lt;p&gt;The chosen distribution is &lt;strong&gt;k3s&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It is fully compatible with &amp;ldquo;serious&amp;rdquo; Kubernetes while requiring significantly fewer resources, making it an excellent choice for home labs, edge computing, and IoT deployments.&lt;/p&gt;
&lt;p&gt;Small, lightweight, easy to install, and surprisingly capable. What more could one ask for?&lt;/p&gt;
&lt;h2 id="control-plane"&gt;Control Plane
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;iridium&lt;/code&gt; has been selected as the control-plane node because it is the most powerful machine in the cluster.&lt;/p&gt;
&lt;p&gt;The remaining nodes will join as workers with specific responsibilities.&lt;/p&gt;
&lt;p&gt;Install k3s on &lt;code&gt;iridium&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -sfL https://get.k3s.io | sh -s - server &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --node-name iridium &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --node-ip 10.10.10.24 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --advertise-address 10.10.10.24 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --write-kubeconfig-mode &lt;span style="color:#ae81ff"&gt;644&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --disable traefik
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run this command directly on the machine, either through SSH or a local terminal session.&lt;/p&gt;
&lt;h3 id="what-do-these-flags-do"&gt;What Do These Flags Do?
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--node-ip&lt;/code&gt; — forces a specific IP address and therefore a specific network interface.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--advertise-address&lt;/code&gt; — makes the API server reachable from the LAN.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--disable traefik&lt;/code&gt; — k3s ships with Traefik as its default ingress controller, but this cluster will use NGINX later.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--write-kubeconfig-mode 644&lt;/code&gt; — creates the kubeconfig file with world-readable permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s it.&lt;/p&gt;
&lt;p&gt;Really.&lt;/p&gt;
&lt;p&gt;After installation, verify that everything is running correctly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo systemctl status k3s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;More importantly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get nodes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;should produce something similar to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;iridium Ready control-plane
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An even better command is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get nodes -w
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;-w&lt;/code&gt; option continuously watches for changes.&lt;/p&gt;
&lt;p&gt;This is useful because a node that repeatedly transitions between &lt;code&gt;Ready&lt;/code&gt; and some other state usually indicates an incomplete kernel configuration.&lt;/p&gt;
&lt;p&gt;The most common culprits are missing &lt;code&gt;overlay&lt;/code&gt; or &lt;code&gt;br_netfilter&lt;/code&gt; modules.&lt;/p&gt;
&lt;p&gt;If everything looks healthy, retrieve the cluster configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cat /etc/rancher/k3s/k3s.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Copy it to your workstation.&lt;/p&gt;
&lt;p&gt;You may store it as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;~/.kube/config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or keep multiple cluster configurations separately.&lt;/p&gt;
&lt;p&gt;For Bash:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;export KUBECONFIG&lt;span style="color:#f92672"&gt;=&lt;/span&gt;~/.kube/my-cluster-config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For Fish:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fish" data-lang="fish"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;set&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;-x&lt;/span&gt; KUBECONFIG ~/.kube/my-cluster-config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before using the file, edit the server address:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;clusters&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;cluster&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;certificate-authority-data&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;server&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://127.0.0.1:6443&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Replace:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;127.0.0.1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;10.10.10.24
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should now be able to run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get nodes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;directly from your workstation and obtain the same results.&lt;/p&gt;
&lt;h2 id="joining-worker-nodes"&gt;Joining Worker Nodes
&lt;/h2&gt;&lt;p&gt;The node registration token can be found on the control-plane node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cat /var/lib/rancher/k3s/server/node-token
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Use it on each worker node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -sfL https://get.k3s.io | &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;K3S_URL&lt;span style="color:#f92672"&gt;=&lt;/span&gt;https://10.10.10.24:6443 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;K3S_TOKEN&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;YOUR_TOKEN_HERE&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sh -s - agent &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --node-name vanadium &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --node-ip 10.10.10.21
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This example joins &lt;code&gt;vanadium&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Remember to adjust both the node name and IP address accordingly.&lt;/p&gt;
&lt;p&gt;Run the command directly on the target machine.&lt;/p&gt;
&lt;p&gt;Once all nodes have joined, verify the cluster:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get nodes -o wide
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every node should eventually report:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Ready
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Simple?&lt;/p&gt;
&lt;p&gt;Simple.&lt;/p&gt;
&lt;p&gt;A little time-consuming, perhaps, but there are no lengthy computations involved.&lt;/p&gt;
&lt;p&gt;Only now is it safe to reach for your favorite book.&lt;/p&gt;</description></item><item><title>2026-06-22</title><link>https://lvajxi03.github.io/posts/2026-06-22/</link><pubDate>Mon, 22 Jun 2026 20:05:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-22/</guid><description>
 &lt;blockquote&gt;
 &lt;p&gt;The original version of this article was written in 2006.&lt;/p&gt;
&lt;p&gt;Back then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python 2 was still alive,&lt;/li&gt;
&lt;li&gt;distutils existed,&lt;/li&gt;
&lt;li&gt;XEmacs was technically not dead,&lt;/li&gt;
&lt;li&gt;and people still owned VHS tapes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s see how much of this still works in 2026.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h1 id="modules-lies-and-vhs-tapes"&gt;Modules, Lies, and VHS Tapes
&lt;/h1&gt;&lt;p&gt;Sometimes you need to write Python bindings for a strange third-party library. It happens.&lt;/p&gt;
&lt;p&gt;Sometimes you need to write the library itself. That happens too.&lt;/p&gt;
&lt;p&gt;And sometimes you have to do all of that on Windows using GNU tools.&lt;/p&gt;
&lt;p&gt;This article serves both as a professional tutorial and as a public service announcement suggesting that you abandon everything and move to a remote mountain cabin. Mostly because I can never remember the correct order of the steps involved.&lt;/p&gt;
&lt;h2 id="prerequisites-on-windows"&gt;Prerequisites on Windows
&lt;/h2&gt;&lt;p&gt;The easiest approach is to use MSYS2 and GCC.&lt;/p&gt;
&lt;p&gt;You should have the following tools available somewhere in your PATH:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GCC&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dlltool&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nothing particularly exotic.&lt;/p&gt;
&lt;h2 id="a-sample-library"&gt;A Sample Library
&lt;/h2&gt;&lt;p&gt;Our example library is intentionally simple. It contains only two functions: one that adds two integers and one that multiplies them.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mul2x&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; x, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; y)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; x &lt;span style="color:#f92672"&gt;*&lt;/span&gt; y;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;add2x&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; x, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; y)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; x &lt;span style="color:#f92672"&gt;+&lt;/span&gt; y;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Since the library is simple, let&amp;rsquo;s call it &lt;code&gt;SimpLib&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Assume the source code is stored in a file called &lt;code&gt;simplib.c&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="building-the-library"&gt;Building the Library
&lt;/h2&gt;&lt;p&gt;As always, source files should first be compiled into object files.&lt;/p&gt;
&lt;p&gt;To produce a shared library, GCC requires the &lt;code&gt;-shared&lt;/code&gt; option.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Following Unix naming conventions, shared libraries usually have a &lt;code&gt;lib&lt;/code&gt; prefix and a &lt;code&gt;.so&lt;/code&gt; extension. Linking with &lt;code&gt;-lname&lt;/code&gt; assumes the presence of a library named &lt;code&gt;libname.so&lt;/code&gt; somewhere on the library search path. Many GNU projects ported to Windows continue to use this naming convention internally, even though the final artifact is a &lt;code&gt;.dll&lt;/code&gt;.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="linux"&gt;Linux
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o simplib.o -c simplib.c
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o libsimp.so simplib.o -shared
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Of course, for a single source file you could simply write:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o libsimp.so simplib.c -shared
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, once a project grows beyond a trivial example, compiling each source file separately and linking everything at the end is usually a much better idea.&lt;/p&gt;
&lt;p&gt;Congratulations. The library has been built and is ready for use.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cue triumphant fanfare.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="windows"&gt;Windows
&lt;/h3&gt;&lt;p&gt;Although we&amp;rsquo;re using exactly the same compiler, Windows requires one additional step.&lt;/p&gt;
&lt;p&gt;We need to create an &lt;strong&gt;import library&lt;/strong&gt; (sometimes called an interface library) that other applications can link against. This is where &lt;code&gt;dlltool&lt;/code&gt; enters the picture.&lt;/p&gt;
&lt;h4 id="creating-the-import-library"&gt;Creating the Import Library
&lt;/h4&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o simplib.o -c simplib.c
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dlltool -l libsimp.lib simplib.o
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o libsimp.dll simplib.o -shared
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The DLL is now ready.&lt;/p&gt;
&lt;p&gt;The import library is also ready.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cue Windows-themed fanfare.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="header-file"&gt;Header File
&lt;/h2&gt;&lt;p&gt;Providing a header file is generally considered a polite thing to do.&lt;/p&gt;
&lt;p&gt;Other developers will appreciate not having to reverse-engineer your function declarations, and they will be less inclined to say unpleasant things about you.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#ifndef __SIMPLIB_H__
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#define __SIMPLIB_H__
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mul2x&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; x, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; y);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;add2x&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; x, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; y);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#endif
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Assume this file is called &lt;code&gt;simplib.h&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="a-sample-c-program"&gt;A Sample C Program
&lt;/h2&gt;&lt;p&gt;The best way to test a library is to write a small program that uses it.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;There are not many activities one can perform while fully clothed that are more enjoyable than programming, although surprisingly few people seem aware of this fact.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#include&lt;/span&gt; &lt;span style="color:#75715e"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#include&lt;/span&gt; &lt;span style="color:#75715e"&gt;&amp;#34;simplib.h&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;main&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; argc, &lt;span style="color:#66d9ef"&gt;char&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;argv[])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;printf&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Adding 5 to 10: %d&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;add2x&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;printf&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Multiplying 5 by 10: %d&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;mul2x&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Assume this program is stored as &lt;code&gt;simprg.c&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="building-the-sample-program"&gt;Building the Sample Program
&lt;/h2&gt;&lt;h3 id="linux-1"&gt;Linux
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o simprg.o -c simprg.c
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o simprg simprg.o -lsimp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="windows-1"&gt;Windows
&lt;/h3&gt;&lt;p&gt;Remember the import library:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o simprg.o -c simprg.c
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o simprg simprg.o libsimp.lib -lsimp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="a-brief-note-about-flags"&gt;A Brief Note About Flags
&lt;/h3&gt;&lt;p&gt;If the library is not located on the system library search path, remember to use the &lt;code&gt;-L&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;Windows searches the current directory by default.&lt;/p&gt;
&lt;p&gt;Linux does not.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcc -o simprg simprg.o -L. -lsimp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="writing-a-python-extension-module"&gt;Writing a Python Extension Module
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;Historical note&lt;/p&gt;
&lt;p&gt;The code below targets Python 2.x and reflects the state of the CPython API around 2006.
Modern Python 3 extension modules use a different initialization mechanism based on
&lt;code&gt;PyModuleDef&lt;/code&gt; and &lt;code&gt;PyModule_Create&lt;/code&gt;.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;The following code demonstrates Python bindings for &lt;code&gt;SimpLib&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#include&lt;/span&gt; &lt;span style="color:#75715e"&gt;&amp;lt;Python.h&amp;gt;&lt;/span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#include&lt;/span&gt; &lt;span style="color:#75715e"&gt;&amp;#34;simplib.h&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; PyObject &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;simp_mul2x&lt;/span&gt;(PyObject &lt;span style="color:#f92672"&gt;*&lt;/span&gt;self, PyObject &lt;span style="color:#f92672"&gt;*&lt;/span&gt;args)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; x, y, result;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PyObject &lt;span style="color:#f92672"&gt;*&lt;/span&gt;pyresult;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;PyArg_ParseTuple&lt;/span&gt;(args, &lt;span style="color:#e6db74"&gt;&amp;#34;ii&amp;#34;&lt;/span&gt;, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;x;, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;y;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; NULL;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mul2x&lt;/span&gt;(x, y);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pyresult &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Py_BuildValue&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;i&amp;#34;&lt;/span&gt;, result);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; pyresult;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; PyObject &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;simp_add2x&lt;/span&gt;(PyObject &lt;span style="color:#f92672"&gt;*&lt;/span&gt;self, PyObject &lt;span style="color:#f92672"&gt;*&lt;/span&gt;args)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; x, y, result;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PyObject &lt;span style="color:#f92672"&gt;*&lt;/span&gt;pyresult;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;PyArg_ParseTuple&lt;/span&gt;(args, &lt;span style="color:#e6db74"&gt;&amp;#34;ii&amp;#34;&lt;/span&gt;, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;x;, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;y;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; NULL;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;add2x&lt;/span&gt;(x, y);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pyresult &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Py_BuildValue&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;i&amp;#34;&lt;/span&gt;, result);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; pyresult;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; PyMethodDef SimpMethods[] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;mul2x&amp;#34;&lt;/span&gt;, simp_mul2x, METH_VARARGS, &lt;span style="color:#e6db74"&gt;&amp;#34;Multiply two integer values&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;add2x&amp;#34;&lt;/span&gt;, simp_add2x, METH_VARARGS, &lt;span style="color:#e6db74"&gt;&amp;#34;Add two integer values&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {NULL, NULL, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, NULL}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PyMODINIT_FUNC &lt;span style="color:#a6e22e"&gt;initsimp&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;Py_InitModule&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;simp&amp;#34;&lt;/span&gt;, SimpMethods);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Assume the file is called &lt;code&gt;simpmodule.c&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="what-is-happening-here"&gt;What Is Happening Here?
&lt;/h3&gt;&lt;p&gt;Every extension module requires an initialization function.&lt;/p&gt;
&lt;p&gt;Historically, for Python 2.x, this function was named &lt;code&gt;init&lt;/code&gt; followed by the module name, hence:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;initsimp&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The initialization function creates the module object itself.&lt;/p&gt;
&lt;p&gt;Every function listed in the module&amp;rsquo;s method table becomes a method of that module.&lt;/p&gt;
&lt;p&gt;In our case:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; PyMethodDef SimpMethods[]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the names visible from Python,&lt;/li&gt;
&lt;li&gt;the addresses of the corresponding C functions,&lt;/li&gt;
&lt;li&gt;additional flags describing how arguments are passed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;METH_VARARGS&lt;/code&gt; flag indicates that Python will pass arguments as a tuple.&lt;/p&gt;
&lt;p&gt;The binding functions themselves follow a simple pattern:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Validate input arguments.&lt;/li&gt;
&lt;li&gt;Perform the actual computation.&lt;/li&gt;
&lt;li&gt;Convert the result into a Python object.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Argument validation is handled by:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PyArg_ParseTuple&lt;/span&gt;(args, &lt;span style="color:#e6db74"&gt;&amp;#34;ii&amp;#34;&lt;/span&gt;, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;x, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;y)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The format string &lt;code&gt;&amp;quot;ii&amp;quot;&lt;/code&gt; means:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;I expect two arguments, and both of them must be integers.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;If validation fails, the function should simply return &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Building the return value is similarly straightforward:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Py_BuildValue&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;i&amp;#34;&lt;/span&gt;, result);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which constructs a Python integer object.&lt;/p&gt;
&lt;p&gt;The Python documentation traditionally recommends declaring all binding functions as &lt;code&gt;static&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="building-the-module"&gt;Building the Module
&lt;/h2&gt;&lt;p&gt;One way to build the extension is to use &lt;code&gt;distutils&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="example-setuppy"&gt;Example setup.py
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; distutils.core &lt;span style="color:#f92672"&gt;import&lt;/span&gt; setup, Extension
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;simpmodule &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Extension(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;simp&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sources&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#39;simpmodule.c&amp;#39;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; libraries&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#39;simp&amp;#39;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; library_dirs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#39;.&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;setup(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;SimpLib&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; version&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;1.0&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; description&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;This is SimpLib bindings package&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ext_modules&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[simpmodule]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;Extension&lt;/code&gt; object describes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;source files,&lt;/li&gt;
&lt;li&gt;required libraries,&lt;/li&gt;
&lt;li&gt;library search paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;setup()&lt;/code&gt; function handles the rest.&lt;/p&gt;
&lt;h3 id="linux-2"&gt;Linux
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python setup.py build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="windows-2"&gt;Windows
&lt;/h3&gt;&lt;p&gt;Unfortunately, Python traditionally expects Visual C++ when building extension modules.&lt;/p&gt;
&lt;p&gt;If that doesn&amp;rsquo;t bother you, feel free to use it.&lt;/p&gt;
&lt;p&gt;That would, however, make this article considerably shorter.&lt;/p&gt;
&lt;p&gt;To force MinGW:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python setup.py build --compiler&lt;span style="color:#f92672"&gt;=&lt;/span&gt;mingw32
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="installing-the-module"&gt;Installing the Module
&lt;/h2&gt;&lt;h3 id="linux-3"&gt;Linux
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python setup.py install
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The extension and all required files will be installed into the appropriate Python directories.&lt;/p&gt;
&lt;h3 id="windows-3"&gt;Windows
&lt;/h3&gt;&lt;p&gt;The generated &lt;code&gt;simp.pyd&lt;/code&gt; file should be copied into either:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Lib\site-packages
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;%APPDATA%\Roaming\Python\site-packages
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make sure you know which Python installation you&amp;rsquo;re targeting before copying files around.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;If you happen to own Visual C++, and if the sight of it does not cause emotional distress, installation works exactly like it does on Linux.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="a-short-test-program"&gt;A Short Test Program
&lt;/h2&gt;&lt;p&gt;Naturally, the best way to test the module is to use it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; simp &lt;span style="color:#f92672"&gt;import&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(&lt;span style="color:#e6db74"&gt;&amp;#34;Adding 212 to 384:&amp;#34;&lt;/span&gt;, add2x(&lt;span style="color:#ae81ff"&gt;212&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;384&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(&lt;span style="color:#e6db74"&gt;&amp;#34;Multiplying 212 by 384:&amp;#34;&lt;/span&gt;, mul2x(&lt;span style="color:#ae81ff"&gt;212&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;384&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should also verify that incorrect input is handled properly.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(mul2x(&lt;span style="color:#ae81ff"&gt;212&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;A tester walks into a bar&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(mul2x(&lt;span style="color:#ae81ff"&gt;212&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;384&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;25&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both cases should result in Python exceptions being raised by the argument validation code.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts
&lt;/h2&gt;&lt;p&gt;Keep your spirits up.&lt;/p&gt;
&lt;p&gt;Rome wasn&amp;rsquo;t built in a day.&lt;/p&gt;
&lt;p&gt;Neither are C libraries, Python bindings, build systems, or debugging skills.&lt;/p&gt;
&lt;p&gt;Compilation errors, broken builds, failed tests, and nights spent chasing mysterious bugs are simply part of the profession.&lt;/p&gt;
&lt;p&gt;The important thing is to remember that every experienced developer has been through exactly the same process.&lt;/p&gt;
&lt;p&gt;Some of them are still going through it.&lt;/p&gt;</description></item><item><title>2026-06-20</title><link>https://lvajxi03.github.io/posts/2026-06-20/</link><pubDate>Sat, 20 Jun 2026 08:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-20/</guid><description>&lt;h1 id="what-is-emacs-and-do-i-need-it"&gt;What Is Emacs, and Do I Need It?
&lt;/h1&gt;&lt;ul&gt;
&lt;li&gt;GNU Emacs is a text editor originally created by Richard Stallman for the PDP-10 platform.&lt;/li&gt;
&lt;li&gt;GNU Emacs is a universal development environment available on Unix and Unix-like systems, Microsoft Windows, macOS, and many others.&lt;/li&gt;
&lt;li&gt;GNU Emacs is an operating system disguised as a text editor.&lt;/li&gt;
&lt;li&gt;GNU Emacs is a philosophy and a way of life.&lt;/li&gt;
&lt;li&gt;GNU Emacs is a religion.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All right, let&amp;rsquo;s start over.&lt;/p&gt;
&lt;h2 id="a-brief-history"&gt;A Brief History
&lt;/h2&gt;&lt;p&gt;Richard Stallman, while working at MIT, became increasingly frustrated with proprietary software. Not only was it often full of bugs, but there was usually little anyone outside the vendor could do to fix them. Bug reports rarely led to meaningful improvements, and users were expected to simply accept whatever they were given.&lt;/p&gt;
&lt;p&gt;Eventually, Stallman left MIT and launched the GNU Project, whose goal was to create a completely free Unix-like operating system.&lt;/p&gt;
&lt;p&gt;Emacs itself began as a collection of macros for the TECO editor. After a rapid evolution, it became a standalone application and eventually one of the most influential pieces of software in the free software ecosystem. A large community of developers, thousands of articles, books, tutorials, and decades of active use suggest that Emacs still has a long future ahead of it.&lt;/p&gt;
&lt;p&gt;Not everyone, however, was happy with the project&amp;rsquo;s leadership style. The most significant fork occurred when developers from Lucid, who wanted to use Emacs as the foundation of their development environment, split off and created Lucid Emacs. This project later evolved into XEmacs.&lt;/p&gt;
&lt;p&gt;Incidentally, the &amp;ldquo;X&amp;rdquo; in XEmacs has nothing to do with whether the editor can run under the X Window System. Both GNU Emacs and XEmacs work perfectly well with or without X11.&lt;/p&gt;
&lt;p&gt;Over the years, various other forks appeared, often for technical or political reasons. While some introduced interesting ideas, none managed to surpass GNU Emacs itself. XEmacs, despite its innovations, gradually lost momentum. A long period without significant releases and declining community activity eventually left the project effectively dormant. Its website still feels like a time capsule from the late 1990s, apparently untouched by mobile devices, TLS certificates, or modern web design.&lt;/p&gt;
&lt;h2 id="what-is-emacs"&gt;What Is Emacs?
&lt;/h2&gt;&lt;h3 id="a-text-editor"&gt;A Text Editor
&lt;/h3&gt;&lt;p&gt;At its core, Emacs is a text editor.&lt;/p&gt;
&lt;p&gt;This remains the primary goal of the project: creating an exceptionally capable environment for editing text of all kinds. Source code, notes, books, articles, documentation, LaTeX manuscripts, configuration files—everything is ultimately text, and Emacs is designed to handle all of it.&lt;/p&gt;
&lt;h3 id="a-lisp-interpreter-and-runtime-environment"&gt;A Lisp Interpreter and Runtime Environment
&lt;/h3&gt;&lt;p&gt;One of Emacs&amp;rsquo; defining features is its built-in Lisp interpreter.&lt;/p&gt;
&lt;p&gt;Through Emacs Lisp, users can extend the editor to perform virtually any task imaginable. Email clients, news readers, calendars, web browsers, file managers, games, and countless other tools are available either as part of the standard distribution or through third-party packages.&lt;/p&gt;
&lt;p&gt;Need an AI assistant? Someone has almost certainly written one already.&lt;/p&gt;
&lt;p&gt;Installing extensions is typically as simple as fetching the appropriate package from a repository and enabling it.&lt;/p&gt;
&lt;h3 id="a-development-environment"&gt;A Development Environment
&lt;/h3&gt;&lt;p&gt;For many users, Emacs serves primarily as an integrated development environment.&lt;/p&gt;
&lt;p&gt;Support exists for an astonishing number of programming languages. Syntax highlighting, code navigation, language servers, debugging tools, build systems, version control integration, documentation generation, shells, terminals, and countless other development tools are readily available.&lt;/p&gt;
&lt;p&gt;Whether you write C++, Go, Python, Rust, Lisp, or something more obscure, chances are that someone has already built excellent Emacs support for it.&lt;/p&gt;
&lt;h3 id="an-environment-for-markup-languages"&gt;An Environment for Markup Languages
&lt;/h3&gt;&lt;p&gt;Emacs is equally comfortable working with markup and document-oriented formats.&lt;/p&gt;
&lt;p&gt;XML, HTML, YAML, Markdown, Org Mode, TeX, LaTeX, DocBook, and many others are all well supported. Validation, transformation, navigation, and editing tools are often available out of the box.&lt;/p&gt;
&lt;p&gt;The live preview capabilities available for TeX and LaTeX workflows remain particularly impressive even today.&lt;/p&gt;
&lt;h3 id="anything-you-can-convince-lisp-to-do"&gt;Anything You Can Convince Lisp to Do
&lt;/h3&gt;&lt;p&gt;Ultimately, Emacs is whatever you can persuade Emacs Lisp to become.&lt;/p&gt;
&lt;p&gt;This flexibility is both its greatest strength and one of the reasons why Emacs users occasionally develop a reputation for eccentricity.&lt;/p&gt;
&lt;h2 id="what-emacs-is-not"&gt;What Emacs Is Not
&lt;/h2&gt;&lt;h3 id="a-universal-solution"&gt;A Universal Solution
&lt;/h3&gt;&lt;p&gt;Emacs is an incredibly powerful tool, but it is not magic.&lt;/p&gt;
&lt;p&gt;Installing Emacs will not automatically teach you a programming language, system administration, networking, or software architecture. It can help you work more effectively, but it cannot replace knowledge or experience.&lt;/p&gt;
&lt;h2 id="is-emacs-for-me"&gt;Is Emacs for Me?
&lt;/h2&gt;&lt;p&gt;Perhaps.&lt;/p&gt;
&lt;p&gt;Emacs may be a good fit if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You want a consistent environment for editing, programming, writing, note-taking, and other text-oriented tasks.&lt;/li&gt;
&lt;li&gt;You enjoy highly configurable tools.&lt;/li&gt;
&lt;li&gt;You appreciate software that can evolve alongside your workflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Emacs may not be a good fit if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Editing configuration files makes you uncomfortable.&lt;/li&gt;
&lt;li&gt;You expect every feature to be immediately discoverable through graphical menus.&lt;/li&gt;
&lt;li&gt;You are unwilling to invest time learning new tools.&lt;/li&gt;
&lt;li&gt;Your primary computing activity consists of scrolling through social media feeds on a phone.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Emacs has a steep learning curve, but it rewards patience. For many users it eventually becomes much more than an editor; it becomes the environment in which a significant portion of their computing life takes place.&lt;/p&gt;
&lt;p&gt;Whether that sounds exciting or alarming is entirely up to you.&lt;/p&gt;</description></item><item><title>2026-06-17</title><link>https://lvajxi03.github.io/posts/2026-06-17/</link><pubDate>Wed, 17 Jun 2026 08:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-17/</guid><description>&lt;p&gt;Continuation of the post &lt;a class="link" href="https://lvajxi03.github.io/posts/2026-06-15" &gt;Hardware and Software (I)&lt;/a&gt;&lt;/p&gt;
&lt;h1 id="odroid-m2"&gt;Odroid M2
&lt;/h1&gt;&lt;p&gt;The &lt;code&gt;Odroid M2&lt;/code&gt; deserves a dedicated article, as installing and configuring the operating system requires a few extra steps and a fair amount of patience.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Odroid M2" class="gallery-image" data-flex-basis="232px" data-flex-grow="96" height="529" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-17/odroidm2-1.jpg" width="512"&gt;&lt;/p&gt;
&lt;p&gt;The board vendor seems particularly fond of shipping museum-grade software as officially supported images. Personally, I am not a fan of running &lt;code&gt;Ubuntu 20.04&lt;/code&gt; with a &lt;code&gt;4.9&lt;/code&gt; or &lt;code&gt;5.1&lt;/code&gt; kernel in 2026. That is not exactly what modern workloads are looking for.&lt;/p&gt;
&lt;p&gt;Instead, I strongly recommend using &lt;code&gt;Armbian&lt;/code&gt;, whose developers release updates regularly. If you&amp;rsquo;re impatient, you can always build your own image.&lt;/p&gt;
&lt;p&gt;Unfortunately, after installing the official image on an SD card, it quickly becomes apparent that &lt;code&gt;Kubernetes&lt;/code&gt; is not particularly happy with the default kernel configuration. The kernel is built with only a minimal set of features enabled, so it is time to roll up your sleeves and build a custom version.&lt;/p&gt;
&lt;h2 id="storage"&gt;Storage
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;Odroid M2&lt;/code&gt; provides an &lt;code&gt;M.2 NVMe&lt;/code&gt; slot on the underside of the board, making SSD installation straightforward. Simply insert the drive, secure it using the supplied screw, and enjoy your new storage.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Odroid M2 with NVME disk (2280 format)" class="gallery-image" data-flex-basis="230px" data-flex-grow="96" height="533" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-17/odroidm2-2.jpg" width="512"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The board cannot boot directly from an NVMe SSD. You must use either an SD card or the built-in eMMC module for booting.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Take a close look at the connectors and switches on the board. One of them controls the SD/eMMC boot source selection. During installation and initial setup, leave it in the &lt;code&gt;Micro SD&lt;/code&gt; position.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Boot source selection" class="gallery-image" data-flex-basis="386px" data-flex-grow="161" height="318" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-17/odroidm2-3.jpg" width="512"&gt;&lt;/p&gt;
&lt;h2 id="building-the-system"&gt;Building the System
&lt;/h2&gt;&lt;p&gt;Additional requirements: two SD cards (assuming limited prior experience and the possibility of rebuilding the image later).&lt;/p&gt;
&lt;h3 id="bootstrap"&gt;Bootstrap
&lt;/h3&gt;&lt;p&gt;Start by installing the latest version of &lt;code&gt;Armbian&lt;/code&gt; onto an SD card and booting from it.&lt;/p&gt;
&lt;p&gt;Next:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complete the initial system configuration (passwords, user accounts, locale, and everything else the setup wizard asks for).&lt;/li&gt;
&lt;li&gt;Allow your user to execute &lt;code&gt;sudo&lt;/code&gt; without a password by adding something similar to &lt;code&gt;/etc/sudoers&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;marcin ALL=(ALL) NOPASSWD:ALL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;git&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Clone the build repository:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git clone --depth &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; https://github.com/armbian/build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;Launch the main build script:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd build
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;./compile.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The script handles the entire build process:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;installs missing dependencies,&lt;/li&gt;
&lt;li&gt;guides the user through the required configuration,&lt;/li&gt;
&lt;li&gt;downloads and builds the kernel,&lt;/li&gt;
&lt;li&gt;fetches all remaining packages,&lt;/li&gt;
&lt;li&gt;generates a complete system image.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="configuration-and-build"&gt;Configuration and Build
&lt;/h3&gt;&lt;p&gt;Important options in the configuration wizard:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Show a kernel configuration menu before compilation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The default kernel configuration is generally reasonable, but it is worth verifying that options required by Kubernetes are enabled, particularly various &lt;code&gt;netfilter&lt;/code&gt; features.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Choose a board&lt;/code&gt; → &lt;strong&gt;odroidm2&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Choose a kernel&lt;/code&gt; → &lt;strong&gt;current&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Choose a release package base&lt;/code&gt; → &lt;strong&gt;noble&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(&lt;code&gt;resolute&lt;/code&gt; has recently appeared, but I have not tested it yet.)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Choose image type&lt;/code&gt; → &lt;strong&gt;Image with console interface&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Choose image type (II)&lt;/code&gt; → &lt;strong&gt;Standard image with console interface&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You may spend anywhere from a few minutes to several hours reviewing kernel options, which is why I recommend having a spare SD card available.&lt;/p&gt;
&lt;p&gt;Once configuration is complete, the build system will spend quite some time compiling kernels, building packages, and assembling the final image.&lt;/p&gt;
&lt;p&gt;During that time, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;go for a run,&lt;/li&gt;
&lt;li&gt;visit the swimming pool,&lt;/li&gt;
&lt;li&gt;ride a bicycle,&lt;/li&gt;
&lt;li&gt;or, as a last resort, take a very long walk.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once the build completes successfully, you should find something similar to the following in &lt;code&gt;output/images&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Armbian-unofficial_26.08.0-trunk_Odroidm2_resolute_current_6.18.35.img
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Yes, every locally built image automatically receives the &lt;code&gt;-unofficial&lt;/code&gt; suffix.&lt;/p&gt;
&lt;h3 id="writing-the-image"&gt;Writing the Image
&lt;/h3&gt;&lt;p&gt;Since the device only provides a single SD card slot, you will need to transfer the generated image to your regular workstation and write it to the second SD card.&lt;/p&gt;
&lt;p&gt;After that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;shut down the device,&lt;/li&gt;
&lt;li&gt;replace the SD card,&lt;/li&gt;
&lt;li&gt;power it on,&lt;/li&gt;
&lt;li&gt;enjoy your shiny new system,&lt;/li&gt;
&lt;li&gt;go through the initial configuration one more time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make sure the SSD is visible:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;lsblk | grep nvme0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The output should look similar to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nvme0n1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="installing-the-system-onto-emmc-and-nvme"&gt;Installing the System onto eMMC and NVMe
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;Running from an SD card is useful for diagnostics and experimentation, but the built-in eMMC should be used for everyday operation.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;To begin installation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;armbian-install
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The most important choice during installation is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;boot from eMMC, root from NVMe
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The installer will warn about potential data loss, create fresh partitions on both eMMC and SSD storage, and transfer the already configured system onto the SSD.&lt;/p&gt;
&lt;p&gt;As a result, you will not need to repeat the entire setup process again.&lt;/p&gt;
&lt;p&gt;Once installation completes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;shut down the device,&lt;/li&gt;
&lt;li&gt;remove the SD card,&lt;/li&gt;
&lt;li&gt;switch the boot selector to &lt;code&gt;eMMC&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;power the board on,&lt;/li&gt;
&lt;li&gt;enjoy a significantly faster-booting &lt;code&gt;Odroid M2&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="final-system-configuration"&gt;Final System Configuration
&lt;/h2&gt;&lt;p&gt;The first thing to do is disable swap.&lt;/p&gt;
&lt;p&gt;Check whether swap is active:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;swapon --show
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;free -h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Typical output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME TYPE SIZE USED PRIO
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/swap.img file 8G 0B -2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Disable it completely:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo swapoff -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If &lt;code&gt;swapon --show&lt;/code&gt; reports something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/dev/zram0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;then zram should be disabled as well:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo systemctl disable --now armbian-zram-config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Optionally remove the configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo rm -f /etc/default/armbian-zram-config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Kubernetes does not particularly appreciate active swap devices, so it is best to disable them before proceeding further.&lt;/p&gt;
&lt;p&gt;Your user account probably no longer has passwordless sudo access, so repeat the same &lt;code&gt;sudoers&lt;/code&gt; modification as before.&lt;/p&gt;
&lt;p&gt;Finally, replace your SSH keys:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh-copy-id marcin@odroidm2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In my case:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh-copy-id marcin@10.10.10.24
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Voilà. The system is now ready for the next stage.&lt;/p&gt;</description></item><item><title>2026-06-15</title><link>https://lvajxi03.github.io/posts/2026-06-15/</link><pubDate>Mon, 15 Jun 2026 19:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-15/</guid><description>&lt;p&gt;This post provides a brief overview of the hardware, installation methods, and software configuration used in my home Kubernetes lab.&lt;/p&gt;
&lt;h1 id="hardware"&gt;Hardware
&lt;/h1&gt;&lt;p&gt;The cluster currently consists of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Odroid N2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Odroid M2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Raspberry Pi 5&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Raspberry Pi 500&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Orange Pi 5 Plus&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Among these devices, only the &lt;code&gt;Odroid N2&lt;/code&gt; lacks an &lt;code&gt;M.2&lt;/code&gt; interface for an &lt;code&gt;NVMe&lt;/code&gt; SSD and must rely on an &lt;code&gt;SD&lt;/code&gt; card for storage. All the others support &lt;code&gt;NVMe&lt;/code&gt; drives — well, almost all of them, but we&amp;rsquo;ll get to that in a moment.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m only interested in &lt;strong&gt;2280&lt;/strong&gt; form-factor drives due to their lower cost and wider availability.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s take a quick look at what is required to use — and boot from — an &lt;code&gt;NVMe&lt;/code&gt; drive on each platform.&lt;/p&gt;
&lt;h2 id="raspberry-pi-5"&gt;Raspberry Pi 5
&lt;/h2&gt;&lt;p&gt;The Raspberry Pi 5 supports &lt;code&gt;M.2 NVMe&lt;/code&gt; storage through an additional PCIe expansion board (HAT). The best option in terms of price and quality is the one made by &lt;em&gt;Pimoroni&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The Internet is full of installation guides, so I&amp;rsquo;ll keep it brief. The process consists of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;attaching the metal standoffs,&lt;/li&gt;
&lt;li&gt;mounting the SSD using the supplied screw,&lt;/li&gt;
&lt;li&gt;connecting the ribbon cable,&lt;/li&gt;
&lt;li&gt;attaching the HAT to the underside of the Raspberry Pi.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The SSD ends up hidden inside the assembly, between the Raspberry Pi 5 and the HAT itself.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Ribbon cable connecting the HAT to the Raspberry Pi 5" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="384" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-15/rpi5-1.jpg" width="512"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="The underside of the HAT" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="384" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-15/rpi5-2.jpg" width="512"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Trust me, there’s an NVMe SSD in there!" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="384" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://lvajxi03.github.io/posts/2026-06-15/rpi5-3.jpg" width="512"&gt;&lt;/p&gt;
&lt;h3 id="operating-system-installation"&gt;Operating System Installation
&lt;/h3&gt;&lt;p&gt;There are two ways to install the operating system: with or without an &lt;code&gt;SD&lt;/code&gt; card. For the purposes of this project, we&amp;rsquo;re only interested in &lt;code&gt;Ubuntu Server&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="method-1"&gt;Method 1
&lt;/h4&gt;&lt;p&gt;The first method is straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;write a standard &lt;code&gt;Raspberry Pi OS&lt;/code&gt; image (or any other operating system) to an SD card and boot the Raspberry Pi,&lt;/li&gt;
&lt;li&gt;verify that the system can see the SSD:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ lsblk | grep nvme0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The command should return something similar to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nvme0n1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;install and launch &lt;code&gt;rpi-imager&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;select &lt;code&gt;Ubuntu Server&lt;/code&gt; from the operating system list,&lt;/li&gt;
&lt;li&gt;choose the NVMe drive as the target device,&lt;/li&gt;
&lt;li&gt;start the image-writing process,&lt;/li&gt;
&lt;li&gt;shut down the Raspberry Pi,&lt;/li&gt;
&lt;li&gt;remove the SD card,&lt;/li&gt;
&lt;li&gt;boot the system again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is no need to modify the boot order and make NVMe booting the default. It&amp;rsquo;s useful to retain the ability to boot from an SD card &amp;ldquo;just in case&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The Raspberry Pi will still boot from the NVMe drive automatically, albeit a few seconds later. Those extra seconds are hardly a problem.&lt;/p&gt;
&lt;h4 id="method-2"&gt;Method 2
&lt;/h4&gt;&lt;p&gt;The second method requires an &lt;code&gt;M.2&lt;/code&gt; to &lt;code&gt;USB&lt;/code&gt; adapter.&lt;/p&gt;
&lt;p&gt;In this case, the operating system image is written directly to the SSD before it is mounted onto the HAT.&lt;/p&gt;
&lt;p&gt;Once installed, the Raspberry Pi boots exactly the same way as in the first method.&lt;/p&gt;
&lt;h3 id="configuration"&gt;Configuration
&lt;/h3&gt;&lt;p&gt;There are two things worth doing immediately after installation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;disable swap,&lt;/li&gt;
&lt;li&gt;create a dedicated user account, grant passwordless &lt;code&gt;sudo&lt;/code&gt; access, and exchange SSH keys.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can verify whether swap is enabled using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ swapon --show
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ free -h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If swap is active, you&amp;rsquo;ll likely see something similar to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME TYPE SIZE USED PRIO
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/swap.img file 8G 0B -2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Disable swap immediately:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ sudo swapoff -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then remove the corresponding entry from &lt;code&gt;/etc/fstab&lt;/code&gt;, for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/swap.img none swap sw 0 0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This prevents swap from being re-enabled after a reboot.&lt;/p&gt;
&lt;p&gt;Creating a dedicated administrative user is equally simple. Assuming the new user is called &lt;code&gt;marcin&lt;/code&gt;, add the following entry to &lt;code&gt;/etc/sudoers&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# marcins and others:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;marcin ALL=(ALL) NOPASSWD:ALL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, exchange SSH keys:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ ssh-copy-id marcin@raspberry-pi-5-address
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In my case:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ ssh-copy-id marcin@10.10.10.21
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ ssh-copy-id marcin@10.10.10.26
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that done, the machine is ready for automation with tools such as Ansible.&lt;/p&gt;
&lt;p&gt;In the next installment: &lt;strong&gt;Odroid M2&lt;/strong&gt; — and there will be quite a bit to talk about.&lt;/p&gt;</description></item><item><title>2026-06-12</title><link>https://lvajxi03.github.io/posts/2026-06-12/</link><pubDate>Fri, 12 Jun 2026 19:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-12/</guid><description>&lt;h1 id="home-kubernetes-lab"&gt;Home Kubernetes Lab
&lt;/h1&gt;&lt;p&gt;I&amp;rsquo;ve always been a fan of ARM-based single-board computers (SBCs). My journey started with a Raspberry Pi 4, which served as my primary workstation for nearly two years, followed by an Odroid N2. These are simple, reliable devices that are surprisingly versatile—even capable retro-gaming machines.&lt;/p&gt;
&lt;p&gt;While the Raspberry Pi 4 has since been replaced by a Raspberry Pi 5, the Odroid N2 remains in active service. It hosts several lightweight databases and web applications and continues to perform flawlessly.&lt;/p&gt;
&lt;p&gt;Over time, my collection has expanded to include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Odroid M2&lt;/strong&gt; — the next generation of the Odroid product line,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Orange Pi 5 Plus&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raspberry Pi 500&lt;/strong&gt; — purchased as an impulse bargain.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="hardware-overview"&gt;Hardware Overview
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Hostname&lt;/th&gt;
 &lt;th&gt;palladium&lt;/th&gt;
 &lt;th&gt;vanadium&lt;/th&gt;
 &lt;th&gt;iridium&lt;/th&gt;
 &lt;th&gt;ruthenium&lt;/th&gt;
 &lt;th&gt;tantalum&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;SBC Model&lt;/td&gt;
 &lt;td&gt;ODROID N2&lt;/td&gt;
 &lt;td&gt;Raspberry Pi 5&lt;/td&gt;
 &lt;td&gt;ODROID M2&lt;/td&gt;
 &lt;td&gt;Orange Pi 5 Plus&lt;/td&gt;
 &lt;td&gt;Raspberry Pi 500&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SoC&lt;/td&gt;
 &lt;td&gt;Amlogic S922X&lt;/td&gt;
 &lt;td&gt;Broadcom BCM2712&lt;/td&gt;
 &lt;td&gt;Rockchip RK3588S2&lt;/td&gt;
 &lt;td&gt;Rockchip RK3588&lt;/td&gt;
 &lt;td&gt;Broadcom BCM2712&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CPU Architecture&lt;/td&gt;
 &lt;td&gt;4× Cortex-A73 + 2× Cortex-A53&lt;/td&gt;
 &lt;td&gt;4× Cortex-A76&lt;/td&gt;
 &lt;td&gt;4× Cortex-A76 + 4× Cortex-A55&lt;/td&gt;
 &lt;td&gt;4× Cortex-A76 + 4× Cortex-A55&lt;/td&gt;
 &lt;td&gt;4× Cortex-A76&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Memory&lt;/td&gt;
 &lt;td&gt;4 GB&lt;/td&gt;
 &lt;td&gt;8 GB&lt;/td&gt;
 &lt;td&gt;16 GB&lt;/td&gt;
 &lt;td&gt;16 GB&lt;/td&gt;
 &lt;td&gt;16 GB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Storage Type&lt;/td&gt;
 &lt;td&gt;microSD&lt;/td&gt;
 &lt;td&gt;NVMe&lt;/td&gt;
 &lt;td&gt;NVMe + eMMC&lt;/td&gt;
 &lt;td&gt;NVMe&lt;/td&gt;
 &lt;td&gt;NVMe&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Storage Capacity&lt;/td&gt;
 &lt;td&gt;128 GB&lt;/td&gt;
 &lt;td&gt;512 GB&lt;/td&gt;
 &lt;td&gt;512 GB + 64 GB&lt;/td&gt;
 &lt;td&gt;512 GB&lt;/td&gt;
 &lt;td&gt;256 GB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Operating System&lt;/td&gt;
 &lt;td&gt;Armbian&lt;/td&gt;
 &lt;td&gt;Ubuntu Server&lt;/td&gt;
 &lt;td&gt;Armbian&lt;/td&gt;
 &lt;td&gt;Armbian&lt;/td&gt;
 &lt;td&gt;Ubuntu Server&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="retired-systems"&gt;Retired Systems
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Hostname&lt;/th&gt;
 &lt;th&gt;ferrum&lt;/th&gt;
 &lt;th&gt;thorium&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;SBC Model&lt;/td&gt;
 &lt;td&gt;Radxa Rock Pi S&lt;/td&gt;
 &lt;td&gt;FriendlyElec NanoPi Neo&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SoC&lt;/td&gt;
 &lt;td&gt;Rockchip RK3308&lt;/td&gt;
 &lt;td&gt;Allwinner H3&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Architecture&lt;/td&gt;
 &lt;td&gt;64-bit (ARMv8-A)&lt;/td&gt;
 &lt;td&gt;32-bit (ARMv7-A)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CPU&lt;/td&gt;
 &lt;td&gt;4× Cortex-A35 @ 1.3 GHz&lt;/td&gt;
 &lt;td&gt;4× Cortex-A7 @ 1.2 GHz&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Memory&lt;/td&gt;
 &lt;td&gt;512 MB DDR3&lt;/td&gt;
 &lt;td&gt;512 MB DDR3&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Storage Type&lt;/td&gt;
 &lt;td&gt;microSD&lt;/td&gt;
 &lt;td&gt;microSD&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Storage Capacity&lt;/td&gt;
 &lt;td&gt;64 GB&lt;/td&gt;
 &lt;td&gt;64 GB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Operating System&lt;/td&gt;
 &lt;td&gt;DietPi&lt;/td&gt;
 &lt;td&gt;DietPi&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="objectives"&gt;Objectives
&lt;/h2&gt;&lt;p&gt;The goal of this project is to build a robust Kubernetes cluster capable of hosting both personal and widely used applications and databases. The cluster should also include a complete platform engineering stack, featuring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;comprehensive observability,&lt;/li&gt;
&lt;li&gt;CI/CD pipelines,&lt;/li&gt;
&lt;li&gt;GitOps-driven deployments.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="proposed-node-roles"&gt;Proposed Node Roles
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;iridium&lt;/strong&gt; — Kubernetes control plane,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;palladium&lt;/strong&gt; — lightweight worker node and test environment,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vanadium&lt;/strong&gt; — CI/CD workloads,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tantalum&lt;/strong&gt; — observability stack,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ruthenium&lt;/strong&gt; — heavy worker node for resource-intensive web applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="tooling"&gt;Tooling
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;k3s&lt;/strong&gt; — lightweight Kubernetes distribution,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ansible&lt;/strong&gt; — infrastructure automation and configuration management.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>2026-06-06</title><link>https://lvajxi03.github.io/posts/2026-06-06/</link><pubDate>Sat, 06 Jun 2026 16:15:00 +0200</pubDate><guid>https://lvajxi03.github.io/posts/2026-06-06/</guid><description>&lt;p&gt;This blog is designed to be a pragmatic engineering log. If you are passionate about open-source infrastructure, automation, and writing efficient software, you will feel right at home.&lt;/p&gt;
&lt;p&gt;This will also be a place for retro-lovers (&lt;strong&gt;C64&lt;/strong&gt;, &lt;strong&gt;ZX Spectrum&lt;/strong&gt;, etc)&lt;/p&gt;
&lt;h3 id="what-we-will-explore-here"&gt;What We Will Explore Here
&lt;/h3&gt;&lt;p&gt;Moving forward, the content will focus on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Software Engineering&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware&lt;/strong&gt; &amp;ndash; arm64 based SoCs and SBCs&lt;/li&gt;
&lt;li&gt;Linux &amp;amp; BSD Systems&lt;/li&gt;
&lt;li&gt;Automation &amp;amp; Orchestration&lt;/li&gt;
&lt;li&gt;CI/CD&lt;/li&gt;
&lt;li&gt;Cloud Native&lt;/li&gt;
&lt;li&gt;Observability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retro Gaming&lt;/strong&gt; &amp;ndash; hardware, software, emulation&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>