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
Navigate
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 pods, exec a shell into the selected podd, describe (equivalent tokubectl describe)e, edit in$EDITORctrl+d, delete the selected resourcectrl+k, force-kill the selected pod/, filter the current listesc, go back / close panelq, 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
Cluster lifecycle
minikube startminikube start -p name --cpus N --memory Nminikube profile listminikube stop -p nameminikube start -p nameminikube pause -p nameminikube delete -p nameContext management
kubectl config get-contextskubectl config use-context namekubectl config current-contextkubectl get pods --context=namekubectl config view --minify --context=name --raw > name.yamlexport KUBECONFIG=~/.kube/name.yamlk9s shortcuts
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.