Local Kubernetes Dev — Part 4: Setting up your workstation
Install Docker, kubectl, k3d, helm and Tilt on macOS, Linux or Windows, verify everything with a smoke test, and work around the classic install gotchas.
In the previous chapter we sorted out who does what: Docker builds images, k3d spins up the cluster, kubectl and helm manage resources, and Tilt runs the fast development loop. Now let's install the whole set and make sure it works. The goal of this chapter is for you to end up with a working toolchain on your machine, with the command kubectl get pods -A responding without errors against a local cluster.
From here on I'll assume our running example: the myapp service is an HTTP API built on Python 3.12 + FastAPI, listening on port 8080 and depending on PostgreSQL. We'll containerize and deploy the service itself in the chapters ahead; for now we're just getting the tools ready.
System requirements: what your laptop needs
The good news: a local k3d cluster is very lightweight compared to "real" Kubernetes. It's still k3s (a lightweight Kubernetes distribution) packaged into Docker containers. Even so, you'll want some headroom, because Docker, the cluster itself, your myapp service and its dependencies (PostgreSQL), and Tilt with its file watching all run side by side.
There's exactly one hard requirement, and it's about Docker: k3d runs on top of Docker version 20.10.5 or newer (with runc no older than v1.0.0-rc93). This is stated explicitly in the k3d documentation. Everything else is about host resources.
For hardware, use these as your guideline:
- CPU: x86-64 or Apple Silicon (M-series) with hardware virtualization support. Virtualization (VT-x on Intel, AMD-V on AMD, native on Apple Silicon) is mandatory — without it Docker Desktop simply won't start.
- RAM: 8 GB is a comfortable minimum, 16 GB means you never have to think about it. You can squeeze into 4 GB, but it'll be tight, especially once you bring up myapp + PostgreSQL + Tilt at the same time. By the way, Docker Desktop for Windows officially requires at least 8 GB of RAM, so 8 GB isn't a whim of mine — it's the vendor's floor.
- Disk: budget 10–20 GB of free space for Docker images, build layers, and cluster data. Images tend to accumulate, so leave yourself some slack.
If you're on Windows, here are a couple more items from the Docker Desktop requirements: you need a 64-bit processor with SLAT support, WSL 2.1.5+ installed, and a reasonably recent OS — Windows 10 22H2 or Windows 11 23H2 and newer.
Installation: Docker, kubectl, k3d, helm, tilt
We need five tools. The logical install order is bottom-up through the stack: first Docker (nothing runs without it), then kubectl, k3d, helm, and Tilt. The commands below are split by operating system.
macOS
On a Mac, the easiest way to install almost everything is via Homebrew. Docker Desktop is more convenient to download from the website (it installs both the daemon and the GUI), and the rest goes in a single command:
1# Install Docker Desktop separately from docker.com (or: brew install --cask docker)
2# Everything else in one command:
3brew install kubectl helm k3d tiltTilt is also available from Homebrew as tilt-dev/tap/tilt, but the plain tilt formula works too — see the Tilt installation guide.
Linux
On Linux, instead of Docker Desktop you usually install Docker Engine (that's the daemon itself, without a GUI) — following the official instructions for your distribution. After installing, be sure to add yourself to the docker group, otherwise every command will demand sudo (more on that gotcha in section 4.5):
1sudo usermod -aG docker $USER
2# then log out and back in so the group takes effectkubectl on Linux is installed by downloading the binary from the official Kubernetes mirror (example for amd64; for arm64 replace the path with linux/arm64):
1# kubectl Linux (amd64)
2curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
3sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectlThat's exactly the command given in the kubectl documentation for Linux. Next come k3d, helm, and Tilt via their official scripts:
1# k3d
2curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
3
4# helm (the current installer points to Helm 4)
5curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4
6chmod 700 get_helm.sh && ./get_helm.sh
7
8# tilt
9curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bashIf you have Homebrew on Linux, everything listed above (except Docker Engine itself) installs the same way, in a single command: brew install kubectl helm k3d tilt.
A note on curl ... | bash-style scripts
It's convenient, but you're running someone else's code from the internet. If you want to play it safe, download the script to a file first, read through it, and then run it. For kubectl and helm, the official docs also offer package managers (apt, dnf, snap), which is cleaner for long-term maintenance.
Windows + WSL2
Here's a key point that saves hours of frustration. Do all your dev work inside WSL2 (for example, in an Ubuntu distribution): keep the myapp repository there too, and run k3d, kubectl, helm, and Tilt from there. The filesystem inside WSL2 runs at near-native speed, whereas source files living in the Windows filesystem (C:\...) and read from WSL2 are slow and break Tilt's file sync. More on this gotcha in 4.5.
The setup is as follows:
- Install Docker Desktop for Windows with the WSL2 backend enabled. The Docker daemon will live inside WSL2, while the
dockerCLI is available from both PowerShell and your WSL2 distribution. - Then enter your WSL2 distribution (Ubuntu) and install kubectl, k3d, helm, and tilt there exactly as on Linux (see the commands above). This is the recommended path.
The alternative — installing the tools into Windows itself via the Chocolatey or Scoop package managers — is something you should only reach for if you're deliberately not working from WSL2:
1# Chocolatey
2choco install k3d kubernetes-helm k9s
3
4# Scoop (for Tilt, add the bucket first)
5scoop bucket add tilt-dev https://github.com/tilt-dev/scoop-bucket
6scoop install k3d helm k9s tiltTilt can also be installed on Windows via a PowerShell script:
1iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.ps1'))Remember that, per its documentation, Tilt requires Docker and kubectl to already be installed and a working local cluster to exist — so install Tilt last.
Optional, but handy: k9s
k9s is a terminal UI for Kubernetes. Instead of constantly typing kubectl get pods, kubectl describe, kubectl logs, you open k9s and navigate the cluster with the arrow keys: you see Pods, drop into logs, restart, delete — all from one interactive screen. For a beginner it's a great visual replacement for dozens of repetitive kubectl commands, so I highly recommend installing it.
1# macOS / Linux (Homebrew)
2brew install derailed/k9s/k9s
3
4# Windows
5winget install k9s # or scoop install k9s / choco install k9sThere are other ways too — MacPorts, snap, pacman, dnf (Fedora 42+), webi, go install, a Docker Desktop extension — all listed in the k9s repository. On compatibility: k9s prefers Kubernetes 1.28+, reads your kubeconfig, and needs read permissions (read-RBAC) in the cluster — not a problem for our local k3d, where you'll have permissions to spare. If you want 256 colors in your terminal, set TERM=xterm-256color.
Verifying the installation
After installing, run the version commands — this confirms the binaries are in place and on your PATH:
1docker version # client AND daemon; if the daemon doesn't respond, Docker isn't running
2kubectl version --client
3k3d version
4helm version
5tilt version
6k9s versiondocker version shows the versions of both the client and the daemon. If the output reports a connection error for the daemon, it means Docker isn't running (see 4.5 for how to fix this). kubectl version --client is the command straight from the kubectl documentation; the --client flag is needed so kubectl doesn't try to reach a cluster that doesn't exist yet. For helm, besides helm version, the official docs suggest helm help as a simple check that the client is available.
And now the main thing — a smoke test, that is, a check that the whole chain really works together. We'll create a temporary cluster, switch to it, look at the Pods, and delete it:
1# Create a cluster named dev
2k3d cluster create dev
3
4# Add it to kubeconfig and make it the current context
5k3d kubeconfig merge dev --kubeconfig-switch-context
6
7# We should see the k3s system Pods (coredns, traefik, metrics-server, etc.)
8kubectl get pods -A
9
10# Clean up after ourselves
11k3d cluster delete devIf kubectl get pods -A showed a list of system Pods in Running status — congratulations, your workstation is ready. The cluster name dev is no accident: in the next chapter we'll spin up a "real" dev cluster for myapp (with a registry and port forwarding) under the same name. This same sequence of commands is taken from the k3d quick-start.
Common installation problems and how to get around them
Almost every gotcha at this stage boils down to a handful of classic situations. Run through the list — chances are your problem is here.
The Docker daemon isn't running. The most common one. k3d and Tilt can't work without Docker and fail with connection refused or something similar. The fix is simple: start Docker Desktop (on Mac/Windows) or run sudo systemctl start docker (on Linux) and wait for the daemon to fully come up. Sometimes after a reboot you'll see port already in use — in that case, recreate the cluster (k3d cluster delete dev && k3d cluster create dev).
too many open files. This is probably the trickiest class of problem, and it's tied to file watching: Pods and Tilt itself watch files for changes, bumping up against system limits on file descriptors and inotify. The symptom is too many open files errors in Pod logs or in kubelet. The fix on a Linux/Docker host is to raise the inotify limits and restart Docker:
1sudo sysctl fs.inotify.max_user_watches=1048576
2sudo sysctl fs.inotify.max_user_instances=8192
3# and if needed, raise the descriptor limit: ulimit -nTo make the changes survive a reboot, put them in /etc/sysctl.conf (or a file under /etc/sysctl.d/). This problem and its workarounds are covered in detail in k3d issue #803; as a fallback, the same thread suggests creating a k3d cluster without agent nodes (server node only), which reduces the total number of open files.
An outdated Docker Desktop on macOS. Historically, some older Docker Desktop versions (around 4.3–4.4) broke k3d, and the fix was to update. The takeaway is simple: keep Docker Desktop current, especially if k3d suddenly stops creating clusters after a system update that seemingly changed nothing.
Linux: permission denied on docker.sock. If every docker command demands sudo or complains about access to the socket — you're not in the docker group. Add yourself (sudo usermod -aG docker $USER) and log out and back in. This is safe for local development.
Windows: sources in the Windows filesystem instead of WSL2. If the myapp repository lives in C:\... but you run the tools from WSL2, you'll get a slow filesystem and broken Tilt file sync (the uvicorn --reload hot reload may simply fail to trigger). Move the repository into your WSL2 distribution (into its home directory) and work from there.
Virtualization disabled in BIOS/UEFI. If Docker Desktop refuses to start with a complaint about virtualization, go into your BIOS/UEFI and enable hardware virtualization (VT-x / AMD-V; for WSL2 on some machines, SLAT as well). Without it, neither Docker Desktop nor, by extension, k3d will work.
Too little RAM. 4 GB is tight, and once you add PostgreSQL and a few Pods alongside myapp you'll start running into OOM. If you can, give Docker 8 GB or more (in Docker Desktop this is configured under Settings → Resources).
That wraps up the setup: the tools are installed, the versions are verified, and the smoke test passed. In the next chapter we'll spin up a real local cluster with k3d — this time with a built-in registry k3d-registry.localhost:5000 and port forwarding, so we have somewhere to deploy our myapp.