Do You Actually Need Local K8s?
Before setting anything up, ask yourself this: does your production run on Kubernetes? If the answer is no, you probably don’t need it locally either. Docker Compose is simpler, faster to set up, and perfectly fine for most applications.
Local K8s makes sense when you need dev/prod parity, when you’re working with CRDs or operators, or when your team already has Helm charts that define how your services run. It’s about matching your environment, not adding complexity for its own sake.
Rule of thumb: if your app is a few containers behind a reverse proxy, use Docker Compose. If you’re deploying to a K8s cluster with custom resources and service mesh, mirror that locally.
Choosing Your Local Cluster
The landscape has settled. Here’s what matters in practice:
k3d wraps k3s (Rancher’s lightweight K8s) inside Docker containers. It starts in seconds, uses minimal resources, and ships with Traefik as an ingress controller out of the box. This is what I recommend for most developers.
Kind runs K8s nodes as Docker containers. It’s the tool the Kubernetes project itself uses for testing. Excellent for CI pipelines and throwaway clusters. Slightly more verbose to configure than k3d but rock solid.
Minikube is the original local K8s tool. Still maintained, supports multiple drivers, has a rich addon ecosystem. It works, but it’s heavier than the alternatives and slower to start.
OrbStack deserves a mention if you’re on macOS. It replaces Docker Desktop entirely and includes built-in K8s support. Significantly faster and lighter on resources. If you haven’t tried it yet, you should.
For this guide, I’ll use k3d. It hits the right balance of speed, simplicity, and production similarity.
Setting Up k3d
Install k3d:
brew install k3d curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash choco install k3d Create a cluster with a local registry:
k3d cluster create dev \
--servers 1 \
--agents 2 \
--registry-create dev-registry:5050 \
--port "8080:80@loadbalancer" \
--port "8443:443@loadbalancer"
This gives you a single server node, two worker nodes, a local container registry at localhost:5050, and port mappings so you can access services via localhost:8080.
Verify everything is running:
NAME STATUS ROLES AGE VERSION
k3d-dev-server-0 Ready control-plane,master 10s v1.31.4+k3s1
k3d-dev-agent-0 Ready <none> 8s v1.31.4+k3s1
k3d-dev-agent-1 Ready <none> 8s v1.31.4+k3s1 Three nodes ready. The whole process takes about ten seconds.
Why a local registry? It avoids pushing images to Docker Hub or any remote registry during development. Build locally, push to localhost:5050, and K8s pulls from there. No network latency, no credentials to manage.
The Inner Dev Loop
Setting up a cluster is the easy part. The real question is: how do you develop against it without losing your flow?
The old approach was painful. Change code, build a Docker image, push it, update the deployment, wait for the pod to restart. That’s a two-minute feedback loop for a one-line change.
Tilt
Tilt watches your source code and syncs changes directly into running containers. No image rebuild, no push, no restart. Your changes show up in seconds.
docker_build(
'localhost:5050/api',
'./api',
live_update=[
sync('./api/src', '/app/src'),
run('npm install', trigger=['./api/package.json']),
]
)
k8s_yaml('k8s/api.yaml')
k8s_resource('api', port_forwards='3000:3000')
Run tilt up and open the dashboard. Every time you save a file, Tilt syncs it into the running container. Package.json changes trigger a fresh install. Port 3000 is forwarded to your machine.
This is the workflow that makes local K8s actually viable for daily development.
Skaffold
Skaffold is Google’s alternative. It’s more CI/CD oriented but works well for local dev too. The main difference is philosophy: Tilt gives you a dashboard and focuses on the interactive experience, Skaffold is more pipeline-driven.
apiVersion: skaffold/v4beta11
kind: Config
build:
artifacts:
- image: localhost:5050/api
context: ./api
sync:
manual:
- src: 'src/**/*.ts'
dest: /app
deploy:
kubectl:
manifests:
- k8s/*.yaml
Both work. I prefer Tilt for development and Skaffold for CI. Use what fits your team.
Managing Manifests
You need a way to define what runs in your cluster. Two mainstream options.
Your project structure should look something like this:
k8s/ ├── base/ │ ├── deployment.yaml │ ├── service.yaml │ └── kustomization.yaml ├── overlays/ │ ├── dev/ │ │ └── kustomization.yaml │ └── prod/ │ └── kustomization.yaml ├── Tiltfile └── skaffold.yaml
Helm for Dependencies
Use Helm to install third-party services. Don’t reinvent the wheel for PostgreSQL, Redis, or monitoring stacks:
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install postgres bitnami/postgresql --set auth.postgresPassword=dev
helm install redis bitnami/redis --set auth.enabled=false
Kustomize for Your Services
Use Kustomize for your own application manifests. It keeps things declarative and makes it easy to maintain environment-specific differences:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: localhost:5050/api
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: api-secrets
key: database-url
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- target:
kind: Deployment
name: api
patch: |
- op: replace
path: /spec/replicas
value: 1
Apply with kubectl apply -k k8s/overlays/dev. Same manifests, different values per environment.
A Real Workflow
Putting it all together, a typical morning looks like this:
k3d cluster start dev
tilt up
That’s it. Your cluster comes up, Tilt deploys everything, port-forwards are established, and you’re coding against live services. Change a file, save it, see the result in seconds.
When you need to debug a specific service against a shared staging cluster, Telepresence intercepts traffic from the remote cluster to your local machine:
telepresence intercept api --port 3000:3000
Now requests to the api service in staging hit your local code. Fix the bug, verify it works, push the fix.
When you’re done for the day:
tilt down
k3d cluster stop dev
Everything is disposable. No leftover containers, no orphan volumes, no state to clean up.
Final Thoughts
Local Kubernetes used to be a pain. It’s not anymore. The tools have matured, the workflows are fast, and the overhead is minimal if you choose the right stack.
The key is knowing when to use it. If K8s is part of your production story, match it locally. If it’s not, keep things simple. The best development environment is the one that gets out of your way and lets you focus on the actual problem you’re solving.