~/blog/local-kubernetes-with-k3d

Running Local Kubernetes with k3d: Fast, Ephemeral, and Kind to Your Battery

7 min read

Every developer's laptop has a local Kubernetes cluster. Most of them are abandoned within a fortnight because they eat RAM, fight Docker for network ports, and run background agents that cut battery life in half while you're presenting in front of a client.

k3d is different. It wraps k3s — the lightweight Kubernetes distribution from Rancher — inside Docker containers. The entire cluster runs as a set of Docker containers. When you're done, you delete those containers. Nothing persists. Nothing idles in the background consuming 4 GB you didn't allocate to it. The next morning you run one command and you have a clean cluster again in under 30 seconds.


Why Most Local Kubernetes Setups Fail

The standard advice is to install minikube or Docker Desktop's built-in Kubernetes. Both are fine for getting started. Neither is fine for daily platform engineering work.

Minikube runs a VM. That VM has a fixed memory allocation you set once at creation time and cannot easily adjust. If you set it too low, workloads OOM. If you set it high, it's consuming that RAM whether you're using Kubernetes or not. The VM starts with your machine by default. It has a persistent disk image. Deleting a minikube cluster is not guaranteed to clean up everything it touched.

Docker Desktop's Kubernetes is worse for iteration. It shares the Docker daemon, which means its networking can interfere with containers you're running outside Kubernetes. You can't create multiple clusters. You can't script cluster creation in CI. The configuration lives in a GUI settings pane, not in a file you can commit.

k3d solves both problems by having no persistent state that isn't a Docker container. The entire cluster is containers. Containers are deleteable. That's the deal.


What k3d Actually Does

k3s is a CNCF-certified Kubernetes distribution that strips out the components most people don't need in a local or edge context — in-tree cloud provider integrations, most alpha features, embedded etcd replaced with SQLite by default. What's left is a valid Kubernetes cluster that starts in seconds and uses a fraction of the RAM.

k3d is a CLI wrapper that:

  • Creates Docker containers running k3s server/agent binaries
  • Sets up a network between them
  • Configures a load balancer container (Traefik or NGINX) if you want one
  • Merges the kubeconfig into your local ~/.kube/config
  • Tears all of it down with a single command

Nothing else runs on your system. No system daemon. No background agent. No persistent volume on the host unless you explicitly mount one.


Installation

brew install k3d kubectl helm
k3d version  # ≥ v5.6.0
brew install k3d kubectl helm
k3d version  # ≥ v5.6.0

Docker Desktop must be running. Allocate at least 8 GB RAM in Docker Desktop → Settings → Resources — not for k3d specifically, but because the LGTM stack later in this series needs it.


The Cluster Config

Don't use k3d cluster create with inline flags. Write a config file and commit it. You will recreate this cluster many times.

k3d-config.yaml
apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
  name: platform-local
servers: 1
agents: 2
options:
  k3s:
    extraArgs:
      - arg: "--disable=traefik"
        nodeFilters:
          - server:*
volumes:
  - volume: /tmp/k3d-local:/var/lib/rancher/k3s/storage
    nodeFilters:
      - all
k3d-config.yaml
apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
  name: platform-local
servers: 1
agents: 2
options:
  k3s:
    extraArgs:
      - arg: "--disable=traefik"
        nodeFilters:
          - server:*
volumes:
  - volume: /tmp/k3d-local:/var/lib/rancher/k3s/storage
    nodeFilters:
      - all

A few decisions worth explaining:

One server, two agents. The server runs the control plane (API server, scheduler, controller manager). The agents run your workloads. Two agents means you can test pod anti-affinity, node selectors, and topology spread constraints without faking it. More agents cost more memory; two is the right default.

Traefik disabled. k3s ships with Traefik as its default ingress controller. We're going to install Envoy Gateway and use the Gateway API instead — that's the next article in this series. Two ingress controllers will fight each other. Disable the one you didn't choose.

Volume mount. /tmp/k3d-local gives persistent storage to the cluster. Without it, every PersistentVolumeClaim survives only as long as the Docker container does. With it, Prometheus data and Loki indices survive cluster restarts.

Create the cluster:

k3d cluster create --config k3d-config.yaml
kubectl config use-context k3d-platform-local
kubectl get nodes
k3d cluster create --config k3d-config.yaml
kubectl config use-context k3d-platform-local
kubectl get nodes

Three nodes: one server, two agents, all Ready within 20–30 seconds.


Day-to-Day Workflow

Load local images into the cluster

When you build an image locally, it lives in Docker's image store. The k3d containers don't have access to it — they have their own containerd runtime. Import images explicitly:

docker build -t my-service:dev .
k3d image import my-service:dev --cluster platform-local
docker build -t my-service:dev .
k3d image import my-service:dev --cluster platform-local

After import, reference my-service:dev in your manifests with imagePullPolicy: Never. The image is already there; no registry needed.

Resetting cleanly

When a cluster gets messy — conflicting CRDs, botched Helm releases, experiments gone sideways — don't debug it. Delete it and start fresh. That's the point.

k3d cluster delete platform-local
k3d cluster create --config k3d-config.yaml
k3d cluster delete platform-local
k3d cluster create --config k3d-config.yaml

Thirty seconds. Clean state. This is not a practice drill for production; this is the actual workflow. The config is committed. Recreating is not a cost.

Multiple clusters

k3d handles multiple simultaneous clusters cleanly because each is an isolated set of Docker containers with separate networks. If you want to test cross-cluster federation or demonstrate multi-cluster service mesh, create two:

k3d cluster create --config k3d-config.yaml
k3d cluster create --config k3d-config-secondary.yaml
kubectx  # switch between them
k3d cluster create --config k3d-config.yaml
k3d cluster create --config k3d-config-secondary.yaml
kubectx  # switch between them

Both clusters run concurrently. Both can be deleted independently.


k3d vs kind vs minikube

These tools are not interchangeable. Here's the honest trade-off:

k3dkindminikube
Startup time~20s~30s~90s (VM)
Memory overheadLow (k3s)Medium (kubeadm)High (VM)
Multi-clusterEasyEasyAwkward
Image loadingk3d image importkind load docker-imageminikube image load
Default storagelocal-pathlocal-pathhostPath
Upstream CI usageNoYes (k8s SIG)No
Production similarityk3s (not standard)kubeadm (closer)kubeadm (closer)

Use kind if your primary goal is testing Kubernetes conformance or if you're contributing to upstream Kubernetes — kind is what Kubernetes' own CI uses. Use k3d if your primary goal is running workloads locally with minimal resource cost and fast iteration. Use minikube if you need GPU passthrough or specific VM networking features.

For platform engineering work — running Helm charts, testing operators, iterating on manifests — k3d is the right tool.


Teardown

k3d cluster delete platform-local
rm -rf /tmp/k3d-local
k3d cluster delete platform-local
rm -rf /tmp/k3d-local

Nothing left on your system except the k3d binary and the config file you committed.


What's Next

A cluster with no routing layer is a cluster where everything needs a port-forward. The next article in this series sets up the Kubernetes Gateway API with Envoy — host-based routing across all services, the way a real platform would expose them, running entirely on this cluster.

Gateway API in Practice: From Ingress Migration to Envoy Debugging →