<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tasos Tsournos</title>
    <description>The latest articles on DEV Community by Tasos Tsournos (@anavalo).</description>
    <link>https://dev.to/anavalo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1182686%2Fd63185dd-f180-4acf-9c18-5862364e9c67.jpeg</url>
      <title>DEV Community: Tasos Tsournos</title>
      <link>https://dev.to/anavalo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anavalo"/>
    <language>en</language>
    <item>
      <title>Build a Kubernetes Cluster at Home with Raspberry Pis</title>
      <dc:creator>Tasos Tsournos</dc:creator>
      <pubDate>Sun, 15 Mar 2026 17:40:01 +0000</pubDate>
      <link>https://dev.to/anavalo/building-a-kubernetes-cluster-at-home-with-raspberry-pis-1l8c</link>
      <guid>https://dev.to/anavalo/building-a-kubernetes-cluster-at-home-with-raspberry-pis-1l8c</guid>
      <description>&lt;p&gt;Most people interact with Kubernetes through the cloud. They probably are as close to the cluster as they are to their laptop. Using a cluster in the cloud with a web browser or terminal is fine but there is something more intimate or rewarding to deploying a kubernetes cluster on bare metal. And even more rewarding is physically unplugging a node and watch Kubernetes rebalancing the workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;p&gt;The total cost is roughly €300 depending on what you already own, the number of nodes you will choose to have and whether you will get a case to wrap everything nicely.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;4x Raspberry Pi 4 (or Pi 5) (4GB RAM or more).&lt;/strong&gt; With 4GB per board, there's enough headroom to run workloads on top. 8GB model is nice to have but not necessary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;4x microSD cards (32GB).&lt;/strong&gt; The OS, container images, and Kubernetes binaries all live on these. 32GB gives you comfortable room.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A multi-port USB-C charger (at least 15W per port).&lt;/strong&gt; Each Pi 4 draws up to 15W under load. A 4-port USB-C PD GaN charger powers the whole cluster from a single wall outlet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;4x USB-C cables&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;An ethernet switch.&lt;/strong&gt; &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;4x ethernet cables.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A microSD card reader, a monitor (the Pi 4 has two micro-HDMI ports) and a USB keyboard.&lt;/strong&gt; You'll only need the monitor and keyboard briefly during initial setup. Once SSH is configured, everything happens remotely from your laptop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A cluster case.&lt;/strong&gt; Something like the &lt;a href="https://www.amazon.com/UCTRONICS-Upgraded-Enclosure-Raspberry-Compatible/dp/B09S11Q684" rel="noopener noreferrer"&gt;UCTRONICS Raspberry Pi cluster enclosure&lt;/a&gt; keeps your four boards stacked neatly with proper airflow. Optional but recommended plus it looks great on a desk.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3pxhofm5ag3bguqxnaq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3pxhofm5ag3bguqxnaq.png" alt="Kubernetes cluster" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture We're Building
&lt;/h2&gt;

&lt;p&gt;Before we start plugging things in, let's understand what we're building and why it's shaped this way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9w11ph49e9zyoa2laphv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9w11ph49e9zyoa2laphv.png" alt="Architecture diagramm" width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pi 1 has a dual role. Its Wi-Fi interface connects to your home router and the internet. Its ethernet interface connects to a private switch where the three worker nodes live. Pi 1 acts as a gateway. It performs &lt;a href="https://www.cisco.com/site/us/en/learn/topics/networking/what-is-network-address-translation-nat.html" rel="noopener noreferrer"&gt;NAT&lt;/a&gt; (Network Address Translation) so the workers can reach the internet through it, the same way your home router lets your devices reach the internet through its single public IP.&lt;/p&gt;

&lt;p&gt;Why this topology? The workers live on an isolated &lt;code&gt;10.0.0.0/24&lt;/code&gt; subnet. This is deliberate. It mirrors how production clusters work where nodes sit in a private network and communicate over a dedicated fabric, not over the same Wi-Fi your phone uses. It also means your cluster won't interfere with other devices on your home network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Flash the Operating System
&lt;/h2&gt;

&lt;p&gt;We'll use &lt;strong&gt;Ubuntu Server&lt;/strong&gt; (64-bit, aarch64 edition) on all four Pis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing the SD Cards
&lt;/h3&gt;

