k3d: k3s in containers
The fastest local cluster you can get, what comes bundled with k3s out of the box, and why it's worth watching the project's release pace.
Why
k3d is a wrapper that runs k3s inside Docker containers. A two- or three-node cluster comes up on a laptop in about thirty seconds and goes away with one command. To understand the tool, you first need to understand k3s.
Concepts
k3s is a lightweight Kubernetes distribution. Same API, same objects, same manifests, but built into a single binary with a trimmed set of internal components. Out of the box it ships with an ingress controller, a load balancer for Service of type LoadBalancer, and a storage provisioner. k3s itself targets Linux and installs directly on the machine.
k3d takes that binary and packs the nodes into containers. The result is the same idea as kind, just with a different distribution inside. That is where the speed comes from: in the official minikube time-to-k8s benchmark (v1.36.0, linux/amd64), k3d v5.8.3 brings up a cluster in 19.9 seconds, against 33.0 for kind v0.29.0 and 50.8 for minikube.
k3s has its own terminology. Server is what regular Kubernetes calls the
control plane. Agent is a worker node. In front of the server nodes, k3d
puts another container, the load balancer (k3d-<name>-serverlb).
Requests to the API and every mapped port go through it.
Version and release pace
The current k3d version is v5.9.0, released June 2, 2026. By default it
ships Kubernetes v1.35.5, k3s image v1.35.5-k3s1. It requires Docker
20.10.5 or newer.
To be blunt: this project has the slowest release pace among the popular local-cluster tools. Almost 16 months passed between v5.8.3 (February 2025) and v5.9.0 (June 2026). The last commit in the repository is dated June 12, 2026.
That doesn't mean the project is dead: the release shipped, commits keep coming, the tool works. But the pace is noticeably slower than kind's, where releases land every few months. For local learning, a pause of almost a year and a half changes almost nothing. For something that lives for years and needs quick updates for new Kubernetes versions, the difference between the two projects already matters.
Installation
macOS and Linux with Homebrew:
brew install k3d
Linux and WSL2 without Homebrew, the official script with a pinned version:
curl -s -o install-k3d.sh https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh
less install-k3d.sh
TAG=v5.9.0 bash install-k3d.sh
Without TAG the script installs the latest release. Windows without
WSL2: choco install k3d. To check that everything is in place:
k3d version
k3d version v5.9.0
k3s version v1.35.5-k3s1 (default)
How it works
Cluster lifecycle
k3d cluster create lab --servers 1 --agents 2 -p "8081:80@loadbalancer" --api-port 6550
k3d cluster create lab --image rancher/k3s:v1.36.4-k3s1
k3d cluster list
k3d cluster delete lab
--servers 1 --agents 2 gives a three-node cluster: one server and two
agents. --api-port 6550 publishes the API server on host port 6550.
Without it, k3d picks a random free port.
The Kubernetes version isn't pinned by a separate flag, but by the k3s
image: --image rancher/k3s:v1.36.4-k3s1. The tag format is the
Kubernetes version plus the k3s revision suffix. In kubectl get nodes
output, the same version shows up as v1.36.4+k3s1.
kubeconfig
By default, k3d cluster create appends the new cluster to your
~/.kube/config and immediately switches the current context to it. If a
work cluster lives there, kubectl now points at the lab cluster instead.
To keep the lab's kubeconfig separate (see the kubectl lesson), create the
cluster with --kubeconfig-update-default=false and save the config to a
file:
k3d kubeconfig get lab > lab-kubeconfig
export KUBECONFIG="$PWD/lab-kubeconfig"
kubectl config current-context
k3d-lab
k3d always names the context k3d-<cluster name>.
Exposing ports
k3d has two ways to expose a port to the host, and it pays to keep them
apart. Both are written with the -p flag and differ by the node filter
after the @.
The first goes through the load balancer:
-p "8081:80@loadbalancer"
Port 8081 on the host goes to port 80 on the server nodes. Traefik (the k3s ingress controller) listens there, so this is what you need for Ingress and for Service of type LoadBalancer.
The second goes to a specific node, for NodePort:
-p "8082:30080@agent:0"
Here agent:0 is the first worker node, and 30080 is the NodePort on it.
Port 8082 is still published on the serverlb container: in k3d v5 every
mapped port goes through it, and the node filter tells the balancer which
node and port to send the traffic to. You can check what maps where like
this:
docker port k3d-lab-serverlb
| Flag | Where the port goes | What it's for |
|---|---|---|
-p "8081:80@loadbalancer" | port 80 on the server nodes, Traefik listens there | Ingress and Service of type LoadBalancer |
-p "8082:30080@agent:0" | port 30080 on the first worker node | NodePort |
The outside port and the Service's nodePort have to match. If the Service got a different nodePort, the request reaches the node, but nothing is waiting for it there.
The temptation to map the whole NodePort range at once is something the docs allow, but they warn about the cost: Docker spins up a separate proxy process for every mapped port. Across the 30000-32767 range that's nearly three thousand processes, and the machine can stall or hang for a long while.
What comes out of the box
Along with k3s, the cluster immediately gets:
- Traefik: the ingress controller, takes ports 80 and 443;
- servicelb (klipper-lb): assigns addresses for Service of type LoadBalancer;
- local-path: a StorageClass, marked as the default;
- metrics-server: the component
kubectl topneeds to work.
The last one makes life noticeably easier. In kind and minikube, metrics-server is installed separately; in k3d it's already there.
Anything extra is disabled through k3s flags, which k3d passes along via
--k3s-arg:
k3d cluster create --agents 1 --k3s-arg "--disable=traefik@server:*" --k3s-arg "--disable=servicelb@server:*"
The value format is ARG@NODEFILTER. If the cluster has more than one
node, k3d refuses to create it without a node filter, with the error
lacks a node filter. So always write the @server:* suffix. You
disable Traefik when another ingress controller takes its place, and
servicelb when you need MetalLB.
Common mistakes
The cluster ended up in the work kubeconfig. You created the cluster
without --kubeconfig-update-default=false, and the current context in
~/.kube/config switched to k3d-<name>. Check kubectl config current-context before any command that changes something.
The port is mapped to the wrong place. -p "8082:80@agent:0" with a
Service of type NodePort, or -p "8081:30080@loadbalancer" with Ingress:
the container has the port, but there's no response. Compare the output
of docker port k3d-<name>-serverlb with the Service's ports (kubectl get svc).
ZFS. On a ZFS filesystem, a cluster with more than one server node won't come up: k3s doesn't support ZFS.
BTRFS. Needs the device mapper passed into the node:
-v /dev/mapper:/dev/mapper.
NodeHasDiskPressure. Nodes are containers, and they share disk with the host. When the host runs low on space, kubelet sees the shortage and starts evicting Pods. The cause sits outside the cluster, so you won't find it inside.
Reaching the host from Pods. To reach services running on the host
machine, use the name host.k3d.internal. The address localhost inside
a Pod points at the Pod itself.
In short
- k3d runs k3s in Docker containers; k3s is a lightweight Kubernetes distribution with the same API.
- v5.9.0 ships Kubernetes v1.35.5 by default; the version is pinned by
the
rancher/k3s:<version>image. - Releases are rare: 16 months between v5.8.3 and v5.9.0. The project is alive, but its pace is slower than kind's.
--kubeconfig-update-default=falsekeeps the lab cluster out of~/.kube/config.-p "8081:80@loadbalancer"for Ingress,-p "8082:30080@agent:0"for NodePort; both ports are published onserverlb. Don't map the whole 30000-32767 range.- Traefik, servicelb, local-path and metrics-server are already in the
cluster; disable them with
--k3s-arg "--disable=…@server:*".
What you'll need for the tasks
k3d cluster createwith the flags--servers,--agents,--image,--api-port,--k3s-arg,-p,--kubeconfig-update-default=false.k3d kubeconfig get <name>andkubectl config current-context.k3d cluster list,k3d cluster delete <name>.docker port k3d-<name>-serverlb: which host port leads where.kubectl get nodes,kubectl get svc,kubectl top nodes.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress