<?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: Metronom</title>
    <description>The latest articles on DEV Community by Metronom (@mtrnm).</description>
    <link>https://dev.to/mtrnm</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4037870%2F0250143b-258c-4970-929e-847fa83a53e4.png</url>
      <title>DEV Community: Metronom</title>
      <link>https://dev.to/mtrnm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mtrnm"/>
    <language>en</language>
    <item>
      <title>k3d for Local Kubernetes: How We Cut Cluster Startup From 3 Minutes to Under 10 Seconds</title>
      <dc:creator>Metronom</dc:creator>
      <pubDate>Sun, 02 Aug 2026 08:39:17 +0000</pubDate>
      <link>https://dev.to/mtrnm/k3d-for-local-kubernetes-how-we-cut-cluster-startup-from-3-minutes-to-under-10-seconds-3ba2</link>
      <guid>https://dev.to/mtrnm/k3d-for-local-kubernetes-how-we-cut-cluster-startup-from-3-minutes-to-under-10-seconds-3ba2</guid>
      <description>&lt;p&gt;k3d for local Kubernetes turned "reset your cluster" from a coffee-break chore into a one-line reflex. Our onboarding doc had a line I grew to hate: "if your local cluster is acting weird, delete it and start over (grab a coffee, this takes a few minutes)." On minikube and Docker Desktop's built-in Kubernetes, that was literally minutes of VM boot and a laptop that sounded like it was preparing for liftoff. Here's the switch, the command, and the gotchas.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;Recreating a cluster was expensive, so people avoided it. They nursed broken clusters with hacks instead of resetting to a known-good state, which manufactured "works on my machine" bugs that cost even more time. Multiply a few resets a day across eight engineers: real hours burned on a thing that should be instant.&lt;/p&gt;

&lt;p&gt;I wanted local Kubernetes disposable — cheap enough that recreating it is boring. There's a solid walkthrough of standing up a real local cluster in a couple of minutes at &lt;a href="https://dorokhovich.com/blog/local-k8s-cluster-with-k3d?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=local-k8s-cluster-with-k3d" rel="noopener noreferrer"&gt;this k3d cluster writeup&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What k3d actually is
&lt;/h2&gt;

&lt;p&gt;The naming trips everyone up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;k3s&lt;/strong&gt; — Rancher's minimalist Kubernetes distro that packs the whole control plane (API server, scheduler, controllers) into one binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;k3d&lt;/strong&gt; — a lightweight wrapper that runs k3s nodes as ordinary Docker containers. "Little helper to run CNCF's k3s in Docker" (&lt;a href="https://github.com/k3d-io/k3d" rel="noopener noreferrer"&gt;the k3d project&lt;/a&gt;). k3d isn't Kubernetes; it orchestrates k3s-in-Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lightness has a concrete source. Standard Kubernetes stores cluster state in &lt;strong&gt;etcd&lt;/strong&gt;, a distributed key-value store you stand up and babysit. k3s defaults to an embedded &lt;strong&gt;SQLite&lt;/strong&gt; file instead — no etcd cluster to maintain, so startup is fast and memory modest (&lt;a href="https://docs.k3s.io/datastore" rel="noopener noreferrer"&gt;k3s datastore docs&lt;/a&gt;). For a single local cluster the trade-off is invisible; the payoff is that up and down are so cheap that recreating from scratch is routine, not scary. (SQLite can't back a multi-server cluster — for HA, k3s uses embedded etcd via &lt;code&gt;--cluster-init&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why k3d over kind and minikube
&lt;/h2&gt;

