<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>HomeLab on dev@iostream.pl</title><link>https://dev.iostream.pl/categories/homelab/</link><description>Recent content in HomeLab on dev@iostream.pl</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Wed, 17 Jun 2026 08:15:00 +0200</lastBuildDate><atom:link href="https://dev.iostream.pl/categories/homelab/index.xml" rel="self" type="application/rss+xml"/><item><title>2026-06-17</title><link>https://dev.iostream.pl/posts/2026-06-17/</link><pubDate>Wed, 17 Jun 2026 08:15:00 +0200</pubDate><guid>https://dev.iostream.pl/posts/2026-06-17/</guid><description>&lt;p&gt;Continuation of the post &lt;a class="link" href="https://dev.iostream.pl/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://dev.iostream.pl/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://dev.iostream.pl/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://dev.iostream.pl/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://dev.iostream.pl/posts/2026-06-15/</link><pubDate>Mon, 15 Jun 2026 19:15:00 +0200</pubDate><guid>https://dev.iostream.pl/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://dev.iostream.pl/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://dev.iostream.pl/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://dev.iostream.pl/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://dev.iostream.pl/posts/2026-06-12/</link><pubDate>Fri, 12 Jun 2026 19:15:00 +0200</pubDate><guid>https://dev.iostream.pl/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></channel></rss>