&lt;p&gt;Use the &lt;a href="https://www.raspberrypi.com/software/" rel="noopener noreferrer"&gt;Raspberry Pi Imager&lt;/a&gt; to flash each card. Before writing, click the gear icon to pre-configure each node:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For every node:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable SSH with public-key authentication. Generate an ed25519 key pair on your laptop (&lt;code&gt;ssh-keygen -t ed25519&lt;/code&gt;) if you don't have one, and paste the public key here.&lt;/li&gt;
&lt;li&gt;Set the username.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Set unique hostnames for each node:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pi 1: &lt;code&gt;k8s-control&lt;/code&gt; (this will be our control plane and gateway)&lt;/li&gt;
&lt;li&gt;Pi 2: &lt;code&gt;worker-1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pi 3: &lt;code&gt;worker-2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pi 4: &lt;code&gt;worker-3&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flash each card and insert them into the Pis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Network Setup
&lt;/h2&gt;

&lt;p&gt;This phase is the foundation everything else depends on. If the nodes can't reliably talk to each other, nothing that follows will work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1. Configure the Gateway Node (Pi 1)
&lt;/h3&gt;

&lt;p&gt;Boot Pi 1 with a monitor and keyboard attached. First, set up the Wi-Fi interface so it connects to your home router, and give the ethernet interface a static IP that will serve as the gateway for the cluster network.&lt;/p&gt;