&lt;p&gt;Match each tool to a job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;k3d&lt;/strong&gt; — speed and lightness; the daily inner loop and budget CI where you spin clusters up and down constantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;kind&lt;/strong&gt; — maximum prod parity; runs clusters as Docker "nodes," built to test Kubernetes itself, which is why the project uses it for its own CI (&lt;a href="https://kind.sigs.k8s.io/" rel="noopener noreferrer"&gt;kind docs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;minikube&lt;/strong&gt; — learning and realistic simulation, VM-level isolation, rich addons.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more names show up in every 2026 local-Kubernetes comparison: &lt;strong&gt;MicroK8s&lt;/strong&gt; (Canonical's snap-based single package, great on Ubuntu) and &lt;strong&gt;k0s&lt;/strong&gt; (another single-binary distro). Both solid; we skipped them because k3d's Docker-native, throwaway-cluster model fit our all-macOS, container-first workflow best. On Ubuntu or wanting a systemd service instead of containers, MicroK8s is worth a look.&lt;/p&gt;

&lt;p&gt;Our bottleneck was iteration speed, not conformance testing. The honest caveat: because it's k3s and not full upstream Kubernetes, some advanced edge cases and policies (audit logging, for instance) behave differently and need explicit config. Our app never noticed; if yours needs byte-perfect parity, that's what kind is for. The &lt;a href="https://k3d.io/stable/" rel="noopener noreferrer"&gt;k3d documentation&lt;/a&gt; is the reference I send people who ask which local Kubernetes to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one command we standardized on
&lt;/h2&gt;

&lt;p&gt;This line replaced our whole onboarding paragraph — one control-plane node, two workers, a built-in registry, and a forwarded ingress port in a single shot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;k3d cluster create dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--servers&lt;/span&gt; 1 &lt;span class="nt"&gt;--agents&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--registry-create&lt;/span&gt; k3d-registry.localhost:5000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"8081:80@loadbalancer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flag by flag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--servers 1 --agents 2&lt;/code&gt; — one control plane, two workers, so &lt;code&gt;kubectl get nodes&lt;/code&gt; shows three &lt;code&gt;Ready&lt;/code&gt; nodes and you get real multi-node scheduling.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--registry-create k3d-registry.localhost:5000&lt;/code&gt; — a built-in image registry next to the cluster, so you don't round-trip a public registry on every iteration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-p "8081:80@loadbalancer"&lt;/code&gt; — forwards host 8081 to port 80 inside the cluster via k3d's built-in serverlb, which sits in front of Traefik (k3s's default ingress). App reachable at &lt;code&gt;http://localhost:8081&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To match a prod version, pin the k3s image: &lt;code&gt;k3d cluster create dev --image rancher/k3s:v1.31.5-k3s1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two gotchas that would have burned us
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The context is named &lt;code&gt;k3d-dev&lt;/code&gt;, not &lt;code&gt;dev&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When k3d creates a cluster it adds a kubectl context prefixed with &lt;code&gt;k3d-&lt;/code&gt; and switches to it automatically. We put &lt;code&gt;kubectl config current-context&lt;/code&gt; in everyone's shell prompt after one engineer nearly ran a destructive command against the wrong cluster:&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="nv"&gt;$ &lt;/span&gt;kubectl config current-context
k3d-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking context before anything dangerous is now muscle memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Port forwards are frozen at creation time
&lt;/h3&gt;

&lt;p&gt;You cannot add a forwarded port to a running k3d cluster — you recreate it. Sounds painful until you remember recreation is cheap, which is the whole point. We plan ports up front (8081 ingress, 5000 registry) so we rarely need to.&lt;/p&gt;

&lt;p&gt;Registry subtlety: k3d prepends &lt;code&gt;k3d-&lt;/code&gt; to the registry name, so you push to the forwarded &lt;code&gt;localhost:5000&lt;/code&gt; from your machine and reference &lt;code&gt;k3d-registry.localhost:5000&lt;/code&gt; in manifests. On macOS we added &lt;code&gt;127.0.0.1 k3d-registry.localhost&lt;/code&gt; to &lt;code&gt;/etc/hosts&lt;/code&gt; for reliable resolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker tag myapp:dev localhost:5000/myapp:dev
docker push localhost:5000/myapp:dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The move that changed the workflow: disposable clusters
&lt;/h2&gt;

&lt;p&gt;When a cluster gets weird, we don't debug the cluster — we delete and recreate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;k3d cluster delete dev &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; k3d cluster create dev &lt;span class="nt"&gt;--agents&lt;/span&gt; 2 &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"8081:80@loadbalancer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean environment in one line, in seconds. That killed an entire class of "my local Kubernetes is haunted" tickets. The behavior change is bigger than the stopwatch: when a reset is free, people reset before they debug, so they're always debugging their app against a known-good cluster instead of fighting accumulated cruft. Cheap recreation is the actual product here, not raw startup speed. Runbook note: a registry created with &lt;code&gt;--registry-create&lt;/code&gt; is deleted with the cluster, so to survive a reset create it standalone with &lt;code&gt;k3d registry create&lt;/code&gt; and attach via &lt;code&gt;--registry-use&lt;/code&gt;. For our fast &lt;code&gt;dev&lt;/code&gt; loop a fresh registry every reset is a feature. Health check is &lt;code&gt;kubectl get nodes&lt;/code&gt; (expect three &lt;code&gt;Ready&lt;/code&gt;) and &lt;code&gt;kubectl cluster-info&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before / after
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;minikube / Docker Desktop K8s&lt;/th&gt;
&lt;th&gt;k3d&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fresh cluster startup&lt;/td&gt;
&lt;td&gt;minutes&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Reset my cluster" friction&lt;/td&gt;
&lt;td&gt;coffee-break chore&lt;/td&gt;
&lt;td&gt;routine one-liner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local image push loop&lt;/td&gt;
&lt;td&gt;public registry round-trip&lt;/td&gt;
&lt;td&gt;built-in &lt;code&gt;localhost:5000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle laptop resource use&lt;/td&gt;
&lt;td&gt;heavy (VM)&lt;/td&gt;
&lt;td&gt;light (containers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI cluster spin-up cost&lt;/td&gt;
&lt;td&gt;slow, expensive runners&lt;/td&gt;
&lt;td&gt;fast, cheap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding to a working cluster&lt;/td&gt;
&lt;td&gt;multi-step doc&lt;/td&gt;
&lt;td&gt;one command&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Guardrail
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confirm the context before every destructive command.&lt;/strong&gt; &lt;code&gt;k3d-dev&lt;/code&gt; vs a real cluster is one wrong &lt;code&gt;kubectl delete&lt;/code&gt; away from a bad day. Put it in the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide port layout at creation.&lt;/strong&gt; Forwards are immutable; plan ingress and registry ports once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recreate, don't nurse.&lt;/strong&gt; A haunted cluster is cheaper to delete than to diagnose. Reset early and often.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;k3s is not upstream Kubernetes.&lt;/strong&gt; For conformance-sensitive tests (audit policy, admission edge cases), keep a kind job around.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Standardize the single &lt;code&gt;cluster create&lt;/code&gt; command in the repo from day one, not after a month of everyone hand-rolling flags. We've since wired the same command into CI so every pipeline run gets a pristine ephemeral cluster, layered Tilt on top to take the image build-and-push loop off developers, and kept a kind-based job for the rare conformance-sensitive test where k3s parity isn't enough.&lt;/p&gt;

&lt;p&gt;Bottom line: if "recreate the local cluster" is a dreaded chore, the tooling is the problem — k3d makes clusters disposable, and disposable clusters make the whole team debug their app instead of their environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://k3d.io/stable/" rel="noopener noreferrer"&gt;k3d — official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/k3d-io/k3d" rel="noopener noreferrer"&gt;k3d — k3d-io/k3d (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.k3s.io/datastore" rel="noopener noreferrer"&gt;k3s — Cluster Datastore (SQLite / embedded etcd)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kind.sigs.k8s.io/" rel="noopener noreferrer"&gt;kind — Kubernetes IN Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dorokhovich.com/blog/local-k8s-cluster-with-k3d?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=success-story&amp;amp;utm_content=local-k8s-cluster-with-k3d" rel="noopener noreferrer"&gt;k3d local-cluster walkthrough (standardized command &amp;amp; recreation workflow)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>infrastructure</category>
      <category>kubernetes</category>
    </item>
  </channel>
</rss>
