Notebook / Infrastructure / 002
note entry no. 002 · May 03, 2026

Working with Kubernetes locally: Minikube, multiple clusters, and k9s

Iterating against a remote Kubernetes cluster means a build-push-deploy cycle on every change. Minikube gives us real clusters on our machine, several of them, where the full deployment path can be tested without consequences.

Working against a remote Kubernetes cluster has a specific cost. Build the image, push it to a registry, deploy, wait. Three minutes per cycle on a good day. When we’re iterating on a network policy, testing resource limits, or debugging a pod that won’t start, that’s not a feedback loop. It’s punishment.

Minikube runs a real Kubernetes cluster on our machine. Not a simplified version, the same API server, the same scheduler, the same RBAC model. We can run several named clusters in parallel (one per project, each with its own CPU and memory budget) and switch between them cleanly without tearing anything down.

This post covers installation on Mac, creating multiple profiles with custom resources, switching between them using kubectl contexts, exporting a per-cluster kubeconfig, and using k9s to make day-to-day interaction fast.


Installing Minikube

The cleanest path on Mac is Homebrew:

brew install minikube

Verify the installation:

minikube version

Choosing a driver

Minikube needs a driver to run the cluster VM or container. On Mac, two options work well in practice:

Docker: if Docker Desktop is already running, Minikube detects it automatically. No extra setup.

QEMU: lighter than Docker Desktop, runs natively on both Intel and Apple Silicon:

brew install qemu

If Docker Desktop is already running, use it. On Apple Silicon QEMU tends to use less memory and start faster, which is what I run on M-series machines. On Intel the difference is minimal. Either way, pin the driver so it’s not re-evaluated on each start:

minikube config set driver docker
# or
minikube config set driver qemu2

Starting a cluster

minikube start

This creates a cluster named minikube (the default profile) with 2 CPUs and 2 GB of RAM. For workloads that actually resemble production:

minikube start --cpus 4 --memory 8192

Memory is in MB, so 8192 is 8 GB. Once the cluster is up, verify it:

minikube status
kubectl cluster-info
kubectl get nodes

One node should appear in Ready state.


Multiple clusters with profiles

A profile is an independent Minikube cluster: separate VM or container, separate Kubernetes state, separate network. Profiles run in parallel without interfering with each other. This is what lets us keep one cluster per project and jump between them without tearing anything down.

Creating named clusters

# A resource-intensive microservices project
minikube start -p project-a --cpus 4 --memory 8192

# A lighter stack, a database and a couple of APIs
minikube start -p project-b --cpus 2 --memory 4096

Both clusters run independently. List all of them:

minikube profile list
|-----------|--------|---------|--------------|------|---------|---------|-------|--------|
|  Profile  |   VM   | Runtime |      IP      | Port | Version | Status  | Nodes | Active |
|-----------|--------|---------|--------------|------|---------|---------|-------|--------|
| project-a | docker | docker  | 192.168.49.2 | 8443 | v1.31.0 | Running |     1 | *      |
| project-b | docker | docker  | 192.168.49.3 | 8443 | v1.31.0 | Running |     1 |        |
|-----------|--------|---------|--------------|------|---------|---------|-------|--------|

The * marks the currently active profile.

Cluster lifecycle

# Stop without losing state
minikube stop -p project-a

# Restart (picks up exactly where it left off)
minikube start -p project-a

# Pause: frees CPU without stopping the cluster
minikube pause -p project-b

# Unpause
minikube unpause -p project-b

# Delete completely
minikube delete -p project-b

Stopping preserves the cluster state: workloads, configmaps, secrets. Deleting removes everything.


Switching between clusters

When Minikube creates a profile, it automatically adds a kubectl context with the same name to ~/.kube/config. A context is a named entry (cluster endpoint, credentials, namespace) that tells kubectl which cluster to target.

List and switch

# Show all available contexts
kubectl config get-contexts

# Switch globally
kubectl config use-context project-a
kubectl config use-context project-b