&lt;p&gt;Create a netplan configuration at &lt;code&gt;/etc/netplan/01-cluster.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;wifis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;wlan0&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;dhcp4&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;access-points&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_WIFI_SSID"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-wifi-password"&lt;/span&gt;
  &lt;span class="na"&gt;ethernets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;eth0&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;addresses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.0.0.1/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;netplan apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Pi 1 has two interfaces: &lt;code&gt;wlan0&lt;/code&gt; gets an IP from your home router (internet access), and &lt;code&gt;eth0&lt;/code&gt; is statically set to &lt;code&gt;10.0.0.1&lt;/code&gt; (the cluster's gateway address).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;10.0.0.0/24&lt;/code&gt;?&lt;/strong&gt; We make sure the range doesn't collide with the home network (which is probably &lt;code&gt;192.168.x.x&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2. Enable NAT (Network Address Translation)
&lt;/h3&gt;

&lt;p&gt;The workers will live on the &lt;code&gt;10.0.0.0/24&lt;/code&gt; network, but that network has no direct route to the internet. Pi 1 needs to act as a router.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enable IP forwarding&lt;/span&gt;
&lt;span class="c"&gt;# This tells the Linux kernel to route packets&lt;/span&gt;
&lt;span class="c"&gt;# between interfaces instead of dropping them&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'net.ipv4.ip_forward = 1'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/sysctl.d/ip-forward.conf
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;--system&lt;/span&gt;

&lt;span class="c"&gt;# Set up NAT masquerade&lt;/span&gt;
&lt;span class="c"&gt;# Rewrite source IPs from 10.0.0.x to Pi 1's&lt;/span&gt;
&lt;span class="c"&gt;# Wi-Fi address so the internet sees them as coming from Pi 1&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; iptables-persistent
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-t&lt;/span&gt; nat &lt;span class="nt"&gt;-A&lt;/span&gt; POSTROUTING &lt;span class="nt"&gt;-o&lt;/span&gt; wlan0 &lt;span class="nt"&gt;-j&lt;/span&gt; MASQUERADE
&lt;span class="nb"&gt;sudo &lt;/span&gt;netfilter-persistent save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this step, the workers could talk to each other and to Pi 1, but any attempt to reach the internet (which they'll need to pull container images) would silently fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3. Run a DHCP Server on Pi 1
&lt;/h3&gt;

&lt;p&gt;Instead of manually configuring a static IP on each worker, we'll run a &lt;a href="https://www.infoblox.com/glossary/dhcp-server/" rel="noopener noreferrer"&gt;DHCP server&lt;/a&gt; on Pi 1 that hands out addresses automatically. More importantly, we'll bind specific IPs to specific MAC addresses so that each worker always gets the same IP. Kubernetes nodes register with their IP, so you don't want those changing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; isc-dhcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;/etc/dhcp/dhcpd.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# The subnet the DHCP server manages
&lt;/span&gt;&lt;span class="n"&gt;subnet&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;netmask&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;.&lt;span class="m"&gt;255&lt;/span&gt;.&lt;span class="m"&gt;255&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt; {
  &lt;span class="n"&gt;range&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;50&lt;/span&gt;;        &lt;span class="c"&gt;# dynamic range for any new devices
&lt;/span&gt;  &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="n"&gt;routers&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;;           &lt;span class="c"&gt;# point clients to Pi 1 as gateway
&lt;/span&gt;  &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;-&lt;span class="n"&gt;name&lt;/span&gt;-&lt;span class="n"&gt;servers&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;.&lt;span class="m"&gt;8&lt;/span&gt;.&lt;span class="m"&gt;8&lt;/span&gt;.&lt;span class="m"&gt;8&lt;/span&gt;, &lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;;  &lt;span class="c"&gt;# public DNS servers
&lt;/span&gt;}

&lt;span class="c"&gt;# Static leases. Each worker always gets the same IP
# Find your Pi's MAC with: ip link show eth0
&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;-&lt;span class="m"&gt;1&lt;/span&gt; {
  &lt;span class="n"&gt;hardware&lt;/span&gt; &lt;span class="n"&gt;ethernet&lt;/span&gt; &lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;;  &lt;span class="c"&gt;# replace with Pi 2's eth0 MAC
&lt;/span&gt;  &lt;span class="n"&gt;fixed&lt;/span&gt;-&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;11&lt;/span&gt;;
}
&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;-&lt;span class="m"&gt;2&lt;/span&gt; {
  &lt;span class="n"&gt;hardware&lt;/span&gt; &lt;span class="n"&gt;ethernet&lt;/span&gt; &lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;;  &lt;span class="c"&gt;# replace with Pi 3's eth0 MAC
&lt;/span&gt;  &lt;span class="n"&gt;fixed&lt;/span&gt;-&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;12&lt;/span&gt;;
}
&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;-&lt;span class="m"&gt;3&lt;/span&gt; {
  &lt;span class="n"&gt;hardware&lt;/span&gt; &lt;span class="n"&gt;ethernet&lt;/span&gt; &lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;:&lt;span class="n"&gt;XX&lt;/span&gt;;  &lt;span class="c"&gt;# replace with Pi 4's eth0 MAC
&lt;/span&gt;  &lt;span class="n"&gt;fixed&lt;/span&gt;-&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;13&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tell the DHCP server to only listen on &lt;code&gt;eth0&lt;/code&gt; (we don't want it interfering with your home network's Wi-Fi). Edit &lt;code&gt;/etc/default/isc-dhcp-server&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;INTERFACESv4&lt;/span&gt;=&lt;span class="s2"&gt;"eth0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, there's a common boot-order problem: the DHCP server can start before the ethernet interface has its static IP assigned. When that happens, the server sees no valid address on &lt;code&gt;eth0&lt;/code&gt; and crashes. To fix this, create a systemd drop-in that makes the DHCP service wait for &lt;code&gt;eth0&lt;/code&gt; to be ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/systemd/system/isc-dhcp-server.service.d
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' | sudo tee /etc/systemd/system/isc-dhcp-server.service.d/wait-for-eth0.conf
[Unit]
After=sys-subsystem-net-devices-eth0.device
Wants=sys-subsystem-net-devices-eth0.device

[Service]
ExecStartPre=/bin/sh -c 'i=0; while [ &lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="sh"&gt; -lt 30 ]; do ip -4 addr show eth0 | grep -q inet &amp;amp;&amp;amp; exit 0; sleep 1; i=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;i+1&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="sh"&gt;; done; exit 1'
Restart=on-failure
RestartSec=5
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; isc-dhcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ExecStartPre&lt;/code&gt; script polls up to 30 seconds for &lt;code&gt;eth0&lt;/code&gt; to have an IPv4 address. The &lt;code&gt;Restart=on-failure&lt;/code&gt; is a safety net, if something else goes wrong, systemd will retry instead of leaving you with no DHCP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4. Boot the Workers and Verify
&lt;/h3&gt;

&lt;p&gt;Plug all three workers into the switch, power them on, and give them a minute to boot and request DHCP leases. From Pi 1, verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check DHCP leases were assigned&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /var/lib/dhcp/dhcpd.leases

&lt;span class="c"&gt;# Ping each worker&lt;/span&gt;
ping &lt;span class="nt"&gt;-c&lt;/span&gt; 2 10.0.0.11
ping &lt;span class="nt"&gt;-c&lt;/span&gt; 2 10.0.0.12
ping &lt;span class="nt"&gt;-c&lt;/span&gt; 2 10.0.0.13

&lt;span class="c"&gt;# Verify workers can reach the internet through Pi 1's NAT&lt;/span&gt;
ssh ubuntu@10.0.0.11 &lt;span class="s1"&gt;'ping -c 2 8.8.8.8'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5. SSH Config on Your Laptop
&lt;/h3&gt;

&lt;p&gt;You'll be managing this cluster from your laptop, not from a monitor plugged into Pi 1. The workers aren't directly reachable from your laptop (they're on the &lt;code&gt;10.0.0.0/24&lt;/code&gt; subnet, which your laptop doesn't know about), so we use Pi 1 as a jump host.&lt;/p&gt;

&lt;p&gt;Add this to &lt;code&gt;~/.ssh/config&lt;/code&gt; on your laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; k8s-control
  &lt;span class="k"&gt;HostName&lt;/span&gt; &amp;lt;Pi &lt;span class="m"&gt;1&lt;/span&gt;'s Wi-Fi IP&amp;gt;
  &lt;span class="k"&gt;User&lt;/span&gt; ubuntu
  &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519

&lt;span class="k"&gt;Host&lt;/span&gt; worker-1
  &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.0.0.11
  &lt;span class="k"&gt;User&lt;/span&gt; ubuntu
  &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519
  &lt;span class="k"&gt;ProxyJump&lt;/span&gt; k8s-control

&lt;span class="k"&gt;Host&lt;/span&gt; worker-2
  &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.0.0.12
  &lt;span class="k"&gt;User&lt;/span&gt; ubuntu
  &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519
  &lt;span class="k"&gt;ProxyJump&lt;/span&gt; k8s-control

&lt;span class="k"&gt;Host&lt;/span&gt; worker-3
  &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.0.0.13
  &lt;span class="k"&gt;User&lt;/span&gt; ubuntu
  &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519
  &lt;span class="k"&gt;ProxyJump&lt;/span&gt; k8s-control
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;ssh worker-2&lt;/code&gt; from your laptop will automatically tunnel through Pi 1. The &lt;code&gt;ProxyJump&lt;/code&gt; directive tells SSH to first connect to &lt;code&gt;k8s-control&lt;/code&gt; and then hop to the worker from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: Kubernetes Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before installing Kubernetes the worker nodes need a few changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1. Disable Swap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;swapoff &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'/swap/d'&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Swap lets the OS spill memory to disk when RAM is full. That's useful in general, but it undermines Kubernetes resource management. Disabling swap means a container that exceeds its memory limit gets OOMKilled immediately, which is easier to debug and more honest. Recent Kubernetes versions (1.28+) do support swap, but configuring it properly doesn't belong in a getting-started guide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2. Load Required Kernel Modules
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe overlay
&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe br_netfilter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;overlay&lt;/code&gt;?&lt;/strong&gt; Containerd (the container runtime) uses the OverlayFS filesystem driver to efficiently layer container images. Each container image is made of read-only layers stacked on top of each other with a thin writable layer on top. Without the &lt;code&gt;overlay&lt;/code&gt; module, containerd can't create containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;br_netfilter&lt;/code&gt;?&lt;/strong&gt; Kubernetes networking relies on Linux bridges to connect containers. By default, traffic traversing a bridge bypasses iptables rules. The &lt;code&gt;br_netfilter&lt;/code&gt; module makes bridged traffic visible to iptables, which is essential because Kubernetes Services (ClusterIP, NodePort) are implemented entirely as iptables rules by kube-proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3. Set Sysctl Parameters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;--system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the runtime settings that complement the kernel modules above. &lt;code&gt;bridge-nf-call-iptables&lt;/code&gt; enables the actual filtering we just made possible with &lt;code&gt;br_netfilter&lt;/code&gt;. &lt;code&gt;ip_forward&lt;/code&gt; allows packets to be routed between network interfaces. Without this, a pod on one node can't communicate with a pod on another node, because the kernel would refuse to forward the traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4: Container Runtime
&lt;/h2&gt;

&lt;p&gt;Kubernetes doesn't run containers itself. It delegates that to a container runtime. We'll use &lt;strong&gt;containerd&lt;/strong&gt;, which is the industry standard.&lt;/p&gt;

&lt;p&gt;Run this on every node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; containerd

&lt;span class="c"&gt;# Generate the default config file&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/containerd
containerd config default | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/containerd/config.toml

&lt;span class="c"&gt;# Enable the systemd cgroup driver&lt;/span&gt;
&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/SystemdCgroup = false/SystemdCgroup = true/'&lt;/span&gt; /etc/containerd/config.toml

&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart containerd
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;containerd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;SystemdCgroup = true&lt;/code&gt;?&lt;/strong&gt; &lt;a href="https://medium.com/@weidagang/linux-beyond-the-basics-cgroups-f157d93bd755" rel="noopener noreferrer"&gt;Linux cgroups&lt;/a&gt; are what enforce resource limits on containers. There are two ways to manage cgroups: the legacy &lt;code&gt;cgroupfs&lt;/code&gt; driver and the &lt;code&gt;systemd&lt;/code&gt; driver. The kubelet defaults to the systemd driver, so containerd must use the same one&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 5: Installing Kubernetes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1. Add the Kubernetes Repository (All Nodes)
&lt;/h3&gt;

&lt;p&gt;Kubernetes packages aren't in Ubuntu's default repos. We need to add the official &lt;code&gt;pkgs.k8s.io&lt;/code&gt; repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Import the signing key, this lets apt verify that packages&lt;/span&gt;
&lt;span class="c"&gt;# actually come from the Kubernetes project and haven't been tampered with&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/kubernetes-apt-keyring.gpg

&lt;span class="c"&gt;# Add the repo&lt;/span&gt;
&lt;span class="c"&gt;# The signed-by clause scopes the key to this repo only,&lt;/span&gt;
&lt;span class="c"&gt;# so it can't be used to authenticate packages from other sources&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/kubernetes.list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2. Install and Pin the Packages (All Nodes)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; kubelet kubeadm kubectl
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark hold kubelet kubeadm kubectl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What each package does:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;kubelet&lt;/strong&gt; is the node agent. It runs on every node as a systemd service, watches the API server for pod assignments, and tells containerd to start or stop containers accordingly. It's the bridge between Kubernetes (the abstract orchestration layer) and containerd (the thing that actually runs containers).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;kubeadm&lt;/strong&gt; think of it as the Kubernetes installer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;kubectl&lt;/strong&gt; is the CLI you use to interact with the cluster. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;apt-mark hold&lt;/code&gt;?&lt;/strong&gt; Kubernetes components must be upgraded in a specific order. If &lt;code&gt;apt-get upgrade&lt;/code&gt; quietly bumped kubectl to v1.33 while your kubelet is still on v1.32, you'd have a version skew that could cause subtle breakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 6: Initializing the Cluster
&lt;/h2&gt;

&lt;p&gt;This is where four independent Linux machines become a Kubernetes cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1. Initialize the Control Plane (Pi 1 Only)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;kubeadm init &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--apiserver-advertise-address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.0.0.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pod-network-cidr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.244.0.0/16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;--apiserver-advertise-address=10.0.0.1&lt;/code&gt;&lt;/strong&gt; tells the API server to bind to Pi 1's ethernet interface. Without this flag, kubeadm might auto-detect the Wi-Fi interface instead, and the workers (which are on the ethernet subnet) wouldn't be able to reach the API server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--pod-network-cidr=10.244.0.0/16&lt;/code&gt;&lt;/strong&gt; reserves this IP range for pod-to-pod networking. Every pod in the cluster gets an IP from this range. It's important that this doesn't overlap with your node subnet (&lt;code&gt;10.0.0.0/24&lt;/code&gt;) or the default Kubernetes service CIDR (&lt;code&gt;10.96.0.0/12&lt;/code&gt;). The &lt;code&gt;10.244.0.0/16&lt;/code&gt; range is the conventional default for Flannel, the CNI plugin we'll install next.&lt;/p&gt;

&lt;p&gt;When this command finishes, kubeadm will have done several things: generated all the TLS certificates under &lt;code&gt;/etc/kubernetes/pki/&lt;/code&gt;, written static pod manifests for etcd, the API server, the controller manager, and the scheduler into &lt;code&gt;/etc/kubernetes/manifests/&lt;/code&gt;, and started the kubelet which launched all of them as containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Save the &lt;code&gt;kubeadm join&lt;/code&gt; command&lt;/strong&gt; from the output, we'll need it in Step 4 to add the workers. &lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2. Configure kubectl (Pi 1)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; /etc/kubernetes/admin.conf &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kubeadm init&lt;/code&gt; generated an admin kubeconfig file that contains a client certificate and the cluster CA. This copies it to your home directory so you can run &lt;code&gt;kubectl&lt;/code&gt; as a normal user. Without this, you'd need &lt;code&gt;sudo&lt;/code&gt; for every kubectl command.&lt;/p&gt;

&lt;p&gt;Test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7364bjuewlaimc6p67z3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7364bjuewlaimc6p67z3.png" alt="k get nodes" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3. Install a CNI Plugin (Flannel)
&lt;/h3&gt;

&lt;p&gt;A CNI (Container Network Interface) plugin is what gives pods their IP addresses and enables pod-to-pod communication across nodes. Without one, the kubelet reports the node as NotReady because the networking requirement isn't satisfied.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command creates a DaemonSet that automatically runs one pod on every node. So as workers join, they'll each get a Flannel pod that configures their local networking.&lt;/p&gt;

&lt;p&gt;Flannel is one of the simplest CNI plugins. It creates a &lt;a href="https://networklessons.com/vxlan" rel="noopener noreferrer"&gt;VXLAN&lt;/a&gt; overlay network that tunnels pod traffic between nodes through the existing ethernet network. Each node gets a &lt;code&gt;/24&lt;/code&gt; slice of the &lt;code&gt;10.244.0.0/16&lt;/code&gt; range and Flannel handles the routing between them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4. Join the Workers (Pi 2, Pi 3, Pi 4)
&lt;/h3&gt;

&lt;p&gt;SSH into each worker and run the join command from Step 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;kubeadm &lt;span class="nb"&gt;join &lt;/span&gt;10.0.0.1:6443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--token&lt;/span&gt; &amp;lt;your-token&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--discovery-token-ca-cert-hash&lt;/span&gt; sha256:&amp;lt;your-hash&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens during a join: the worker contacts the API server at &lt;code&gt;10.0.0.1:6443&lt;/code&gt;, verifies the server's TLS certificate against the hash you provided, sends a certificate signing request which the control plane auto-approves and then starts its kubelet with the signed certificate. The kubelet registers the node with the API server and Kubernetes begins scheduling the Flannel DaemonSet pod onto it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5. Verify the Cluster
&lt;/h3&gt;

&lt;p&gt;Back on Pi 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After about a minute (while Flannel pulls the container images), all four nodes should show &lt;code&gt;Ready&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5raabbr1gnekyzh3u7i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5raabbr1gnekyzh3u7i.png" alt="k get nodes" width="800" height="115"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check that all system pods are running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1ygesmjus27n6e0pge0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1ygesmjus27n6e0pge0.png" alt="k get pods" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The control plane node has a taint (&lt;code&gt;node-role.kubernetes.io/control-plane:NoSchedule&lt;/code&gt;) that prevents regular workloads from being scheduled on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Have Now
&lt;/h2&gt;

&lt;p&gt;Congratulations! You have a functioning Kubernetes cluster. Specifically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A control plane&lt;/strong&gt; (Pi 1) running etcd, the API server, the controller manager and the scheduler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three worker nodes&lt;/strong&gt; (Pi 2–4) running kubelet and kube-proxy, ready to accept workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An overlay network&lt;/strong&gt; (Flannel) that gives every pod a unique IP and routes traffic between them, even across nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cluster DNS&lt;/strong&gt; (CoreDNS) so pods can find each other by name instead of IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Go Next
&lt;/h2&gt;

&lt;p&gt;This cluster is a foundation. Some next steps to consider:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add metrics server.&lt;/strong&gt; Then you will be able to check node and pod metrics like &lt;code&gt;kubectl top nodes&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy a real application.&lt;/strong&gt; Applications like Amazon's &lt;a href="https://github.com/aws-containers/retail-store-sample-app" rel="noopener noreferrer"&gt;retail store&lt;/a&gt; or Google's &lt;a href="https://github.com/GoogleCloudPlatform/microservices-demo" rel="noopener noreferrer"&gt;microservice demo&lt;/a&gt; is a good place to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try breaking things.&lt;/strong&gt; Drain a node (&lt;code&gt;kubectl drain&lt;/code&gt;), scale a deployment to more replicas than the cluster can handle, set resource limits and watch OOMKill, delete a pod and watch its controller recreate it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set up an Ingress controller.&lt;/strong&gt; Install something like Traefik or ingress-nginx to route external HTTP traffic to services inside the cluster. This is how production clusters expose web applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install a GitOps tool.&lt;/strong&gt; Tools like ArgoCD let you declare your desired cluster state in a Git repository and have it automatically applied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add persistent storage.&lt;/strong&gt; Explore local-path-provisioner or NFS to give your pods storage that survives pod restarts. Stateful workloads (databases, message queues) need this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Epilogue
&lt;/h2&gt;

&lt;p&gt;It may look overwhelming but in reality it's nothing more than a weekend project. It's the closest you'll get to &lt;em&gt;hands on&lt;/em&gt; Kubernetes experience. Pun intended. Have fun!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>raspberrypi</category>
      <category>devops</category>
    </item>
    <item>
      <title>The lost logos of code</title>
      <dc:creator>Tasos Tsournos</dc:creator>
      <pubDate>Sat, 20 Dec 2025 11:01:23 +0000</pubDate>
      <link>https://dev.to/anavalo/the-lost-logos-of-code-28eg</link>
      <guid>https://dev.to/anavalo/the-lost-logos-of-code-28eg</guid>
      <description>&lt;p&gt;Does my code these days reflect &lt;em&gt;&lt;em&gt;me&lt;/em&gt;&lt;/em&gt;? &lt;/p&gt;

&lt;p&gt;I need to do a brief philosophical detour before circling back to the question.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Logos&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;What is &lt;em&gt;logos&lt;/em&gt;? We translate from greek weakly as "word" or "reason," but as philosopher Christos Yannaras defines it, "logos is the mode by which everything that is becomes manifest, becomes known" (Yannaras, quoted in Mitralexis, Ever-Moving Repose, 79; cf. Yannaras, Person and Eros, 159–72).&lt;/p&gt;

&lt;p&gt;Logos is inherently relational. The logos of a thing "speaks" to an observer, disclosing that thing's identity and substance. &lt;/p&gt;

&lt;p&gt;What makes beings "logical" in this original sense is that they are not silent, neutral objects. They are "effected words" that signify personal creative activity (Yannaras, Elements of Faith, 40–41).&lt;/p&gt;

&lt;p&gt;Consider Van Gogh. You can memorize every biographical fact, yet you only &lt;em&gt;know&lt;/em&gt; his personal otherness by standing before his paintings. His logos emerges through working against the material. The thickness of the oil paint, the brush work against the paint, the colors. Through that struggle, through this "dialogue" his logos appears.&lt;/p&gt;

&lt;p&gt;The same with music: we discern the otherness of Mozart by participating in the logos of his work. This is how we distinguish it from Bach. Τhis "empirical recognition of the otherness of the artist's creative logos is a cognitive event that is valid and true," as Mitralexis summarizes (Ever-Moving Repose, 35).&lt;/p&gt;

&lt;p&gt;This isn't mere observation. It's participatory knowledge. A dialogue between person and thing. "Only the creation's logos can 'signify' the reality of the subject, its otherness" (Mitralexis, Ever-Moving Repose, 35).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Coding with AI&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Before AI agents, claude code and chatGPT code was the logos of the software engineer. &lt;/p&gt;

&lt;p&gt;The way you architected a codebase, the names you chose for variables, the elegance of your logic, these disclosed your personal creative activity. Another engineer could read your code and participate in your technical energy, distinguishing your "word" from another developer's, just as we distinguish Van Gogh from Monet.&lt;/p&gt;

&lt;p&gt;Now, code increasingly loses its logos. It becomes the output of Claude Code, of Gemini or Codex. The engineer describes an intent; the machine produces code. &lt;/p&gt;

&lt;p&gt;The code is no longer an "effected word" signifying personal creative activity. It becomes a neutral object, optimized for function.&lt;/p&gt;

&lt;p&gt;This is equivalent to a poet telling ChatGPT about a poem, or a painter describing to Gemini's banana what they want to paint. The output may be competent, even evoke awe, but whose logos does it carry?&lt;/p&gt;

&lt;p&gt;When we read AI-generated code, we are not participating in the creative energy of the engineer. We observe the statistical residue of all the engineers. The dialogue has collapsed. The resistance of the material has been smoothed away. &lt;/p&gt;

&lt;p&gt;I will quote Yannaras once again: "Today our relationship with the world is becoming more and more an indirect relationship—the machine intervenes to subordinate nature and her forces to the demands of understanding, negating the resistance which the material can offer to the efficiency of our own programming." (Yannaras, Elements of Faith, 51–52).&lt;/p&gt;

&lt;p&gt;The code works and it often works quite well.&lt;/p&gt;

&lt;p&gt;But it carries no personal logos. The human presence is not there. &lt;em&gt;I'm&lt;/em&gt; not there.&lt;/p&gt;

&lt;p&gt;How do we get the relationship back?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A reflection on logos, craft, and the changing relationship between humans and the things they make.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yannaras, Christos. &lt;em&gt;Person and Eros&lt;/em&gt;. Holy Cross Orthodox Press, 2007.&lt;/li&gt;
&lt;li&gt;Yannaras, Christos. &lt;em&gt;Elements of Faith: An Introduction to Orthodox Theology&lt;/em&gt;. T&amp;amp;T Clark, 1991.&lt;/li&gt;
&lt;li&gt;Mitralexis, Sotiris. &lt;em&gt;Ever-Moving Repose: A Contemporary Reading of Maximus the Confessor's Theory of Time&lt;/em&gt;. Cascade Books, 2017.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>chatgpt</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Conditional React props with TypeScript Function Overloading</title>
      <dc:creator>Tasos Tsournos</dc:creator>
      <pubDate>Thu, 19 Oct 2023 12:37:55 +0000</pubDate>
      <link>https://dev.to/anavalo/enforcing-prop-combinations-with-typescript-function-overloading-in-react-4ob2</link>
      <guid>https://dev.to/anavalo/enforcing-prop-combinations-with-typescript-function-overloading-in-react-4ob2</guid>
      <description>&lt;p&gt;Optional props in TypeScript interfaces give you some freedom when crafting components. However, this freedom can also create confusion and weaken the integrity of your components. TypeScript function overloading is a way to specify which props are meant to be used together, making your components foolproof.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Optional Props
&lt;/h2&gt;

&lt;p&gt;Optional props offer customization but can lead to unclear combinations of props, creating room for mistakes. For instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Using the Button component with optional props&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Click Me&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;handleClickIcon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;yourClickHandler&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;handleClickIcon&lt;/code&gt; is specified, but &lt;code&gt;icon&lt;/code&gt; is not. It's unclear whether this is intentional or a mistake, and TypeScript won't complain either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;Button&lt;/code&gt; Component
&lt;/h2&gt;

&lt;p&gt;Let's examine a &lt;code&gt;Button&lt;/code&gt; component that can optionally include an icon. The component should only accept an &lt;code&gt;icon&lt;/code&gt; prop if a &lt;code&gt;handleClickIcon&lt;/code&gt; function is also present.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Basic Way: Using Optional Props
&lt;/h3&gt;

&lt;p&gt;The props could be defined as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ButtonProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;handleClickIcon&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won't enforce that &lt;code&gt;handleClickIcon&lt;/code&gt; is relevant only when you've got an &lt;code&gt;icon&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leveling Up: Function Overloading
&lt;/h3&gt;

&lt;p&gt;A better strategy is to use function overloading in TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CommonProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;IconAndClickProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;handleClickIcon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// function overloading&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommonProps&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommonProps&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;IconAndClickProps&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommonProps&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;IconAndClickProps&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;JSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Element&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="c1"&gt;// ...your button implementation&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you'll get a TypeScript error if you try to use &lt;code&gt;handleClickIcon&lt;/code&gt; without supplying an &lt;code&gt;icon&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Just Text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;With Icon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IconComponent&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;handleClickIcon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;yourClickHandler&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're basically instructing TypeScript to allow either Set A or Set B of props. No mixing allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up: Stronger, Self-Documenting Components
&lt;/h2&gt;

&lt;p&gt;Function overloading does more than enforce types. It also acts as a form of built-in documentation and makes your components less prone to errors. So, it's a technique you should think about for crafting clear and robust React components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads" rel="noopener noreferrer"&gt;TypeScript Handbook: Function Overloading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give function overloading a try!&lt;/p&gt;

&lt;p&gt;Feel free to comment and share!&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>webdev</category>
      <category>codequality</category>
    </item>
    <item>
      <title>The Underappreciated Power of TypeScript Interfaces: Going Beyond "Interface vs Type"</title>
      <dc:creator>Tasos Tsournos</dc:creator>
      <pubDate>Wed, 11 Oct 2023 19:20:46 +0000</pubDate>
      <link>https://dev.to/anavalo/the-underappreciated-power-of-typescript-interfaces-going-beyond-interface-vs-type-53i</link>
      <guid>https://dev.to/anavalo/the-underappreciated-power-of-typescript-interfaces-going-beyond-interface-vs-type-53i</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the TypeScript community, there's often a focus on comparing "interfaces" and "types," dissecting their differences, and weighing which is better for different scenarios. While that discussion is invaluable, it tends to overshadow another powerful aspect of TypeScript interfaces—its role in Object-Oriented Programming (OOP).&lt;/p&gt;

&lt;p&gt;In TypeScript, an interface can do more than just define the shape of an object; it can serve as a robust contract that classes must adhere to. This article aims to shed some light on this underappreciated feature, demonstrating its practical applications and benefits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Usually Discussed: Interface vs Type
&lt;/h2&gt;

&lt;p&gt;Before diving into the OOP aspect, let's briefly discuss what usually garners attention. Developers often compare "interface" and "type" to decide which is best for their needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interface&lt;/strong&gt; can be extended and implemented by classes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt; is more flexible and can represent primitive types, union types, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decision usually boils down to specific use-cases. But by focusing solely on this dichotomy, we miss out on the OOP benefits of using interfaces in TypeScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OOP Benefit: Implementing Interfaces in Classes
&lt;/h2&gt;

&lt;p&gt;When working with classes in TypeScript, you can use interfaces to ensure that a class adheres to a particular contract. This approach offers several benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compile-Time Checking&lt;/strong&gt;: TypeScript checks your classes at compile-time to ensure they adhere to the contract, catching issues early on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Modularity&lt;/strong&gt;: Interfaces encourage clean, modular code that's easier to reason about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readability&lt;/strong&gt;: Using a well-named interface makes your code more self-explanatory, enhancing readability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Runnable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Human&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;Runnable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Ran &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; meters`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Car&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;Runnable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Drove &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; meters`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;executeRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runnable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Runnable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runnable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;executeRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Human&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;executeRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Car&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advanced Use Cases: Design Patterns
&lt;/h2&gt;

&lt;p&gt;The practical applications of interfaces don't stop here. If you're familiar with design patterns like Strategy, Adapter, or Composite, you'll find that TypeScript interfaces are extremely useful for implementing these patterns effectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Interfaces in TypeScript offer more than just a way to define the shape of data. They serve as robust contracts that your classes can adhere to, bringing the benefits of strong typing to your OOP endeavors in TypeScript. By looking beyond the "interface vs type" debate, you can leverage the full potential of TypeScript interfaces to write clean, modular, and robust code.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>oop</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
