All entries

Running Kubernetes Locally Without Losing Your Mind

Kubernetes · DevOps · Developer Experience · Local Development

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:

kubectl get nodes
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:

Project structure
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.

Share