# Confirm the active context
kubectl config current-context

Per-command targeting

Switching the global context affects every terminal window. When we’re working on two projects at the same time, the --context flag targets a specific cluster without changing the global pointer:

kubectl get pods --context=project-a
kubectl get services --context=project-b

Per-session kubeconfig

For longer sessions, or for scripts that need to mirror what CI does, we can export a standalone kubeconfig for a specific cluster and point the terminal at it:

# Extract the kubeconfig for project-a
kubectl config view --minify --context=project-a --raw > ~/.kube/project-a.yaml

# In that terminal session only
export KUBECONFIG=~/.kube/project-a.yaml
kubectl get pods  # always hits project-a, regardless of global context

This isolates the session completely. No risk of accidentally deploying to the wrong cluster while a script runs.


k9s

kubectl is precise and exhaustive. Once the cluster is running, k9s is how we actually live in it: a terminal UI that shows pods, deployments, services, events, and logs in real time. I resisted it for a while because it looked like a novelty. It’s not. Live log streaming from any pod with a single keypress, exec into a container without remembering which flags go where, filter lists while watching them update. I barely open kubectl interactively anymore.

Install

brew install k9s

Connect to a cluster

By default, k9s uses the active kubectl context. To target a specific cluster without changing the global context:

# Use the active context
k9s

# Target a specific context
k9s --context project-a

# Use a standalone kubeconfig file
k9s --kubeconfig ~/.kube/project-a.yaml

k9s uses :resource commands to move between resource types. Press : and type the resource name:

  • :pods, all pods in the current namespace
  • :deploy, deployments
  • :svc, services
  • :ns, namespaces (select to switch)
  • :cm, configmaps
  • :secret, secrets

From any resource list, with the cursor on a row:

  • l, stream logs from the selected pod
  • s, exec a shell into the selected pod
  • d, describe (equivalent to kubectl describe)
  • e, edit in $EDITOR
  • ctrl+d, delete the selected resource
  • ctrl+k, force-kill the selected pod
  • /, filter the current list
  • esc, go back / close panel
  • q, quit k9s

The combination of :ns to switch namespace, l for logs, and s for exec covers most of what we do in a debugging session.


Quick reference

minikube Cluster manager
profile flag -p / --profile name
resource flags --cpus N --memory N
driver docker · qemu2
kubectl Context client
config file ~/.kube/config
per-command --context=name
per-session export KUBECONFIG=path
k9s Terminal UI
target context --context name
target kubeconfig --kubeconfig path
navigate :pods · :deploy · :svc

Cluster lifecycle

operation
minikube command
create (defaults)
minikube start
create with profile
minikube start -p name --cpus N --memory N
list all clusters
minikube profile list
stop (keep state)
minikube stop -p name
restart
minikube start -p name
pause (free CPU)
minikube pause -p name
delete completely
minikube delete -p name

Context management

operation
kubectl command
list contexts
kubectl config get-contexts
switch context
kubectl config use-context name
active context
kubectl config current-context
per-command
kubectl get pods --context=name
export kubeconfig
kubectl config view --minify --context=name --raw > name.yaml
per-session
export KUBECONFIG=~/.kube/name.yaml

k9s shortcuts

k9s
:podspod list
:deploydeployments
:svcservices
:nsnamespaces
:cmconfigmaps
:secretsecrets
lstream logs
sexec shell
ddescribe resource
eedit in $EDITOR
ctrl+ddelete resource
ctrl+kforce kill pod
/filter list
escgo back
qquit k9s

The local cluster is step one. What remains is the image step: every code change means building and pushing to a registry before Minikube sees it. We can skip the registry entirely by building directly into Minikube’s Docker daemon. Short enough for its own post.

VM

V. M. Casale

backend / cloud / things that go bump in the night

I keep an engineering notebook of the small fixes, environment tricks, and infrastructure patterns that quietly make my work-week better.

Read next.